Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RDS: Add DBSubnet resource, VPC security groups #13

Closed
wants to merge 9 commits into from
71 changes: 71 additions & 0 deletions examples/ec2-rds-with-vpc.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
let
region = "us-east-1";
accessKeyId = "AKIA...";
in
{
network.description = "NixOps RDS in a VPC Testing";
# A VPC.
resources.vpc.private = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also fix the indentation in this file please and remove any comments such as # A VPC which don't seem to be useful.

inherit region accessKeyId;
enableDnsSupport = true;
enableDnsHostnames = true;
cidrBlock = "10.0.0.0/16";
};

# 2 VPC at least.
resources.vpcSubnets = {
db-a = { resources, ... }: {
inherit region accessKeyId;
vpcId=resources.vpc.private;
cidrBlock="10.0.0.0/19";
zone="us-east-1a";
};
db-b = { resources, ... }: {
inherit region accessKeyId;
vpcId=resources.vpc.private;
zone="us-east-1c";
cidrBlock="10.0.32.0/19";
};
};


resources.ec2SecurityGroups = {
database = { resources, lib, ... }:
{
inherit region accessKeyId;
vpcId = resources.vpc.private;
rules = [
{
sourceIp = "10.0.0.0/16";
fromPort = 5432;
toPort = 5432;
}
];
};
};

resources.rdsDbSubnetGroups.db-subnet =
{resources, ...}:
{
inherit region accessKeyId;
description = "RDS test subnet";
subnetIds = (map (key: resources.vpcSubnets.${"db-" + key}) ["a" "b"]);
};

resources.rdsDbInstances.test-rds-instance =
{ resources, ... }:
{
inherit region accessKeyId;
id = "test-multi-az";
instanceClass = "db.r3.large";
allocatedStorage = 30;
masterUsername = "administrator";
masterPassword = "testing123";
port = 5432;
engine = "postgres";
dbName = "testNixOps";
multiAZ = true;
vpcSecurityGroupIds = [ resources.ec2SecurityGroups.database ];
dbSubnetGroup = resources.rdsDbSubnetGroups.db-subnet.name;
};
}
1 change: 1 addition & 0 deletions nix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ebsVolumes = evalResources ./ebs-volume.nix (zipAttrs resourcesByType.ebsVolumes or []);
elasticIPs = evalResources ./elastic-ip.nix (zipAttrs resourcesByType.elasticIPs or []);
rdsDbInstances = evalResources ./ec2-rds-dbinstance.nix (zipAttrs resourcesByType.rdsDbInstances or []);
rdsDbSubnetGroups = evalResources ./ec2-rds-subnet-group.nix (zipAttrs resourcesByType.rdsDbSubnetGroups or []);
rdsDbSecurityGroups = evalResources ./ec2-rds-dbsecurity-group.nix (zipAttrs resourcesByType.rdsDbSecurityGroups or []);
route53RecordSets = evalResources ./route53-recordset.nix (zipAttrs resourcesByType.route53RecordSets or []);
elasticFileSystems = evalResources ./elastic-file-system.nix (zipAttrs resourcesByType.elasticFileSystems or []);
Expand Down
21 changes: 21 additions & 0 deletions nix/ec2-rds-dbinstance.nix
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,30 @@ with import ./lib.nix lib;
apply = map (x: if builtins.isString x then x else "res-" + x._name);
description = ''
List of names of DBSecurityGroup to authorize on this DBInstance.
Use it if you are not a VPC or are using EC2-Classic.
'';
};

vpcSecurityGroupIds = mkOption {
default = null; # default to the default security group of the DB subnet.
type = types.nullOr (types.listOf (types.either types.str (resource "ec2-security-group")));
apply = map (x: if builtins.isString x then x else "res-" + x._name);
description = ''
List of names of VPCSecurityGroupMembership to authorize on this DBInstance.
Use this if you are in an VPC and not on EC2-Classic. Not applicable for Amazon Aurora.
'';
};

dbSubnetGroup = mkOption {
default = null;
type = types.nullOr (types.either types.str (resource "ec2-rds-db-subnet-group"));
apply = group: if (builtins.isString group || group == null) then group else "res-" + group._name;
description = ''
A name for a DBSubnetGroup.
They must contain at least one subnet in each availability zone in the AWS region.
'';
};

};

config._type = "ec2-rds-dbinstance";
Expand Down
38 changes: 38 additions & 0 deletions nix/ec2-rds-subnet-group.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{ config, lib, uuid, name, ... }:

with lib;
with import ./lib.nix lib;

{
options = {
name = mkOption {
default = "nixops-${uuid}-${name}";
type = types.str;
description = ''
Name of the RDS DB subnet group.
'';
};

description = mkOption {
type = types.str;
description = ''
Description of the RDS DB subnet group.
'';
};

imports = [ ./common-ec2-options.nix ];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addition wasn't tested, can you please make sure that it works ?
It doesn't evaluate as ./common-ec2-options.nix is a function so please make it's merged to options

options = {
   /* options declarations */
} // (import ./common-ec2-options.nix { inherit lib; });

Also if I fix that locally the diff engine will throw an error as it doesn't find a handler that realizes the tags diff, so it needs to be fixed in the python side to create a handler for the tags option.


subnetIds = mkOption {
default = [];
type = types.listOf (types.either types.str (resource "vpc-subnet"));
apply = map (x: if builtins.isString x then x else "res-" + x._name + "." + x._type + ".subnetId");
description = ''List of VPC subnets to use for this DB subnet group
(must contain at least a subnet for each AZ).
It can be a VPC Subnet resource or a string representing the ID of the VPC Subnet.
'';
};

};

config._type = "ec2-rds-db-subnet-group";
}
Empty file modified nix/vpc.nix
100755 → 100644
Empty file.
10 changes: 9 additions & 1 deletion nixopsaws/backends/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import time
import math
import shutil
import socket
import calendar
import boto.ec2
import boto.ec2.blockdevicemapping
Expand All @@ -14,6 +15,7 @@
from nixops.nix_expr import Function, Call, RawValue
from nixopsaws.resources.ebs_volume import EBSVolumeState
from nixopsaws.resources.elastic_ip import ElasticIPState
from nixopsaws.resources.ec2_rds_dbinstance import EC2RDSDbInstanceState
import nixopsaws.resources.ec2_common
import nixopsaws.resources
from nixops.util import device_name_to_boto_expected, device_name_stored_to_real, device_name_user_entered_to_stored
Expand Down Expand Up @@ -271,8 +273,14 @@ def resource_id(self):


def address_to(self, m):
if isinstance(m, EC2State): # FIXME: only if we're in the same region
# FIXME: enforce region/VPC constraints
if isinstance(m, EC2State):
return m.private_ipv4
if isinstance(m, EC2RDSDbInstanceState) and m.rds_dbinstance_endpoint is not None:
# FIXME: it returns a hostname, need DNS resolution in the VPC/region_
# FIXME: support IPv6 stack using getaddrinfo rather.
hostname, _ = m.rds_dbinstance_endpoint.split(":")
return socket.gethostbyname(hostname)
return MachineState.address_to(self, m)


Expand Down
1 change: 1 addition & 0 deletions nixopsaws/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ec2_placement_group
import ec2_rds_dbinstance
import ec2_rds_dbsecurity_group
import ec2_rds_subnet_group
import ec2_security_group
import efs_common
import elastic_file_system
Expand Down
82 changes: 72 additions & 10 deletions nixopsaws/resources/ec2_rds_dbinstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
import nixops.util
import nixopsaws.ec2_utils
import time
import sys
from uuid import uuid4

# TODO: port this file to Boto3 using https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/rds.html

class EC2RDSDbInstanceDefinition(nixops.resources.ResourceDefinition):
"""Definition of an EC2 RDS Database Instance."""

Expand Down Expand Up @@ -37,6 +40,14 @@ def __init__(self, xml):
for sg_str in sg.findall("string"):
sg_name = sg_str.get("value")
self.rds_dbinstance_security_groups.append(sg_name)
self.rds_dbinstance_vpc_security_group_ids = []
for sg in xml.findall("attrs/attr[@name='vpcSecurityGroupIds']/list"):
for sg_str in sg.findall("string"):
sg_name = sg_str.get("value")
self.rds_dbinstance_vpc_security_group_ids.append(sg_name)

self.rds_dbinstance_db_subnet_group = xml.find("attrs/attr[@name='dbSubnetGroup']/string").get("value")

# TODO: implement remainder of boto.rds.RDSConnection.create_dbinstance parameters

# common params
Expand All @@ -62,6 +73,8 @@ class EC2RDSDbInstanceState(nixops.resources.ResourceState):
rds_dbinstance_endpoint = nixops.util.attr_property("ec2.rdsEndpoint", None)
rds_dbinstance_multi_az = nixops.util.attr_property("ec2.multiAZ", False)
rds_dbinstance_security_groups = nixops.util.attr_property("ec2.securityGroups", [], "json")
rds_dbinstance_db_subnet_group = nixops.util.attr_property("ec2.rdsDbSubnetGroup", None)
rds_dbinstance_vpc_security_group_ids = nixops.util.attr_property("ec2.vpcSecurityGroupIds", [], "json")

requires_reboot_attrs = ('rds_dbinstance_id', 'rds_dbinstance_allocated_storage',
'rds_dbinstance_instance_class', 'rds_dbinstance_master_password')
Expand Down Expand Up @@ -91,7 +104,14 @@ def resource_id(self):

def create_after(self, resources, defn):
return {r for r in resources if
isinstance(r, nixopsaws.resources.ec2_rds_dbsecurity_group.EC2RDSDbSecurityGroupState)}
isinstance(r, nixopsaws.resources.ec2_rds_dbsecurity_group.EC2RDSDbSecurityGroupState)
or isinstance(r, nixopsaws.resources.ec2_rds_subnet_group.EC2RDSSubnetGroupState)
or isinstance(r, nixopsaws.resources.ec2_security_group.EC2SecurityGroupState)}

def destroy_before(self, resources):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need destroy_before ? afaik it's automatically handled by doing the opposite of create_after

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know this, as I was exploring the solution for #11

return {r for r in resources if
isinstance(r, nixopsaws.resources.ec2_rds_subnet_group.EC2RDSSubnetGroupState)
or isinstance(r, nixopsaws.resources.ec2_security_group.EC2SecurityGroupState)}

def _connect(self):
if self._conn: return
Expand Down Expand Up @@ -137,6 +157,7 @@ def _try_fetch_dbinstance(self, instance_id):
def _diff_defn(self, defn):
attrs = ('region', 'rds_dbinstance_port', 'rds_dbinstance_engine', 'rds_dbinstance_multi_az',
'rds_dbinstance_instance_class', 'rds_dbinstance_db_name', 'rds_dbinstance_master_username',
'rds_dbinstance_db_subnet_group', 'rds_dbinstance_vpc_security_group_ids',
'rds_dbinstance_master_password', 'rds_dbinstance_allocated_storage', 'rds_dbinstance_security_groups')

def get_state_attr(attr):
Expand All @@ -150,6 +171,10 @@ def get_state_attr(attr):
def get_defn_attr(attr):
if attr == 'rds_dbinstance_security_groups':
return self.fetch_security_group_resources(defn.rds_dbinstance_security_groups)
elif attr == 'rds_dbinstance_vpc_security_group_ids':
return self.fetch_vpc_security_group_resources(defn.rds_dbinstance_vpc_security_group_ids)
elif attr == 'rds_dbinstance_db_subnet_group':
return self.get_db_subnet_group_name(defn.rds_dbinstance_db_subnet_group)
else:
return getattr(defn, attr)

Expand All @@ -164,13 +189,13 @@ def _wait_for_dbinstance(self, dbinstance, state='available'):
while True:
dbinstance.update()
self.log_continue("[{0}] ".format(dbinstance.status))
if dbinstance.status not in {'creating', 'backing-up', 'available', 'modifying'}:
if dbinstance.status not in {'creating', 'backing-up', 'available', 'modifying', 'configuring-enhanced-monitoring'}:
raise Exception("RDS database instance ‘{0}’ in an error state (state is ‘{1}’)".format(dbinstance.id, dbinstance.status))
if dbinstance.status == state:
break
time.sleep(6)

def _copy_dbinstance_attrs(self, dbinstance, security_groups):
def _copy_dbinstance_attrs(self, dbinstance, security_groups=None, vpc_security_group_ids=None):
with self.depl._db:
self.rds_dbinstance_id = dbinstance.id
self.rds_dbinstance_allocated_storage = int(dbinstance.allocated_storage)
Expand All @@ -179,16 +204,20 @@ def _copy_dbinstance_attrs(self, dbinstance, security_groups):
self.rds_dbinstance_engine = dbinstance.engine
self.rds_dbinstance_multi_az = dbinstance.multi_az
self.rds_dbinstance_port = int(dbinstance.endpoint[1])
self.rds_dbinstance_endpoint = "%s:%d"% dbinstance.endpoint
self.rds_dbinstance_endpoint = "%s:%d" % dbinstance.endpoint
self.rds_dbinstance_db_subnet_group = dbinstance.subnet_group.name
self.rds_dbinstance_security_groups = security_groups
self.rds_dbinstance_vpc_security_group_ids = vpc_security_group_ids

def _to_boto_kwargs(self, attrs):
attr_to_kwarg = {
'rds_dbinstance_allocated_storage': 'allocated_storage',
'rds_dbinstance_master_password': 'master_password',
'rds_dbinstance_instance_class': 'instance_class',
'rds_dbinstance_multi_az': 'multi_az',
'rds_dbinstance_security_groups': 'security_groups'
'rds_dbinstance_security_groups': 'security_groups',
'rds_dbinstance_db_subnet_group': 'db_subnet_group_name',
'rds_dbinstance_vpc_security_group_ids': 'vpc_security_groups'
}
return { attr_to_kwarg[attr] : attrs[attr] for attr in attrs.keys() }

Expand All @@ -206,6 +235,27 @@ def fetch_security_group_resources(self, config):
security_groups.append(sg)
return security_groups

def get_db_subnet_group_name(self, group):
if isinstance(group, boto.rds.DBSubnetGroup):
return group.name

if group.startswith("res-"):
res = self.depl.get_typed_resource(group[4:].split(".")[0], "ec2-rds-subnet-group")
return res.resource_id
else:
return group

def fetch_vpc_security_group_resources(self, config):
vpc_sgs = []
for sg in config:
if sg.startswith("res-"):
res = self.depl.get_typed_resource(sg[4:].split('.')[0], "ec2-security-group")
vpc_sgs.append(res.security_group_id) # TODO: replace me by physical_spec/physical_id whatever.
else:
vpc_sgs.append(sg)

return vpc_sgs

def create(self, defn, check, allow_reboot, allow_recreate):
with self.depl._db:
self.access_key_id = defn.access_key_id or nixopsaws.ec2_utils.get_access_key_id()
Expand Down Expand Up @@ -254,13 +304,20 @@ def create(self, defn, check, allow_reboot, allow_recreate):
if not dbinstance and (self.state == self.MISSING or self.state == self.UNKNOWN):
self.logger.log("creating RDS database instance ‘{0}’ (this may take a while)...".format(defn.rds_dbinstance_id))
# create a new dbinstance with desired config
security_groups = self.fetch_security_group_resources(defn.rds_dbinstance_security_groups)
extra = {}
if defn.rds_dbinstance_security_groups:
extra['security_groups'] = self.fetch_security_group_resources(defn.rds_dbinstance_security_groups)
elif defn.rds_dbinstance_vpc_security_group_ids:
extra['vpc_security_groups'] = self.fetch_vpc_security_group_resources(defn.rds_dbinstance_vpc_security_group_ids)
if defn.rds_dbinstance_db_subnet_group:
extra['db_subnet_group_name'] = self.get_db_subnet_group_name(defn.rds_dbinstance_db_subnet_group)

dbinstance = self._conn.create_dbinstance(defn.rds_dbinstance_id,
defn.rds_dbinstance_allocated_storage, defn.rds_dbinstance_instance_class,
defn.rds_dbinstance_master_username, defn.rds_dbinstance_master_password,
port=defn.rds_dbinstance_port, engine=defn.rds_dbinstance_engine,
db_name=defn.rds_dbinstance_db_name, multi_az=defn.rds_dbinstance_multi_az,
security_groups=security_groups)
**extra)

self.state = self.STARTING
self._wait_for_dbinstance(dbinstance)
Expand All @@ -269,7 +326,9 @@ def create(self, defn, check, allow_reboot, allow_recreate):
self.access_key_id = defn.access_key_id or nixopsaws.ec2_utils.get_access_key_id()
self.rds_dbinstance_db_name = defn.rds_dbinstance_db_name
self.rds_dbinstance_master_password = defn.rds_dbinstance_master_password
self._copy_dbinstance_attrs(dbinstance, defn.rds_dbinstance_security_groups)
self._copy_dbinstance_attrs(dbinstance,
security_groups=defn.rds_dbinstance_security_groups,
vpc_security_group_ids=defn.rds_dbinstance_vpc_security_group_ids)
self.state = self.UP

with self.depl._db:
Expand All @@ -296,10 +355,12 @@ def create(self, defn, check, allow_reboot, allow_recreate):
# Ugly hack to prevent from waiting on state
# 'modifying' on sg change as that looks like it's an
# immediate change in RDS.
if not (len(boto_kwargs) == 2 and 'security_groups' in boto_kwargs):
if not (len(boto_kwargs) == 2 and ('security_groups' in boto_kwargs or 'vpc_security_groups' in boto_kwargs)):
self._wait_for_dbinstance(dbinstance, state="modifying")
self._wait_for_dbinstance(dbinstance)
self._copy_dbinstance_attrs(dbinstance, defn.rds_dbinstance_security_groups)
self._copy_dbinstance_attrs(dbinstance,
security_groups=defn.rds_dbinstance_security_groups,
vpc_security_group_ids=defn.rds_dbinstance_vpc_security_group_ids)


def after_activation(self, defn):
Expand Down Expand Up @@ -329,6 +390,7 @@ def destroy(self, wipe=False):
break
self.log_continue("[{0}] ".format(dbinstance.status))
time.sleep(6)
dbinstance.update(validate=False)

else:
self.logger.log("RDS instance `{0}` does not exist, skipping.".format(self.rds_dbinstance_id))
Expand Down
Loading