/aws-static-website

Host a static website using Amazon S3 and CloudFront

Apache License 2.0Apache-2.0

irtnog-aws-static-website

This AWS CloudFormation stack hosts a static website using Amazon S3 and Amazon CloudFront.

Theory of Operation

TODO

Prerequisites

When deploying the stack with a web browser, whether interactively through the CloudFormation console or from the command line via AWS CloudShell, you must log into the AWS Console with an IAM user or role that has the developer power user job function.

Alternatively, to deploy the stack from the command line on a PC or Mac:

  • Install the AWS CLI version 2.

  • Configure AWS API access. Again, your IAM user account from which you derive your credentials must have the developer power user job function.

  • The example shell commands require a contemporary version of GNU Bash. On Windows, use the Git Bash app included with Git. On macOS, use the Terminal app.

  • Install jq. On Windows, download the 64-bit version of jq 1.6, rename jq-win64.exe to jq.exe, and copy it to a directory in the executable search path. On macOS, use MacPorts (preferred) or HomeBrew to install jq.

  • Optionally, set (and export) the environment variable AWS_PROFILE to the AWS CLI named profile to use, e.g., corpdev.

  • Optionally, set (and export) the environment variable AWS_REGION to the AWS data center cluster to which the CloudFormation stack will be deployed, e.g., us-east-1.

In the example commands shown below, these shell variables will be used as follows:

  • TEMPLATE_BUCKET is the S3 bucket hosting the CloudFormation templates, e.g., devops-library-${AWS_REGION}, cf-templates-1234567890abc-${AWS_REGION}. While you may deploy the stack from the release bucket, irtnog-aws-static-website, we recommend deploying from an S3 bucket under your control. Refer to Release Engineering for more information.

  • STACK_NAME is a globally unique ID for the CloudFormation stack, e.g., example-web-site.

  • HOSTNAME is the website's fully qualified domain name (FQDN), e.g., www.example.com.

Certificate

Amazon Certificate Manager (ACM) must have a public certificate for your website in the us-east-1 region. You may import an existing certificate or request a new one. The first FQDN on the certificate must match the website's FQDN. For example, commands similar to the following will request a new certificate from ACM and publish a domain control validation (DCV) record in the matching Amazon Route 53 hosted zone:

eval $(
    aws acm request-certificate \
        --domain-name "${HOSTNAME}" \
        --validation-method DNS \
        --output json \
    | jq -r '@sh "CERTARN=\(.CertificateArn)"'
)
CERTDCVRRNAME=""
until [ -n "${CERTDCVRRNAME}" ]
do
    sleep 5
    eval $(
        aws acm describe-certificate \
            --certificate-arn "${CERTARN}" \
            --output json \
        | jq -r '.Certificate.DomainValidationOptions[0].ResourceRecord
        | @sh "CERTDCVRRNAME=\(.Name); CERTDCVRRTYPE=\(.Type); CERTDCVRRVALUE=\(.Value)"'
    )
done
eval $(
    aws route53 list-hosted-zones-by-name --output json \
    | jq -r '.HostedZones[]|.Name as $name
    | select("'${CERTDCVRRNAME}'"|test($name))
    | @sh "ZONEID=\(.Id|split("/")[2])"'
)
aws route53 change-resource-record-sets \
    --hosted-zone-id "${ZONEID}" \
    --change-batch file:///dev/fd/0 <<EOF
{
    "Changes": [
        {
            "Action": "UPSERT",
            "ResourceRecordSet": {
                "Name": "${CERTDCVRRNAME}",
                "Type": "${CERTDCVRRTYPE}",
                "TTL": 300,
                "ResourceRecords": [
                    {
                        "Value": "${CERTDCVRRVALUE}"
                    }
                ]
            }
        }
    ]
}
EOF

Deployment

Launch the CloudFormation stack in your AWS account using one of the following links:

AWS Region Code AWS Region Name Launch
us-east-1 US East (N. Virginia) cloudformation-launch-stack
us-east-2 US East (Ohio) cloudformation-launch-stack
us-west-1 US West (N. California) cloudformation-launch-stack
us-west-2 US West (Oregon) cloudformation-launch-stack
ca-central-1 Canada (Montreal) cloudformation-launch-stack
eu-north-1 EU (Stockholm) cloudformation-launch-stack
eu-west-3 EU (Paris) cloudformation-launch-stack
eu-west-2 EU (London) cloudformation-launch-stack
eu-west-1 EU (Ireland) cloudformation-launch-stack
eu-central-1 EU (Frankfurt) cloudformation-launch-stack
eu-south-1 EU (Milan) cloudformation-launch-stack
ap-south-1 Asia Pacific (Mumbai) cloudformation-launch-stack
ap-northeast-1 Asia Pacific (Tokyo) cloudformation-launch-stack
ap-northeast-2 Asia Pacific (Seoul) cloudformation-launch-stack
ap-northeast-3 Asia Pacific (Osaka-Local) cloudformation-launch-stack
ap-southeast-1 Asia Pacific (Singapore) cloudformation-launch-stack
ap-southeast-2 Asia Pacific (Sydney) cloudformation-launch-stack
ap-southeast-3 Asia Pacific (Jakarta) cloudformation-launch-stack
ap-east-1 Asia Pacific (Hong Kong) SAR cloudformation-launch-stack
sa-east-1 South America (São Paulo) cloudformation-launch-stack
cn-north-1 China (Beijing) cloudformation-launch-stack
cn-northwest-1 China (Ningxia) cloudformation-launch-stack
us-gov-east-1 GovCloud (US-East) cloudformation-launch-stack
us-gov-west-1 GovCloud (US-West) cloudformation-launch-stack
us-gov-secret-1 AWS Secret Region (US-Secret) cloudformation-launch-stack
us-gov-topsecret-1 AWS Top Secret-East Region (US-Secret) cloudformation-launch-stack
us-gov-topsecret-2 AWS Top Secret-West Region (US-Secret) cloudformation-launch-stack
me-south-1 Middle East (Bahrain) cloudformation-launch-stack
af-south-1 Africa (Cape Town) cloudformation-launch-stack
eu-east-1 EU (Spain) cloudformation-launch-stack
eu-central-2 EU (Zurich) cloudformation-launch-stack
ap-south-2 Asia Pacific (Hyderabad) cloudformation-launch-stack
ap-southeast-3 Asia Pacific (Melbourne) cloudformation-launch-stack
me-south-2 Middle East (United Arab Emirates) cloudformation-launch-stack
eu-north-1 EU (Estonia) cloudformation-launch-stack
eu-south-1 EU (Cyprus) cloudformation-launch-stack
me-west-1 Middle East (Tel Aviv) cloudformation-launch-stack
ru-central-1 Russia (TBD) cloudformation-launch-stack
ap-southeast-4 Asia Pacific (Auckland) cloudformation-launch-stack
ca-west-1 Canada (Calgary) cloudformation-launch-stack

Alternatively, launch the stack using commands similar to the following:

eval $(
    aws acm list-certificates \
        --region us-east-1 \
        --output json \
    | jq -r '.CertificateSummaryList[]|select(.DomainName == "'${HOSTNAME}'")
    | @sh "CERTARN=\(.CertificateArn)"'
)
aws cloudformation create-stack \
    --stack-name ${STACK_NAME} \
    --template-url https://s3.amazonaws.com/${TEMPLATE_BUCKET}/irtnog-aws-static-website.yaml \
    --parameters \
        ParameterKey=TemplateBucket,ParameterValue=${TEMPLATE_BUCKET} \
        ParameterKey=FullyQualifiedDomainName,ParameterValue=${HOSTNAME} \
        ParameterKey=CertificateArn,ParameterValue=${CERTARN} \
;

To publish your website, create a CNAME record for the website's FQDN that points to the stack's DistributionFqdn output value. Note that certain restrictions apply to CNAME records.

However, if your domain is hosted in Route 53, create an alias record instead. For example, commands similar to the following will search Route 53 for a suitable hosted zone and create an alias record in it, overwriting any existing alias record:

eval $(
    aws route53 list-hosted-zones-by-name --output json \
    | jq -r '.HostedZones[]|.Name as $name|select("'${HOSTNAME}'."|test($name))
    | @sh "ZONEID=\(.Id|split("/")[2])"'
)
eval $(
    aws cloudformation describe-stacks \
        --stack-name ${STACK_NAME} \
        --output json \
    | jq -r '.Stacks[0].Outputs[]|select(.OutputKey == "DistributionFqdn")
    | @sh "DISTNAME=\(.OutputValue)"'
)
aws route53 change-resource-record-sets \
    --hosted-zone-id $ZONEID \
    --change-batch file:///dev/fd/0 <<EOF
{
    "Changes": [
        {
            "Action": "UPSERT",
            "ResourceRecordSet": {
                "Name": "${HOSTNAME}.",
                "Type": "A",
                "AliasTarget": {
                    "DNSName": "${DISTNAME}",
                    "HostedZoneId": "Z2FDTNDATAQYW2",
                    "EvaluateTargetHealth": false
                }
            }
        }
    ]
}
EOF

Maintenance

Upload the website content to the S3 bucket created by the stack.

Removal

TODO

Release Engineering

The releng.yaml CloudFormation template creates resources used to deploy the stack. Those resources must be created in the us-east-1 region due to current limitations in the construction of TemplateURL values. The production release is hosted in the us-east-1 region in a bucket named irtnog-aws-static-website, with the relevant resources created using commands similar to the following:

aws cloudformation deploy \
    --template-file releng.yaml \
    --stack-name ${RELENG_STACK_NAME} \
    --region us-east-1

To publish a release to the S3 bucket created by the above:

eval $(
    aws cloudformation describe-stacks \
        --stack-name ${RELENG_STACK_NAME} \
        --region us-east-1 \
        --output json \
    | jq -r '.Stacks[0].Outputs[]|select(.OutputKey == "BucketUrl")
    | @sh "RELENG_BUCKET_URL=\(.OutputValue)"'
)
(cd templates; aws s3 sync . ${RELENG_BUCKET_URL}templates/ --region us-east-1)
aws s3 cp irtnog-aws-static-website.yaml ${RELENG_BUCKET_URL} --region us-east-1

Contributing

TODO