diff --git a/.gitignore b/.gitignore index 803676e..d1ad5a4 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,8 @@ dmypy.json # Ignore Djanog settings files ec_api/settings/local.py + + +# Deployment related files +/.aws-sam/ +/lambda-layers/DependenciesLayer/requirements.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..755125b --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.DEFAULT_GOAL := help + +export SECRET_KEY?=badf00d +export DJANGO_SETTINGS_MODULE?=ec_api.settings.base_lambda +export APP_IS_BEHIND_CLOUDFRONT?=False + +.PHONY: all +all: clean collectstatic lambda-layers/DependenciesLayer/requirements.txt ## Rebuild everything this Makefile knows how to build + +.PHONY: clean +clean: ## Delete any generated static asset or req.txt files and git-restore the rendered API documentation file + rm -rf ec_api/static_files/ lambda-layers/DependenciesLayer/requirements.txt + +.PHONY: collectstatic +collectstatic: ## Rebuild the static assets + python manage.py collectstatic --noinput --clear + +lambda-layers/DependenciesLayer/requirements.txt: Pipfile Pipfile.lock ## Update the requirements.txt file used to build this Lambda function's DependenciesLayer + pipenv lock -r | sed "s/^-e //" >lambda-layers/DependenciesLayer/requirements.txt + +.PHONY: help +# gratuitously adapted from https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +help: ## Display this help text + @grep -E '^[-a-zA-Z0-9_/.]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%s\033[0m\n\t%s\n", $$1, $$2}' diff --git a/public-access-template.yaml b/public-access-template.yaml new file mode 100644 index 0000000..665e091 --- /dev/null +++ b/public-access-template.yaml @@ -0,0 +1,106 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: "EC API public access: TLS, CDN, DNS" + +Parameters: + StackNameSuffix: + Description: "The suffix (automatically prefixed with 'ECApi-') constructing the name of the CloudFormation Stack that created the API Gateway & Lambda function to which this Stack will attach TLS, CDN, and DNS." + Type: String + + CertificateArn: + Type: String + + PublicFqdn: + Type: String + +Resources: + + CloudFrontDistribution: + Type: 'AWS::CloudFront::Distribution' + Properties: + DistributionConfig: + Comment: 'Cloudfront Distribution pointing to Lambda origin' + Origins: + + - Id: Static + DomainName: + Fn::ImportValue: !Sub "ECApiApp-${StackNameSuffix}:ECApiFqdn" + OriginPath: "/Prod" + CustomOriginConfig: + OriginProtocolPolicy: "https-only" + OriginCustomHeaders: + - HeaderName: X-Forwarded-Host + HeaderValue: !Ref PublicFqdn + - HeaderName: X-Forwarded-Proto + HeaderValue: https + + OriginShield: + Enabled: true + OriginShieldRegion: eu-west-2 + + - Id: Dynamic + DomainName: + Fn::ImportValue: !Sub "ECApiApp-${StackNameSuffix}:ECApiFqdn" + OriginPath: "/Prod" + CustomOriginConfig: + OriginProtocolPolicy: "https-only" + OriginCustomHeaders: + - HeaderName: X-Forwarded-Host + HeaderValue: !Ref PublicFqdn + - HeaderName: X-Forwarded-Proto + HeaderValue: https + + Enabled: true + HttpVersion: 'http2' + Aliases: + - !Ref PublicFqdn + PriceClass: "PriceClass_100" + ViewerCertificate: + AcmCertificateArn: !Ref CertificateArn + MinimumProtocolVersion: TLSv1.1_2016 + SslSupportMethod: sni-only + + DefaultCacheBehavior: + AllowedMethods: [ GET, HEAD, OPTIONS ] + TargetOriginId: Dynamic + ForwardedValues: + QueryString: true + Cookies: + Forward: "all" + Headers: + - Authorization + - Origin + ViewerProtocolPolicy: "redirect-to-https" + + CacheBehaviors: + - AllowedMethods: [ GET, HEAD, OPTIONS ] + PathPattern: static/* + TargetOriginId: Static + ForwardedValues: + QueryString: true + Cookies: + Forward: none + Headers: + - Authorization + - Origin + ViewerProtocolPolicy: "redirect-to-https" + MinTTL: '50' + + + DnsRecord: + Type: AWS::Route53::RecordSet + Properties: + AliasTarget: + DNSName: !GetAtt CloudFrontDistribution.DomainName + HostedZoneId: Z2FDTNDATAQYW2 # this is an AWS-owned, global singleton required for Aliases to CloudFront + HostedZoneName: !Sub "${PublicFqdn}." + Name: !Sub "${PublicFqdn}." + Type: A + +Outputs: + CloudFrontDistributionFqdn: + Description: "The FQDN of the CloudFront distribution serving this instance." + Value: !GetAtt CloudFrontDistribution.DomainName + PublicFqdn: + Description: "The EC API's URL." + Value: !Sub "https://${PublicFqdn}/" diff --git a/samconfig.toml b/samconfig.toml new file mode 120000 index 0000000..d81bbf6 --- /dev/null +++ b/samconfig.toml @@ -0,0 +1 @@ +samconfig.toml.d/development.toml \ No newline at end of file diff --git a/samconfig.toml.d/development.toml b/samconfig.toml.d/development.toml new file mode 100644 index 0000000..7f75858 --- /dev/null +++ b/samconfig.toml.d/development.toml @@ -0,0 +1,47 @@ +# This file and its quirks are documented here: +# https://github.com/aws/aws-sam-cli/blob/develop/docs/sam-config-docs.md +version = 0.1 + +#################################################################################### +## NB: Don't insert a "default" profile in this file! ############################## +###### Only use named, per-environment profiles. ################################### +###### This will help guard against accidentally targetting the wrong environment. # +#################################################################################### + +[SYM] + +[SYM.deploy] +[SYM.deploy.parameters] +stack_name = "ECApiApp-sym-dev" +s3_bucket = "ec-api-deployment-artifacts-development-0342fgsd318" +s3_prefix = "sym-dev" +region = "eu-west-2" +confirm_changeset = false +capabilities = "CAPABILITY_IAM" +tags = 'dc-product="ec-api" dc-environment="development" dc-instance="sym-dev"' +# These parameter overrides are *not* merged with those provided directly to the +# `sam XXXX` CLI command: those provided at the CLI are the *only* ones used. +parameter_overrides = """ + AppDjangoSettingsModule=ec_api.settings.base_lambda \ + AppSecretKey=badf00d \ + AppIsBehindCloudFront=False \ +""" + +[SYM.logs] +[SYM.logs.parameters] +stack_name = "ECApiApp-sym-dev" +name = "ECApiFunction" +region = "eu-west-2" + +[SYM-public-access] +[SYM-public-access.deploy] +[SYM-public-access.deploy.parameters] +template = "public-access-template.yaml" +stack_name = "ECApiPublicAccess-sym-dev" +region = "eu-west-2" +capabilities "CAPABILITY_IAM" +parameter_overrides = """ + StackNameSuffix="sym-dev" \ + CertificateArn="arn:aws:acm:us-east-1:486957117838:certificate/26ca3576-14a4-452d-b567-b286d8287308" \ + PublicFqdn="sym-dev.ec-dev.womblelabs.co.uk" \ +""" diff --git a/template.yaml b/template.yaml new file mode 100644 index 0000000..291f2ac --- /dev/null +++ b/template.yaml @@ -0,0 +1,94 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: "EC API app: Lambda, API Gateway" + +Globals: + Function: + Timeout: 10 + Api: + BinaryMediaTypes: + - "*/*" + - "*/*" + +Parameters: + + AppSecretKey: + Description: "The SECRET_KEY environment variable passed to the app." + Type: String + + AppDjangoSettingsModule: + # NB This parameter (and how it reaches the app, and how it's set in + # developer and CI-managed deployments) is used in + # `docs/new-development-deployment.md` as a reference to demonstrate how to + # communicate variables to the app. If you modify this parameter, or remove + # it, please update the document so developers aren't left without + # guidance! + Description: "The DJANGO_SETTINGS_MODULE environment variable passed to the app." + Type: String + + AppIsBehindCloudFront: + Description: "The APP_IS_BEHIND_CLOUDFRONT environment variable passed to the app, which modifies various path- and host-related settings." + Type: String + AllowedValues: + - "True" + - "False" + Default: "False" + + AppLogRetentionDays: + Description: "The number of days that CloudWatch Logs will keep logs from the app." + Type: Number + Default: 60 + AllowedValues: [ 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653 ] + +Resources: + + DependenciesLayer: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: ./lambda-layers/DependenciesLayer/ + CompatibleRuntimes: + - python3.8 + Metadata: + BuildMethod: python3.8 + RetentionPolicy: Delete + + ECApiFunction: + Type: AWS::Serverless::Function + Properties: + Role: !Sub "arn:aws:iam::${AWS::AccountId}:role/ECApiLambdaExecutionRole" + CodeUri: . + Handler: ec_api.lambda_awsgi.lambda_handler + Layers: + - !Ref DependenciesLayer + Runtime: python3.8 + MemorySize: 192 + Environment: + Variables: + SECRET_KEY: !Ref AppSecretKey + DJANGO_SETTINGS_MODULE: !Ref AppDjangoSettingsModule + APP_IS_BEHIND_CLOUDFRONT: !Ref AppIsBehindCloudFront + Events: + HTTPRequests: + Type: Api + Properties: + Path: /{proxy+} + Method: ANY + HTTPRequestRoots: + Type: Api + Properties: + Path: / + Method: ANY + + ECApiFunctionLogGroup: + Type: AWS::Logs::LogGroup + DependsOn: [ ECApiFunction ] + Properties: + LogGroupName: !Sub /aws/lambda/${ECApiFunction} + RetentionInDays: !Ref AppLogRetentionDays + +Outputs: + ECApiFqdn: + Description: "API Gateway endpoint FQDN for EC API function" + Value: !Sub "${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com" + Export: + Name: !Join [ ":", [ !Ref "AWS::StackName", "ECApiFqdn" ] ]