Skip to content

Instantly share code, notes, and snippets.

@filipeandre
Forked from techthoughts2/route53.yml
Last active July 30, 2024 10:46
Show Gist options
  • Save filipeandre/a673a8ac9e669907980f402136d3bcb8 to your computer and use it in GitHub Desktop.
Save filipeandre/a673a8ac9e669907980f402136d3bcb8 to your computer and use it in GitHub Desktop.
Creates an Amazon Route 53 hosted zone and a certificate *.hz that is automaticaly validated
AWSTemplateFormatVersion: '2010-09-09'
Description: >
This CloudFormation template validates ACM certificate using AWS Route53 DNS
service.
Parameters:
HostedZoneName:
Type: String
Description: The DNS name of an Amazon Route 53 hosted zone e.g. enterprise.filipeandre.com
AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?<!-)
ConstraintDescription: must be a valid DNS zone name.
Resources:
PortalACMCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Sub
- "portal.${HostedZoneName}"
- HostedZoneName: !Ref HostedZoneName
SubjectAlternativeNames:
- !Sub
- "www.portal.${HostedZoneName}"
- HostedZoneName: !Ref HostedZoneName
DomainValidationOptions:
- DomainName: !Sub
- "portal.${HostedZoneName}"
- HostedZoneName: !Ref HostedZoneName
ValidationDomain: !Ref HostedZoneName
- DomainName: !Sub
- "www.portal.${HostedZoneName}"
- HostedZoneName: !Ref HostedZoneName
ValidationDomain: !Ref HostedZoneName
ValidationMethod: DNS
AppACMCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Sub
- "app.${HostedZoneName}"
- HostedZoneName: !Ref HostedZoneName
SubjectAlternativeNames:
- !Sub
- "www.app.${HostedZoneName}"
- HostedZoneName: !Ref HostedZoneName
DomainValidationOptions:
- DomainName: !Sub
- "app.${HostedZoneName}"
- HostedZoneName: !Ref HostedZoneName
ValidationDomain: !Ref HostedZoneName
- DomainName: !Sub
- "www.app.${HostedZoneName}"
- HostedZoneName: !Ref HostedZoneName
ValidationDomain: !Ref HostedZoneName
ValidationMethod: DNS
RootACMCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Ref HostedZoneName
SubjectAlternativeNames:
- !Sub
- "*.${HostedZoneName}"
- HostedZoneName: !Ref HostedZoneName
DomainValidationOptions:
- DomainName: !Ref HostedZoneName
ValidationDomain: !Ref HostedZoneName
- DomainName: !Sub
- "*.${HostedZoneName}"
- HostedZoneName: !Ref HostedZoneName
ValidationDomain: !Ref HostedZoneName
ValidationMethod: DNS
ACMLambdaFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
Policies:
- PolicyName: root
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: '*'
- Effect: Allow
Action:
- route53:ChangeResourceRecordSets
- route53:ListHostedZonesByName
- cloudformation:DescribeStackEvents
Resource: '*'
ACMLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: python3.9
Timeout: '300'
Handler: index.handler
Role: !GetAtt ACMLambdaFunctionRole.Arn
Code:
ZipFile: |
#!/usr/bin/env python3
import cfnresponse
import boto3
import logging
import traceback
CFN_CLIENT = boto3.client('cloudformation')
ROUTE53_CLIENT = boto3.client('route53')
LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)
def get_route53_record_from_stack_events(stack_name):
status_reason_text = ''
params = {'StackName': stack_name}
while True:
cfn_response = CFN_CLIENT.describe_stack_events(**params)
LOGGER.info('Stack events: %s', cfn_response)
for event in cfn_response['StackEvents']:
if (
event['ResourceType'] == 'AWS::CertificateManager::Certificate' and
event['ResourceStatus'] == 'CREATE_IN_PROGRESS' and
'ResourceStatusReason' in event and
'Content of DNS Record' in event['ResourceStatusReason']
):
status_reason_text = event['ResourceStatusReason']
if 'NextToken' in cfn_response:
params['NextToken'] = cfn_response['NextToken']
if status_reason_text != '':
break
_dns_request_text=status_reason_text[status_reason_text.find("{")+1:status_reason_text.find("}")]
_name_text = _dns_request_text.split(',')[0]
_type_text = _dns_request_text.split(',')[1]
_value_text = _dns_request_text.split(',')[2]
return {
'Name': _name_text.split(': ')[1],
'Type': _type_text.split(': ')[1],
'Value': _value_text.split(': ')[1]
}
def handler(event, context):
try:
LOGGER.info('Event structure: %s', event)
if event['RequestType'] == 'Create':
stack_name = event['ResourceProperties']['StackName']
hosted_zone_name = event['ResourceProperties']['Route53HostedZoneName']
route53_record = get_route53_record_from_stack_events(stack_name)
LOGGER.info('Route 53 record: %s', route53_record)
route53_response = ROUTE53_CLIENT.list_hosted_zones_by_name(DNSName=hosted_zone_name)
hosted_zone_id = route53_response['HostedZones'][0]['Id']
route53_request_params = {
'HostedZoneId': hosted_zone_id,
'ChangeBatch': {
'Changes': [
{
'Action': 'UPSERT',
'ResourceRecordSet': {
'Name': route53_record['Name'],
'Type': route53_record['Type'],
'TTL': 60,
'ResourceRecords': [
{
'Value': route53_record['Value']
}
]
}
}
]
}
}
LOGGER.info('Route 53 request params: %s', route53_request_params)
ROUTE53_CLIENT.change_resource_record_sets(**route53_request_params)
except Exception as e:
LOGGER.error(e)
traceback.print_exc()
finally:
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
ACMCertificateValidationResource:
Type: Custom::ACMCertificateValidation
Properties:
ServiceToken: !GetAtt ACMLambdaFunction.Arn
Route53HostedZoneName: !Ref HostedZoneName
StackName: !Ref 'AWS::StackName'
Outputs:
PortalCertificateArn:
Value: !Ref PortalACMCertificate
Description: 'The portal certificate arn'
AppCertificateArn:
Value: !Ref AppACMCertificate
Description: 'The app certificate arn'
RootCertificateArn:
Value: !Ref RootACMCertificate
Description: 'The root certificate arn'
AWSTemplateFormatVersion: '2010-09-09'
Description: Creates an Amazon Route 53 hosted zone
Parameters:
DomainName:
Type: String
Description: The DNS name of an Amazon Route 53 hosted zone e.g. enterprise.filipeandre.com
AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?<!-)
ConstraintDescription: must be a valid DNS zone name.
Resources:
DNS:
Type: AWS::Route53::HostedZone
Properties:
HostedZoneConfig:
Comment: !Join ['', ['Hosted zone for ', !Ref 'DomainName']]
Name: !Ref 'DomainName'
DeleteHostedZoneRecordsLambdaFunction:
Type: 'AWS::Lambda::Function'
Properties:
Code:
ZipFile: |
import traceback
import boto3
import logging
import cfnresponse
LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)
ROUTE53_CLIENT = boto3.client('route53')
def lambda_handler(event, context):
hosted_zone_id = event['ResourceProperties']['HostedZoneId']
try:
LOGGER.info('Event structure: %s', event)
if event['RequestType'] == 'Delete':
response = ROUTE53_CLIENT.list_resource_record_sets(HostedZoneId=hosted_zone_id)
changes = []
for record_set in response['ResourceRecordSets']:
if record_set['Type'] != 'NS' and record_set['Type'] != 'SOA':
changes.append({
'Action': 'DELETE',
'ResourceRecordSet': record_set
})
if changes:
ROUTE53_CLIENT.change_resource_record_sets(HostedZoneId=hosted_zone_id, ChangeBatch={'Changes': changes})
LOGGER.info(f'Deleted all records in Hosted Zone {hosted_zone_id}')
except Exception as e:
LOGGER.error(e)
traceback.print_exc()
finally:
cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, hosted_zone_id)
Handler: index.lambda_handler
Runtime: python3.9
Timeout: 30
Role: !GetAtt LambdaExecutionRole.Arn
LambdaExecutionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: 'sts:AssumeRole'
Path: /
Policies:
- PolicyName: 'lambda-execution-policy'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: 'arn:aws:logs:*:*:*'
- Effect: Allow
Action:
- 'route53:ListResourceRecordSets'
- 'route53:ChangeResourceRecordSets'
- 'route53:DeleteHostedZone'
Resource: '*'
DeleteHostedZoneRecordsCustomResource:
Type: 'Custom::DeleteHostedZoneRecords'
Properties:
ServiceToken: !GetAtt DeleteHostedZoneRecordsLambdaFunction.Arn
HostedZoneId: !Ref DNS
Outputs:
NS:
Description: NameServers
Value: !Join [',', !GetAtt DNS.NameServers]
HostedZoneName:
Description: 'The fully qualified domain name'
Value: !Ref DomainName
Export:
Name: HostedZoneName
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment