Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions cloudformation/network/aurora.json
Original file line number Diff line number Diff line change
@@ -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<AWS::EC2::Subnet::Id>"
},
"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"
]
}
}
}
}
17 changes: 17 additions & 0 deletions cloudformation/network/custom_logic.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
}
}
}
4 changes: 3 additions & 1 deletion cloudformation/network/iam.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
"Sid": "AssociateDisassociateVPCFromHostedZone",
"Action": [
"lambda:*",
"logs:*","s3:*",
"logs:*",
"s3:*",
"rds:Describe*",
"iam:PassRole",
"ec2:DescribeVpcs",
"route53:AssociateVPCWithHostedZone",
Expand Down
94 changes: 94 additions & 0 deletions lambda/rds_add_role_to_cluster/lambda_function.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions lambda/rds_add_role_to_cluster/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
boto3==1.4.4
botocore==1.4.59
109 changes: 109 additions & 0 deletions lambda/rds_ro_name/lambda_function.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions lambda/rds_ro_name/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
boto3==1.4.4
botocore==1.4.59
cfn-response==0.0.3
Loading