Skip to content

Instantly share code, notes, and snippets.

@KishCom
Created June 29, 2023 14:08
Show Gist options
  • Save KishCom/7ff1a995f40cbdd70801c6c3c6166377 to your computer and use it in GitHub Desktop.
Save KishCom/7ff1a995f40cbdd70801c6c3c6166377 to your computer and use it in GitHub Desktop.
AWS Static Site

Create the stack

Template.yml is the Cloudformation template defined under this

aws cloudformation create-stack \
    --stack-name STACK_NAME \
    --template-body file://template.yml \
    --profile AWS_PROFILE \
    --region us-east-1 \
    --parameters ParameterKey=DomainName,ParameterValue=DOMAIN_NAME ParameterKey=HostedZoneId,ParameterValue=HOSTED_ZONE_ID

Where

STACK_NAME - is your stack name
AWS_PROFILE - is your credentials profile in the
DOMAIN_NAME - is your domain name (the naked domain, e.g. example.com)
HOSTED_ZONE_ID - is your Route53 zone for the domain, should look like Z07414553OA4KK51T5EZA

Deploy the site

aws s3 cp ./ s3://S3_BUCKET_NAME \
    --acl public-read \
    --profile AWS_PROFILE \
    --recursive \
    --exclude "*" \
    --include index.html \
    --include 404.html \
&& aws cloudfront create-invalidation \
    --profile monithor \
    --distribution-id DISTRIBUTION_ID \
    --paths "/*"

Where:

S3_BUCKET_NAME - is your S3 bucket name
AWS_PROFILE - is your credentials profile
DISTRIBUTION_ID - is your CloudFront distribution ID

Cloudformation

The contents of template.yml

Description: Deploy a static site

Parameters:
    DomainName:
        Description: Domain name
        Type: String
    HostedZoneId:
        Description: Hosted Zone ID
        Type: String

Resources:
    S3Bucket:
        Type: AWS::S3::Bucket
        DeletionPolicy: Delete
        Properties:
            AccessControl: PublicRead
            BucketName: !Sub "${AWS::StackName}"
            WebsiteConfiguration:
                ErrorDocument: "404.html"
                IndexDocument: "index.html"

    S3BucketPolicy:
        Type: AWS::S3::BucketPolicy
        Properties:
            Bucket: !Ref S3Bucket
            PolicyDocument:
                Version: "2012-10-17"
                Statement:
                    - Effect: "Allow"
                      Action: "s3:GetObject"
                      Principal: "*"
                      Resource: !Sub "${S3Bucket.Arn}/*"
    #
    CertificateManagerCertificate:
        Type: AWS::CertificateManager::Certificate
        Properties:
            # naked domain
            DomainName: !Ref DomainName
            # add www to certificate
            SubjectAlternativeNames:
                - !Sub "www.${DomainName}"
            ValidationMethod: DNS
            DomainValidationOptions:
                # DNS record for the naked domain
                - DomainName: !Ref DomainName
                  HostedZoneId: !Ref HostedZoneId
                # DNS record for the www domain
                - DomainName: !Sub "www.${DomainName}"
                  HostedZoneId: !Ref HostedZoneId
    #
    CloudFrontDistribution:
        Type: AWS::CloudFront::Distribution
        Properties:
            DistributionConfig:
                Aliases:
                    - !Ref DomainName
                    - !Sub "www.${DomainName}"
                CustomErrorResponses:
                    - ErrorCachingMinTTL: 60
                      ErrorCode: 404
                      ResponseCode: 404
                      ResponsePagePath: "/404.html"
                DefaultCacheBehavior:
                    AllowedMethods:
                        - GET
                        - HEAD
                    CachedMethods:
                        - GET
                        - HEAD
                    Compress: true
                    DefaultTTL: 86400
                    ForwardedValues:
                        Cookies:
                            Forward: none
                        QueryString: true
                    MaxTTL: 31536000
                    SmoothStreaming: false
                    TargetOriginId: !Sub "S3-${AWS::StackName}"
                    ViewerProtocolPolicy: "redirect-to-https"
                    FunctionAssociations:
                        - EventType: viewer-request
                          FunctionARN: !GetAtt RedirectFunction.FunctionMetadata.FunctionARN
                DefaultRootObject: "index.html"
                Enabled: true
                HttpVersion: http2
                IPV6Enabled: true
                Origins:
                    - CustomOriginConfig:
                          HTTPPort: 80
                          HTTPSPort: 443
                          OriginKeepaliveTimeout: 5
                          # keep http-only to avoid 504 errors after stack creation
                          OriginProtocolPolicy: "http-only"
                          OriginReadTimeout: 30
                          OriginSSLProtocols:
                              - TLSv1
                              - TLSv1.1
                              - TLSv1.2
                      #Bucket website endpoint without http://
                      DomainName: !Join
                          - ""
                          - - !Ref S3Bucket
                            - ".s3-website-"
                            - !Ref AWS::Region
                            - ".amazonaws.com"
                      Id: !Sub "S3-${AWS::StackName}"
                PriceClass: PriceClass_All
                ViewerCertificate:
                    AcmCertificateArn: !Ref CertificateManagerCertificate
                    MinimumProtocolVersion: TLSv1.1_2016
                    SslSupportMethod: sni-only

    RedirectFunction:
        Type: AWS::CloudFront::Function
        Properties:
            AutoPublish: true
            Name: !Sub "${AWS::StackName}-redirects"
            # add the config, even if optional, the stack creation will thrown InternalFailure error otherwise
            FunctionConfig:
                Comment: !Sub "Redirect to ${DomainName}"
                Runtime: cloudfront-js-1.0
            FunctionCode: !Sub |
                function handler(event) {
                    //
                    var request = event.request;
                    var host = request.headers.host.value;

                    if (!host.startsWith("www.")) {
                        return {
                            statusCode: 301,
                            statusDescription: "Permanently moved",
                            headers: {
                                location: { value: "https://www." + host },
                            },
                        };
                    }
                    return request;
                }

    Route53RecordSetGroup:
        Type: AWS::Route53::RecordSetGroup
        Properties:
            # keep the . suffix
            HostedZoneName: !Sub "${DomainName}."
            # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html#cfn-route53-aliastarget-hostedzoneid
            RecordSets:
                - Name: !Ref DomainName
                  Type: A
                  AliasTarget:
                      DNSName: !GetAtt CloudFrontDistribution.DomainName
                      EvaluateTargetHealth: false
                      HostedZoneId: Z2FDTNDATAQYW2 # leave hardcoded, don't confuse w/ !Ref HostedZoneId
                - Name: !Sub "www.${DomainName}"
                  Type: A
                  AliasTarget:
                      DNSName: !GetAtt CloudFrontDistribution.DomainName
                      EvaluateTargetHealth: false
                      HostedZoneId: Z2FDTNDATAQYW2 # leave hardcoded, don't confuse w/ !Ref HostedZoneId

Outputs:
    WebsiteURL:
        Value: !GetAtt S3Bucket.WebsiteURL
        Description: URL for website hosted on S3
    CloudfrontDomainName:
        Value: !GetAtt CloudFrontDistribution.DomainName
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment