Expeditive tutorial to create a reusable way to launch static websites using AWS CloudFormation.

CloudFormation

Following CloudFormation template defines resource required by a static website:

  • Storage space for website files in S3
  • CDN with HTTP2 support offered by a CloudFront distribution
  • HTTPS via AWS Certificate Manager
  • DNS Record managed by AWS Route53
cloudformation.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Creates a static website (S3, CloudFront and Route53 record)'
Parameters:
  Env:
    Type: String
    Description: 'Env type'
  DomainName:
    Type: String
    Description: 'References an existing Route53 zone. Eg. moprea.ro'
  SubDomain:
    Type: String
    Description: 'Subdomain for referenced hosted zone'
  ExistingAcmCertificate:
    Type: String
    Description: 'the Amazon Resource Name (ARN) of an AWS Certificate Manager (ACM) certificate.'
    AllowedPattern: "arn:aws:acm:.*"
  PriceClass:
    Type: String
    Description: 'CloudFront price class'
    Default: 'PriceClass_100'
    AllowedValues: ['PriceClass_100', 'PriceClass_200', 'PriceClass_All']

Conditions:
  IsDev: !Equals [!Ref Env, 'dev']

Resources:
  WebsiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: PublicRead
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: 404.html
        RoutingRules:
          -
            RedirectRule:
              ReplaceKeyWith: 'index.html'
            RoutingRuleCondition:
              KeyPrefixEquals: '/'
    DeletionPolicy: Retain

  WebsiteBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref 'WebsiteBucket'
      PolicyDocument:
        Statement:
          - Sid: PublicReadForGetBucketObjects
            Effect: Allow
            Principal: '*'
            Action: s3:GetObject
            Resource: !Sub 'arn:aws:s3:::${WebsiteBucket}/*'

  WebsiteCloudfront:
    Type: AWS::CloudFront::Distribution
    DependsOn:
      - WebsiteBucket
    Properties:
      DistributionConfig:
        Comment: !Sub 'Cloudfront Distribution for ${SubDomain}.${DomainName}'
        Origins:
          # Use S3 website endpoint to allow S3 redirect rules
          - DomainName: !Sub "${WebsiteBucket}.s3-website-${AWS::Region}.amazonaws.com"
            Id: S3Origin
            CustomOriginConfig:
              HTTPPort: '80'
              HTTPSPort: '443'
              OriginProtocolPolicy: http-only
        CustomErrorResponses:
          # Use our own custom 404 page for 404 errors and cache response for 1h
          - ErrorCode: 404
            ResponseCode: 404
            ResponsePagePath: '/404.html'
            ErrorCachingMinTTL: 3600
          # Use custom 404 pages for missing S3 objects and cache for 5 min
          - ErrorCode: 403
            ResponseCode: 404
            ResponsePagePath: '/404.html'
            ErrorCachingMinTTL: 300
        Enabled: true
        HttpVersion: 'http2'
        DefaultRootObject: 'index.html'
        Aliases:
          - !Sub '${SubDomain}.${DomainName}'
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          DefaultTTL: !If [IsDev, 0, 86400]
          Compress: true
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: false
            Cookies:
              Forward: none
          ViewerProtocolPolicy: 'redirect-to-https'
        PriceClass: !Ref PriceClass
        ViewerCertificate:
          AcmCertificateArn: !Ref ExistingAcmCertificate
          SslSupportMethod: 'sni-only'

  WebsiteDNSName:
    Type: AWS::Route53::RecordSetGroup
    Properties:
      HostedZoneName: !Sub "${DomainName}."
      RecordSets:
        - Name: !Sub '${SubDomain}.${DomainName}.'
          Type: A
          AliasTarget:
            HostedZoneId: 'Z2FDTNDATAQYW2'
            DNSName: !GetAtt WebsiteCloudfront.DomainName
        - Name: !Sub '${SubDomain}.${DomainName}.'
          Type: AAAA
          AliasTarget:
            HostedZoneId: 'Z2FDTNDATAQYW2'
            DNSName: !GetAtt WebsiteCloudfront.DomainName

Outputs:
  BucketName:
    Value: !Ref WebsiteBucket
    Description: 'Name of S3 bucket to hold website content'
  CloudfrontEndpoint:
    Value: !GetAtt WebsiteCloudfront.DomainName
    Description: 'Endpoint for Cloudfront distribution'
  WebsiteUrl:
    Value: !Sub 'https://${SubDomain}.${DomainName}'

Create stack

Fill in parameters file with your domain name, sub domain you would like to create and arn of your existing ACM.

blog-parameters.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
  {
    "ParameterKey": "Env",
    "ParameterValue": "prod"
  },
  {
    "ParameterKey": "DomainName",
    "ParameterValue": "moprea.ro"
  },
  {
    "ParameterKey": "SubDomain",
    "ParameterValue": "blog"
  },
  {
    "ParameterKey": "ExistingAcmCertificate",
    "ParameterValue": "arn:aws:acm:us-east-1:xyz:certificate/a-b-c"
  }
]

create-stackcreate-stack
1
aws cloudformation create-stack --stack-name blog --template-body file://cloudformation.yaml --parameters file://blog_parameters.json

Check AWS Console to see when your stack was fully deployed. It takes some time to provision CloudFront distribution.

Deploy code

Assuming your website contents are stored in public/ folder then you could use AWS CLI to copy this to website S3 bucket.

deploy-codes3-cp
1
aws s3 cp --recursive public/ s3://blog-websitebucket-123/