diff --git a/cloudformation/network/aurora.json b/cloudformation/network/aurora.json new file mode 100644 index 0000000..bfbe795 --- /dev/null +++ b/cloudformation/network/aurora.json @@ -0,0 +1,143 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Aurora RDS cluster", + "Parameters": { + "InternalDomain": { + "Description": "The internal domain name.", + "Type": "String" + }, + "HostedZoneId": { + "Description": "The internal dns zone ID.", + "Type": "AWS::Route53::HostedZone::Id" + }, + "VpcId": { + "Description": "VPC ID", + "Type": "AWS::EC2::VPC::Id" + }, + "SubnetIds": { + "Description": "Subnet ID list", + "Type": "List" + }, + "RDSReaderEndpoint": { + "Description": "Labmda ARN for getting reader endpoint", + "Type": "String" + } + }, + "Resources": { + "DatabaseSecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Extend CFN Demo DB Security Group", + "VpcId": { + "Ref": "VpcId" + } + } + }, + "DBSubnetGroup": { + "Type": "AWS::RDS::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "DB Subnet groups", + "SubnetIds": { + "Ref": "SubnetIds" + } + } + }, + "PrimaryDBCluster": { + "Type": "AWS::RDS::DBCluster", + "Properties": { + "DatabaseName": "extendDemo", + "Engine": "aurora", + "DBSubnetGroupName": { + "Ref": "DBSubnetGroup" + }, + "MasterUsername": "extendDemo", + "MasterUserPassword": "change-me", + "VpcSecurityGroupIds": [ + { + "Fn::GetAtt": [ + "DatabaseSecurityGroup", + "GroupId" + ] + } + ] + }, + "DeletionPolicy": "Snapshot" + }, + "PrimaryDBDNS": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": { + "Fn::Join": [ + "", + [ + "mysql.", + { + "Ref": "InternalDomain" + } + ] + ] + }, + "HostedZoneId": { + "Ref": "HostedZoneId" + }, + "Type": "CNAME", + "TTL": "900", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "PrimaryDBCluster", + "Endpoint.Address" + ] + } + ] + } + }, + "PrimaryDBReadOnlyDNSLambda": { + "Type": "Custom::PrimaryDBReadOnlyDNSLambda", + "Properties": { + "ServiceToken": { "Ref": "RDSReaderEndpoint" }, + "DBClusterIdentifier": { "Ref": "PrimaryDBCluster"} + } + }, + "PrimaryDBReadOnlyDNS": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": { + "Fn::Join": [ + "", + [ + "mysql-ro.", + { + "Ref": "InternalDomain" + } + ] + ] + }, + "HostedZoneId": { + "Ref": "HostedZoneId" + }, + "Type": "CNAME", + "TTL": "900", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "PrimaryDBReadOnlyDNSLambda", + "ReaderEndpoint" + ] + } + ] + } + } + }, + "Outputs": { + "DatabaseSecurityGroup": { + "Description": "Security group for Database. Use with DBSecurityGroupIngress for access.", + "Value": { + "Fn::GetAtt": [ + "DatabaseSecurityGroup", + "GroupId" + ] + } + } + } +} diff --git a/cloudformation/network/custom_logic.json b/cloudformation/network/custom_logic.json index 0298574..3838d8d 100644 --- a/cloudformation/network/custom_logic.json +++ b/cloudformation/network/custom_logic.json @@ -24,12 +24,29 @@ "Timeout" : "60", "Runtime": "python2.7" } + }, + "LambdaRDSDBClusterReaderEndpointFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "lambda_function.lambda_handler", + "Role": { "Ref" : "LambdaRoleArn" }, + "Code": { + "S3Bucket" : { "Ref": "S3Bucket" }, + "S3Key" : "builds/rds_ro_name.zip" + }, + "Timeout" : "60", + "Runtime": "python2.7" + } } }, "Outputs": { "LambdaAttachHostedZoneArn": { "Description": "Lambda attach hosted zone function arn", "Value": { "Fn::GetAtt": [ "LambdaAttachHostedZoneFunction", "Arn"] } + }, + "LambdaRDSDBClusterReaderEndpointFunction": { + "Description": "Lambda RDS DBCluster Get Reader Endpoint function arn", + "Value": { "Fn::GetAtt": [ "LambdaRDSDBClusterReaderEndpointFunction", "Arn"] } } } } diff --git a/cloudformation/network/iam.json b/cloudformation/network/iam.json index 016fe9a..7c272ab 100644 --- a/cloudformation/network/iam.json +++ b/cloudformation/network/iam.json @@ -37,7 +37,9 @@ "Sid": "AssociateDisassociateVPCFromHostedZone", "Action": [ "lambda:*", - "logs:*","s3:*", + "logs:*", + "s3:*", + "rds:Describe*", "iam:PassRole", "ec2:DescribeVpcs", "route53:AssociateVPCWithHostedZone", diff --git a/lambda/rds_add_role_to_cluster/lambda_function.py b/lambda/rds_add_role_to_cluster/lambda_function.py new file mode 100755 index 0000000..8e6c024 --- /dev/null +++ b/lambda/rds_add_role_to_cluster/lambda_function.py @@ -0,0 +1,94 @@ +import boto3 +import httplib +import json +import urlparse +import uuid + +""" +Example policy: +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "arn:aws:logs:*:*:*" + }, + { + "Effect": "Allow", + "Action": [ + "rds:AddRoleToDBCluster", + "rds:RemoveRoleFromDBCluster" + ], + "Resource": "arn:aws:rds:*:*:*" + } + ] +} +""" + +def send_response(request, response, status=None, reason=None): + if status is not None: + response['Status'] = status + + if reason is not None: + response['Reason'] = reason + + if 'ResponseURL' in request and request['ResponseURL']: + url = urlparse.urlparse(request['ResponseURL']) + body = json.dumps(response) + https = httplib.HTTPSConnection(url.hostname) + https.request('PUT', url.path+'?'+url.query, body) + + return response + +def lambda_handler(event, context=None): + + response = { + 'StackId': event['StackId'], + 'RequestId': event['RequestId'], + 'LogicalResourceId': event['LogicalResourceId'], + 'Status': 'SUCCESS' + } + + if 'PhysicalResourceId' in event: + response['PhysicalResourceId'] = event['PhysicalResourceId'] + else: + response['PhysicalResourceId'] = str(uuid.uuid4()) + + rds = boto3.client('rds') + + if event['RequestType'] == 'Delete' or event['RequestType'] == 'Update': + try: + clusterId = event['OldResourceProperties']['DBClusterIdentifier'] + roleArn = event['OldResourceProperties']['RoleArn'] + except KeyError: + return send_response(event, response, status='FAILED', reason='Missing DBClusterIdentifier or RoleArn') + + try: + rds.remove_role_from_db_cluster( + DBClusterIdentifier=clusterId, + RoleArn=roleArn + ) + except Exception as e: + return send_response(event, response, status='FAILED', reason=e.message) + + if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': + try: + clusterId = event['ResourceProperties']['DBClusterIdentifier'] + roleArn = event['ResourceProperties']['RoleArn'] + except KeyError: + return send_response(event, response, status='FAILED', reason='Missing DBClusterIdentifier or RoleArn') + + try: + rds.add_role_to_db_cluster( + DBClusterIdentifier=clusterId, + RoleArn=roleArn + ) + except Exception as e: + return send_response(event, response, status='FAILED', reason=e.message) + + return send_response(event, response) diff --git a/lambda/rds_add_role_to_cluster/requirements.txt b/lambda/rds_add_role_to_cluster/requirements.txt new file mode 100644 index 0000000..903dd18 --- /dev/null +++ b/lambda/rds_add_role_to_cluster/requirements.txt @@ -0,0 +1,2 @@ +boto3==1.4.4 +botocore==1.4.59 diff --git a/lambda/rds_ro_name/lambda_function.py b/lambda/rds_ro_name/lambda_function.py new file mode 100755 index 0000000..1184dd6 --- /dev/null +++ b/lambda/rds_ro_name/lambda_function.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python2.7 +import json +import boto3 +import urllib2 +from cfnresponse import send, SUCCESS, FAILED +import logging +from optparse import OptionParser + + +logger = logging.getLogger() +logger.setLevel(logging.INFO) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +ch.setFormatter(formatter) +logger.addHandler(ch) + +class rds_readerendpoint(object): + reason = None + response_data = None + + def __init__(self, event, context): + self.event = event + self.context = context + logger.info("Event: %s" % self.event) + logger.info("Context: %s" % self.context) + if self.context != None: + self.rds = boto3.session.Session().client('rds') + else: + self.rds = boto3.session.Session(profile_name=event['ResourceProperties']['Profile']).client('rds') + try: + self.db_cluster_id = event['ResourceProperties']['DBClusterIdentifier'] + except KeyError as e: + self.reason = "Missing required property %s" % e + logger.error(self.reason) + if self.context: + self.send_status(FAILED) + return + + def create(self, updating=False): + try: + response = self.rds.describe_db_clusters( + DBClusterIdentifier=self.db_cluster_id + ) + self.response_data = {} + self.response_data['ReaderEndpoint'] = response['DBClusters'][0]['ReaderEndpoint'] + logger.info("Response: %s" % response) + if not updating: + self.send_status(SUCCESS) + except Exception as e: + self.reason = "Describe DB Cluster call Failed %s" % e + logger.error(self.reason) + if self.context: + self.send_status(FAILED) + return + + def delete(self, updating=False): + self.send_status(SUCCESS) + + def update(self): + self.create(updating=True) + #self.delete(updating=True) + self.send_status(SUCCESS) + + def send_status(self, PASS_OR_FAIL): + send( + self.event, + self.context, + PASS_OR_FAIL, + reason=self.reason, + response_data=self.response_data + ) + +def lambda_handler(event, context): + attachment = rds_readerendpoint(event, context) + if event['RequestType'] == 'Delete': + attachment.delete() + return + if event['RequestType'] == 'Create': + attachment.create() + return + if event['RequestType'] == 'Update': + attachment.update() + return + logger.info("Received event: " + json.dumps(event, indent=2)) + if context: + send(event, context, FAILED, reason="Unknown Request Type %s" % event['RequestType']) + + +if __name__ == "__main__": + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + parser.add_option("-d","--db_cluster_id", help="Which DB Cluster.") + parser.add_option("-p","--profile", help="Profile name to use when connecting to aws.", default="default") + parser.add_option("-x","--execute", help="Execute an update create or delete.", default="Create") + (opts, args) = parser.parse_args() + + options_broken = False + if not opts.db_cluster_id: + logger.error("Must Specify DB Cluster") + options_broken = True + if options_broken: + parser.print_help() + exit(1) + if opts.execute != 'Update': + event = { 'RequestType': opts.execute, 'ResourceProperties': { 'DBClusterIdentifier': opts.db_cluster_id, 'Profile': opts.profile } } + else: + event = { 'RequestType': opts.execute, 'ResourceProperties': { 'DBClusterIdentifier': opts.db_cluster_id, 'Profile': opts.profile }, 'OldResourceProperties': { 'DBClusterIdentifier': opts.db_cluster_id, 'Profile': opts.profile } } + lambda_handler(event, None) diff --git a/lambda/rds_ro_name/requirements.txt b/lambda/rds_ro_name/requirements.txt new file mode 100644 index 0000000..52f42c0 --- /dev/null +++ b/lambda/rds_ro_name/requirements.txt @@ -0,0 +1,3 @@ +boto3==1.4.4 +botocore==1.4.59 +cfn-response==0.0.3 diff --git a/scripts/build_lambdas.sh b/scripts/build_lambdas.sh index 33aa00d..958a243 100755 --- a/scripts/build_lambdas.sh +++ b/scripts/build_lambdas.sh @@ -1,7 +1,15 @@ #!/bin/bash -#build attach hosted zone +mkdir -p builds + +# build attach hosted zone cd lambda/attach_hosted_zone/ pip install -r requirements.txt -t . -mkdir -p ../../builds zip -r ../../builds/attach_hosted_zone.zip ./* +cd - + +# build get DBCluster.ReaderEndpoint +cd lambda/rds_ro_name/ +pip install -r requirements.txt -t . +zip -r ../../builds/rds_ro_name.zip ./* +cd -