Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save huevos-y-bacon/cf875b757b975b132aea98ff33495112 to your computer and use it in GitHub Desktop.
Save huevos-y-bacon/cf875b757b975b132aea98ff33495112 to your computer and use it in GitHub Desktop.
All-in-one cloudformation stack with embedded lambda custom resource. See https://missionimpossiblecode.io/building-a-best-practice-cloudformation-custom-resource-pattern
# Source: https://missionimpossiblecode.io/building-a-best-practice-cloudformation-custom-resource-pattern
Parameters:
SpecifyVPCToUse:
Description: >
DefaultVPC - finds the VPC and configures all of its subnets for you. Otherwise type
in the VPC id of a VPC in the same region where you run the template.
All subnets and azs of the chosen vpc will be used.
The VPC and chosen subnets must be setup in a way that allows the runner instances
to resolve the DNS name and connect to port 443 on the GitLab instance URL you provide.
Default: DefaultVPC
Type: String
# While it is tempting to make the above parameter of type "AWS::EC2::VPC::Id"
# this prevents automatic discovery and usage of the DefaultVPC.
# However, if your organization NEVER uses default VPCs or disables them, changing
# the type to AWS::EC2::VPC::Id actually improves the user experience because users do not have to
# lookup VPC ids in the console.
Resources:
#Arguments: Vpc-id or "DefaultVPC"
#Returns: vpc-id, number of subnets and ordered list of subnetids and az ids.
# The index of these two return lists are correlated if it is desirable to choose less than the whole list using the CloudFormation function "Select" against both lists.
VPCInfoLambda:
Type: 'AWS::Lambda::Function'
Properties:
Description: Returns the lowercase version of a string
MemorySize: 256
Runtime: python3.8
Handler: index.handler
Role: !GetAtt CFCustomResourceLambdaRole.Arn
Timeout: 240
Code:
ZipFile: |
import logging
import traceback
import signal
import cfnresponse
import boto3
LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)
def handler(event, context):
# Setup alarm for remaining runtime minus a second
signal.alarm((int(context.get_remaining_time_in_millis() / 1000)) - 1)
try:
LOGGER.info('REQUEST RECEIVED:\n %s', event)
LOGGER.info('REQUEST RECEIVED:\n %s', context)
if event['RequestType'] == 'Delete':
LOGGER.info('DELETE!')
cfnresponse.send(event, context, "SUCCESS", {
"Message": "Resource deletion successful!"})
return
elif event['RequestType'] == 'Update':
LOGGER.info('UPDATE!')
cfnresponse.send(event, context, "SUCCESS",{
"Message": "Resource update successful!"})
elif event['RequestType'] == 'Create':
LOGGER.info('CREATE!')
request_properties = event.get('ResourceProperties', None)
VpcToGet = event['ResourceProperties'].get('VpcToGet', '')
ec2 = boto3.resource('ec2')
VpcCheckedList = []
TargetVPC = None
vpclist = ec2.vpcs.all()
for vpc in vpclist:
VpcCheckedList.append(vpc.id)
if VpcToGet == "DefaultVPC" and vpc.is_default == True:
TargetVPC=vpc
elif vpc.vpc_id == VpcToGet:
TargetVPC=vpc
if TargetVPC == None:
raise Exception(f'VPC {VpcToGet} was not found among the ones in this account and region, VPC which are: {", ".join(VpcCheckedList)}')
else:
VPCOutput = TargetVPC.id
subidlist = []
zoneidlist = []
subnets = list(TargetVPC.subnets.all())
for subnet in subnets:
subidlist.append(subnet.id)
zoneidlist.append(subnet.availability_zone)
subidOutput = ",".join(subidlist)
zoneidOutput = ",".join(zoneidlist)
if not subnets:
raise Exception(f'There are no subnets in VPC: {VpcToGet}')
LOGGER.info('subnet ids are: %s', subidOutput)
LOGGER.info('zone ids are: %s', zoneidOutput)
responseData = {}
responseData['VPC_id'] = VPCOutput
responseData['OrderedSubnetIdList'] = subidOutput
responseData['OrderedZoneIdList'] = zoneidOutput
responseData['SubnetCount'] = len(subidlist)
cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
except Exception as err:
AccountRegionInfo=f'Occured in Account {context.invoked_function_arn.split(":")[4]} in region {context.invoked_function_arn.split(":")[3]}'
FinalMsg=str(err) + ' ' + AccountRegionInfo
LOGGER.info('ERROR: %s', FinalMsg)
LOGGER.info('TRACEBACK %s', traceback.print_tb(err.__traceback__))
cfnresponse.send(event, context, "FAILED", {
"Message": "{FinalMsg}"})
def timeout_handler(_signal, _frame):
'''Handle SIGALRM'''
raise Exception('Time exceeded')
signal.signal(signal.SIGALRM, timeout_handler)
#Custom Function IAM Role Declaration
CFCustomResourceLambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: "lambda-write-logs"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: "arn:aws:logs:*:*"
- PolicyName: "describe-vpcs-and-subnets"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "ec2:DescribeVpcs"
- "ec2:DescribeSubnets"
Resource: "*"
#Calling Function to Retrieve Data
LookupVPCInfo:
Type: Custom::VPCInfo
Properties:
ServiceToken: !GetAtt VPCInfoLambda.Arn
VpcToGet: !Ref SpecifyVPCToUse
Outputs:
VPCZoneIdentifier:
Description: VPCZoneIdentifier
Value: !GetAtt LookupVPCInfo.OrderedSubnetIdList
AvailabilityZones:
Description: AvailabilityZones
Value: !GetAtt LookupVPCInfo.OrderedZoneIdList
# Value: !Split [",",!GetAtt LookupVPCInfo.OrderedZoneIdList]
# Export:
# Name:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment