Permalink
Switch branches/tags
Nothing to show
Find file Copy path
91f1be7 Feb 14, 2018
2 contributors

Users who have contributed to this file

@ehammond @ryansb
605 lines (566 sloc) 21 KB
---
AWSTemplateFormatVersion: "2010-09-09"
Description: |
Static web site stack including:
* CodeCommit Git repository
* S3 bucket for web site content
* Redirect from "www." to base domain
* Access logs written to logs bucket
* ACM Certificate for SSL
* CloudFront distributions for website https access
* Route 53 hosted zone with DNS entries
* CodePipeline (source CodeCommit, invoke Lambda functions)
* S3 bucket for CodePipeline artifacts
* AWS Lambda function(s) to generate static website from Git contents
* AWS Lambda function to copy generated website to website S3 bucket
* SNS topic for CodeCommit Git change notifications
* Email address subscribed to SNS notification topic
Parameters:
# Domain: example.com
DomainName:
Type: String
Description: "The base domain name for the web site (no 'www')"
MinLength: 4
MaxLength: 253
AllowedPattern: "[a-z0-9]+[-.a-z0-9]*(\\.[a-z][a-z]+)+"
ConstraintDescription: "Provide a valid domain name using only lowercase letters, numbers, and dash (-)"
# Email address to receive Git activity notifications: you@anotherdomain.com
# CANNOT be in same domain!
NotificationEmail:
Type: String
Description: "Initial email address to receive Git change notifications"
MinLength: 6
AllowedPattern: ".+@[a-z0-9]+[-.a-z0-9]*(\\.[a-z][a-z]+)+"
ConstraintDescription: "Provide a valid email address"
DefaultTTL:
Type: Number
Description: "TTL in seconds"
Default: 30
MinimumTTL:
Description: "Minimum cache lifetime in seconds for the CloudFront distribution"
Default: 5
Type: Number
PriceClass:
Description: "Distribution price class. Default is US-only, PriceClass_All is worldwide but more expensive."
Default: PriceClass_100
AllowedValues:
- PriceClass_100
- PriceClass_200
- PriceClass_All
Type: String
GeneratorLambdaFunctionS3Bucket:
Type: String
Description: "S3 bucket containing ZIP of AWS Lambda function (static site generator)"
Default: "run.alestic.com"
GeneratorLambdaFunctionS3Key:
Type: String
Description: "S3 key containing ZIP of AWS Lambda function (static site generator)"
Default: "lambda/aws-lambda-site-generator-identity.zip"
GeneratorLambdaFunctionRuntime:
Type: String
Description: "Runtime language for AWS Lambda function (static site generator)"
Default: "python2.7"
AllowedValues:
- "python2.7"
- "nodejs"
- "nodejs4.3"
- "java8"
GeneratorLambdaFunctionHandler:
Type: String
Description: "Function Handler for AWS Lambda function (static site generator)"
Default: "index.handler"
GeneratorLambdaFunctionUserParameters:
Type: String
Description: "User parameters for AWS Lambda function (static site generator)"
Default: "unused"
MinLength: 1
MaxLength: 1000
SyncLambdaFunctionS3Bucket:
Type: String
Description: "S3 bucket containing ZIP of AWS Lambda function (sync to S3)"
Default: "run.alestic.com"
SyncLambdaFunctionS3Key:
Type: String
Description: "S3 key containing ZIP of AWS Lambda function (sync to S3)"
Default: "lambda/aws-lambda-git-backed-static-website.zip"
PreExistingGitRepository:
Description: "Optional Git repository name for pre-existing CodeCommit repository. Leave empty to have CodeCommit Repository created and managed by this stack."
Type: String
Default: ""
PreExistingHostedZoneDomain:
Description: "Optional domain name for pre-existing Route 53 hosted zone. Leave empty to have hosted zone created and managed by this stack."
Type: String
Default: ""
PreExistingSiteBucket:
Description: "Optional name of pre-existing website bucket. Leave empty to have website bucket created and managed by this stack."
Type: String
Default: ""
PreExistingRedirectBucket:
Description: "Optional name of pre-existing redirect bucket. Leave empty to have redirect bucket created and managed by this stack."
Type: String
Default: ""
PreExistingLogsBucket:
Description: "Optional name of pre-existing access logs bucket. Leave empty to have access logs bucket created and managed by this stack."
Type: String
Default: ""
PreExistingCodePipelineBucket:
Description: "Optional name of pre-existing CodePipeline artifact bucket. Leave empty to have CodePipeline bucket created and managed by this stack."
Type: String
Default: ""
Conditions:
NeedsNewGitRepository: !Equals [!Ref PreExistingGitRepository, ""]
NeedsNewHostedZone: !Equals [!Ref PreExistingHostedZoneDomain, ""]
NeedsNewSiteBucket: !Equals [!Ref PreExistingSiteBucket, ""]
NeedsNewRedirectBucket: !Equals [!Ref PreExistingRedirectBucket, ""]
NeedsNewLogsBucket: !Equals [!Ref PreExistingLogsBucket, ""]
NeedsNewCodePipelineBucket: !Equals [!Ref PreExistingCodePipelineBucket, ""]
Resources:
# Bucket for CloudFront and S3 access logs: logs.example.com
LogsBucket:
Condition: NeedsNewLogsBucket
Type: "AWS::S3::Bucket"
Properties:
BucketName: !Sub "logs.${DomainName}"
AccessControl: LogDeliveryWrite
DeletionPolicy: Retain
# Bucket for site content: example.com
SiteBucket:
Condition: NeedsNewSiteBucket
Type: "AWS::S3::Bucket"
Properties:
BucketName: !Ref DomainName
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
# logs.example.com/logs/s3/example.com/
LoggingConfiguration:
DestinationBucketName: !If [NeedsNewLogsBucket, !Ref LogsBucket, !Ref PreExistingLogsBucket]
LogFilePrefix: !Sub "logs/s3/${DomainName}/"
DeletionPolicy: Retain
# Bucket to redirect to example.com: www.example.com
RedirectBucket:
Condition: NeedsNewRedirectBucket
Type: "AWS::S3::Bucket"
Properties:
BucketName: !Sub "www.${DomainName}"
AccessControl: BucketOwnerFullControl
# logs.example.com/logs/s3/www.example.com/
LoggingConfiguration:
DestinationBucketName: !If [NeedsNewLogsBucket, !Ref LogsBucket, !Ref PreExistingLogsBucket]
LogFilePrefix: !Sub "logs/s3/www.${DomainName}/"
WebsiteConfiguration:
RedirectAllRequestsTo:
HostName: !Ref DomainName
Protocol: https
DeletionPolicy: Delete
# Bucket for CodePipeline artifact storage: codepipeline.example.com
CodePipelineBucket:
Condition: NeedsNewCodePipelineBucket
Type: "AWS::S3::Bucket"
Properties:
BucketName: !Sub "codepipeline.${DomainName}"
VersioningConfiguration:
Status: Enabled
DeletionPolicy: Retain
# Certificate for HTTPS accesss through CloudFront
Certificate:
Type: "AWS::CertificateManager::Certificate"
Properties:
DomainName: !Ref DomainName
SubjectAlternativeNames:
- !Sub "www.${DomainName}"
# CDN serves S3 content over HTTPS for example.com
CloudFrontDistribution:
Type: "AWS::CloudFront::Distribution"
Properties:
DistributionConfig:
Enabled: true
Aliases:
- !Ref DomainName
DefaultRootObject: index.html
PriceClass: !Ref PriceClass
Origins:
-
DomainName: !Join ["", [!Ref DomainName, ".", !FindInMap [RegionMap, !Ref "AWS::Region", websiteendpoint]]]
Id: S3Origin
CustomOriginConfig:
HTTPPort: 80
HTTPSPort: 443
OriginProtocolPolicy: http-only
DefaultCacheBehavior:
TargetOriginId: S3Origin
AllowedMethods:
- GET
- HEAD
Compress: true
DefaultTTL: !Ref DefaultTTL
MinTTL: !Ref MinimumTTL
ForwardedValues:
QueryString: false
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
# logs.example.com/logs/cloudfront/example.com/
Logging:
Bucket: !Join ["", [!If [NeedsNewLogsBucket, !Ref LogsBucket, !Ref PreExistingLogsBucket], ".s3.amazonaws.com"]]
Prefix: !Sub "logs/cloudfront/${DomainName}/"
IncludeCookies: false
ViewerCertificate:
AcmCertificateArn: !Ref Certificate
SslSupportMethod: sni-only
# CDN serves S3 content over HTTPS for www.example.com
RedirectCloudFrontDistribution:
Type: "AWS::CloudFront::Distribution"
Properties:
DistributionConfig:
Enabled: true
Aliases:
- !If [NeedsNewRedirectBucket, !Ref RedirectBucket, !Ref PreExistingRedirectBucket]
PriceClass: PriceClass_100
Origins:
-
DomainName: !Join ["", [!If [NeedsNewRedirectBucket, !Ref RedirectBucket, !Ref PreExistingRedirectBucket], ".", !FindInMap [RegionMap, !Ref "AWS::Region", websiteendpoint]]]
Id: RedirectS3Origin
CustomOriginConfig:
HTTPPort: 80
HTTPSPort: 443
OriginProtocolPolicy: http-only
DefaultCacheBehavior:
TargetOriginId: RedirectS3Origin
AllowedMethods:
- GET
- HEAD
DefaultTTL: !Ref DefaultTTL
MinTTL: !Ref MinimumTTL
ForwardedValues:
QueryString: false
Cookies:
Forward: none
ViewerProtocolPolicy: allow-all
# logs.example.com/logs/cloudfront/www.example.com/
Logging:
Bucket: !Join ["", [!If [NeedsNewLogsBucket, !Ref LogsBucket, !Ref PreExistingLogsBucket], ".s3.amazonaws.com"]]
Prefix: !Sub "logs/cloudfront/www.${DomainName}/"
IncludeCookies: false
ViewerCertificate:
AcmCertificateArn: !Ref Certificate
SslSupportMethod: sni-only
# DNS: example.com, www.example.com
Route53HostedZone:
Condition: NeedsNewHostedZone
Type: "AWS::Route53::HostedZone"
Properties:
HostedZoneConfig:
Comment: !Sub "Created by CloudFormation stack: ${AWS::StackName}"
Name: !Ref DomainName
DeletionPolicy: Retain
Route53RecordSetGroup:
Type: "AWS::Route53::RecordSetGroup"
Properties:
HostedZoneId: !If [NeedsNewHostedZone, !Ref Route53HostedZone, !Ref "AWS::NoValue"]
HostedZoneName: !If [NeedsNewHostedZone, !Ref "AWS::NoValue", !Sub "${PreExistingHostedZoneDomain}."]
RecordSets:
# example.com
- Name: !Sub "${DomainName}."
Type: A
# Resolve to CloudFront distribution
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2 # CloudFront
DNSName: !GetAtt CloudFrontDistribution.DomainName
# www.example.com
- Name: !Sub "www.${DomainName}."
Type: A
# Resolve to Redirect CloudFront distribution
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2 # CloudFront
DNSName: !GetAtt RedirectCloudFrontDistribution.DomainName
# SNS topic for Git repository activity. Email subscription
NotificationTopic:
Type: "AWS::SNS::Topic"
Properties:
DisplayName: !Sub "Activity in ${DomainName} Git repository"
Subscription:
- Endpoint: !Ref NotificationEmail
Protocol: email
# Git repository: example.com
GitRepository:
Condition: NeedsNewGitRepository
Type: "AWS::CodeCommit::Repository"
Properties:
RepositoryDescription: !Sub "Git repository for ${DomainName}"
RepositoryName: !Ref DomainName
Triggers:
- Name: !Sub "Activity in ${DomainName} Git repository"
DestinationArn: !Ref NotificationTopic
Events:
- all
DeletionPolicy: Retain
# IAM info for AWS Lambda functions
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: !Sub "${DomainName}-execution-policy"
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: "logs:*"
Resource: "arn:aws:logs:*:*:*"
- Effect: Allow
Action:
- codepipeline:PutJobSuccessResult
- codepipeline:PutJobFailureResult
Resource: "*"
- Effect: Allow
Action:
- s3:GetBucketLocation
- s3:ListBucket
- s3:ListBucketMultipartUploads
Resource:
- !Join ["", ["arn:aws:s3:::", !If [NeedsNewSiteBucket, !Ref SiteBucket, !Ref PreExistingSiteBucket]]]
- !Join ["", ["arn:aws:s3:::", !If [NeedsNewCodePipelineBucket, !Ref CodePipelineBucket, !Ref PreExistingCodePipelineBucket]]]
- Effect: Allow
Action:
- s3:AbortMultipartUpload
- s3:DeleteObject
- s3:GetObject
- s3:GetObjectAcl
- s3:ListMultipartUploadParts
- s3:PutObject
- s3:PutObjectAcl
Resource:
- !Join ["", ["arn:aws:s3:::", !If [NeedsNewSiteBucket, !Ref SiteBucket, !Ref PreExistingSiteBucket], "/*"]]
- !Join ["", ["arn:aws:s3:::", !If [NeedsNewCodePipelineBucket, !Ref CodePipelineBucket, !Ref PreExistingCodePipelineBucket], "/*"]]
- Effect: Allow
Action: "cloudfront:CreateInvalidation"
Resource: "*"
GeneratorLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Description: !Sub "Static site generator for ${DomainName}"
#TBD: Some static site generators might need more permissions
Role: !GetAtt LambdaExecutionRole.Arn
MemorySize: 1536
Timeout: 300
Runtime: !Ref GeneratorLambdaFunctionRuntime
Handler: !Ref GeneratorLambdaFunctionHandler
Code:
S3Bucket: !Ref GeneratorLambdaFunctionS3Bucket
S3Key: !Ref GeneratorLambdaFunctionS3Key
SyncLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Description: !Sub "Copy Git branch contents to S3 bucket for ${DomainName}"
Role: !GetAtt LambdaExecutionRole.Arn
MemorySize: 1536
Timeout: 300
Runtime: python2.7
Handler: index.handler
Code:
S3Bucket: !Ref SyncLambdaFunctionS3Bucket
S3Key: !Ref SyncLambdaFunctionS3Key
Environment:
Variables:
site_bucket: !If [NeedsNewSiteBucket, !Ref SiteBucket, !Ref PreExistingSiteBucket]
cloudfront_distribution: !Ref CloudFrontDistribution
# IAM info for CodePipeline
CodePipelineRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- "lambda.amazonaws.com"
- "codepipeline.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
Policies:
- PolicyName: "codepipeline-service"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: "*"
Resource: "*"
# CodePipeline: Pass Git contents to AWS Lambda function on Git activity
CodePipeline:
Type: "AWS::CodePipeline::Pipeline"
Properties:
Name: !Sub "${DomainName}-codepipeline"
ArtifactStore:
Type: S3
Location: !If [NeedsNewCodePipelineBucket, !Ref CodePipelineBucket, !Ref PreExistingCodePipelineBucket]
RestartExecutionOnUpdate: false
RoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/${CodePipelineRole}"
Stages:
- Name: Source
Actions:
- Name: SourceAction
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: 1
Configuration:
RepositoryName: !If [NeedsNewGitRepository, !Ref DomainName, !Ref PreExistingGitRepository]
BranchName: master
OutputArtifacts:
- Name: SiteSource
RunOrder: 1
- Name: InvokeGenerator
Actions:
- Name: InvokeAction
InputArtifacts:
- Name: SiteSource
ActionTypeId:
Category: Invoke
Owner: AWS
Provider: Lambda
Version: 1
Configuration:
FunctionName: !Ref GeneratorLambdaFunction
UserParameters: !Ref GeneratorLambdaFunctionUserParameters
OutputArtifacts:
- Name: SiteContent
RunOrder: 1
- Name: InvokeSync
Actions:
- Name: InvokeAction
InputArtifacts:
- Name: SiteContent
ActionTypeId:
Category: Invoke
Owner: AWS
Provider: Lambda
Version: 1
Configuration:
FunctionName: !Ref SyncLambdaFunction
RunOrder: 1
Mappings:
RegionMap:
ap-northeast-1:
S3hostedzoneID: "Z2M4EHUR26P7ZW"
websiteendpoint: "s3-website-ap-northeast-1.amazonaws.com"
ap-northeast-2:
S3hostedzoneID: "Z3W03O7B5YMIYP"
websiteendpoint: "s3-website.ap-northeast-2.amazonaws.com"
ap-south-1:
S3hostedzoneID: "Z11RGJOFQNVJUP"
websiteendpoint: "s3-website.ap-south-1.amazonaws.com"
ap-southeast-1:
S3hostedzoneID: "Z3O0J2DXBE1FTB"
websiteendpoint: "s3-website-ap-southeast-1.amazonaws.com"
ap-southeast-2:
S3hostedzoneID: "Z1WCIGYICN2BYD"
websiteendpoint: "s3-website-ap-southeast-2.amazonaws.com"
eu-central-1:
S3hostedzoneID: "Z21DNDUVLTQW6Q"
websiteendpoint: "s3-website.eu-central-1.amazonaws.com"
eu-west-1:
S3hostedzoneID: "Z1BKCTXD74EZPE"
websiteendpoint: "s3-website-eu-west-1.amazonaws.com"
sa-east-1:
S3hostedzoneID: "Z7KQH4QJS55SO"
websiteendpoint: "s3-website-sa-east-1.amazonaws.com"
us-east-1:
S3hostedzoneID: "Z3AQBSTGFYJSTF"
websiteendpoint: "s3-website-us-east-1.amazonaws.com"
us-east-2:
S3hostedzoneID: "Z2O1EMRO9K5GLX"
websiteendpoint: "s3-website.us-east-2.amazonaws.com"
us-west-1:
S3hostedzoneID: "Z2F56UZL2M1ACD"
websiteendpoint: "s3-website-us-west-1.amazonaws.com"
us-west-2:
S3hostedzoneID: "Z3BJ6K6RIION7M"
websiteendpoint: "s3-website-us-west-2.amazonaws.com"
Outputs:
DomainName:
Description: Domain name
Value: !Ref DomainName
RedirectDomainName:
Description: Redirect hostname
Value: !If [NeedsNewRedirectBucket, !Ref RedirectBucket, !Ref PreExistingRedirectBucket]
SiteBucket:
Value: !If [NeedsNewSiteBucket, !Ref SiteBucket, !Ref PreExistingSiteBucket]
RedirectBucket:
Value: !If [NeedsNewRedirectBucket, !Ref RedirectBucket, !Ref PreExistingRedirectBucket]
LogsBucket:
Description: S3 Bucket with access logs
Value: !If [NeedsNewLogsBucket, !Ref LogsBucket, !Ref PreExistingLogsBucket]
HostedZoneId:
Description: Route 53 Hosted Zone id
Value: !If [NeedsNewHostedZone, !Ref Route53HostedZone, "N/A"]
CloudFrontDomain:
Description: CloudFront distribution domain name
Value: !Ref CloudFrontDistribution
RedirectCloudFrontDomain:
Description: Redirect CloudFront distribution domain name
Value: !Ref RedirectCloudFrontDistribution
CodePipelineArn:
Description: CodePipeline ARN
Value: !Ref CodePipeline
GitRepositoryName:
Description: Git repository name
Value: !If [NeedsNewGitRepository, !Ref DomainName, !Ref PreExistingGitRepository]
GitCloneUrlHttp:
Description: Git https clone endpoint
Value: !If [NeedsNewGitRepository, !GetAtt GitRepository.CloneUrlHttp, "N/A"]
GitCloneUrlSsh:
Description: Git ssh clone endpoint
Value: !If [NeedsNewGitRepository, !GetAtt GitRepository.CloneUrlSsh, "N/A"]
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Website and Git repository
Parameters:
- DomainName
- Label:
default: Git Activity
Parameters:
- NotificationEmail
- Label:
default: AWS Lambda Function (static site generator)
Parameters:
- GeneratorLambdaFunctionS3Bucket
- GeneratorLambdaFunctionS3Key
- GeneratorLambdaFunctionRuntime
- GeneratorLambdaFunctionHandler
- GeneratorLambdaFunctionUserParameters
- Label:
default: AWS Lambda Function (Sync to S3)
Parameters:
- SyncLambdaFunctionS3Bucket
- SyncLambdaFunctionS3Key
- Label:
default: CloudFront CDN
Parameters:
- PriceClass
- MinimumTTL
- DefaultTTL
- Label:
default: PreExisting Resources To Use (Leave empty for stack to create and manage)
Parameters:
- PreExistingGitRepository
- PreExistingHostedZoneDomain
- PreExistingSiteBucket
- PreExistingRedirectBucket
- PreExistingLogsBucket
- PreExistingCodePipelineBucket