AWS: CloudFormation – создание S3 Website hosting и CloudFront distribution

By | 09/05/2018
 

Пример создания хостинга статического сайта в AWS S3 и подключении к нему AWS CloudFront CDN, к которому подключается SSL сертификат из AWS ACM.

Для сайта используем домен site.azinchenko.com.

Получившийся шаблон доступен в репозитории тут>>>.

AWS::S3::Bucket

Начнём с создания корзины, используем ресурс AWS::S3::Bucket:

{
  "AWSTemplateFormatVersion": "2010-09-09",

  "Description": "AWS CloudFormation S3 website hosting with CloudFront CND stack",

  "Parameters": {
    
    "S3SiteBucketName": {
      "Description": "S3 website hosting bucket name",
      "Type": "String",
      "Default": "site.azinchenko.com"
    }
    
  },
  
  "Resources": {
    
    "SiteBucket": {
      "Type": "AWS::S3::Bucket",
      "DeletionPolicy": "Delete",
      "Properties": {
        "BucketName": {
          "Ref": "S3SiteBucketName"
        },
        "WebsiteConfiguration": {
          "IndexDocument": "index.html",
          "ErrorDocument": "404.html"
        }
      }
    }
    
  }

}

Тут в DeletionPolicy указываем удаление корзины при удалении стека, а в WebsiteConfiguration – указываем на то, что корзина будет использоваться для хостинга сайта.

В имени корзины используем имя домена, которое будет использоваться для доступа к сайту – site.azinchenko.com.

Создаём стек:

aws --profile setevoy-root cloudformation create-stack --stack-name s3-cdn-site-example --template-body file://s3_website_hostig_with_cloudfront.json

Проверяем корзину:

aws --profile setevoy-root s3api list-buckets | jq '.Buckets[] | select(.Name=="site.azinchenko.com")'
{
"Name": "site.azinchenko.com",
"CreationDate": "2018-09-05T07:33:26.000Z"
}

Создаём в ней индексный файл:

echo "This is The Website" > /tmp/index.html
aws --profile setevoy-root s3 cp /tmp/index.html s3://site.azinchenko.com
upload: ../../../../../../tmp/index.html to s3://site.azinchenko.com/index.html

Проверяем файл:

aws --profile setevoy-root s3api list-objects --bucket site.azinchenko.com | jq '.Contents[] .Key'
"index.html"

Но  если проверить доступ по HTTP сейчас – получим AccessDenied:

curl  http://site.azinchenko.com.s3-website-eu-west-1.amazonaws.com
<html>
<head><title>403 Forbidden</title></head>
<body>
<h1>403 Forbidden</h1>
<ul>
<li>Code: AccessDenied</li>
<li>Message: Access Denied</li>
...

AWS::S3::BucketPolicy

Добавляем ещё один ресурс – AWS::S3::BucketPolicy, в котором описываем доступ к корзине – разрешаем метод s3:GetObject ко всем объектам в корзине, и делаем её таким образом Publicly accessible:

...
    "BucketPolicy": {
      "Type": "AWS::S3::BucketPolicy",
      "Properties": {
        "PolicyDocument": {
          "Id": "MyPolicy",
          "Version": "2012-10-17",
          "Statement": [{
            "Sid": "PublicReadForGetBucketObjects",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": { "Fn::Join": ["", [ "arn:aws:s3:::", { "Ref": "SiteBucket" }, "/*" ]] }
          }]
        },
        "Bucket": {
          "Ref": "SiteBucket"
        }
      }
    }
...

Обновляем стек:

aws --profile setevoy-root cloudformation update-stack --stack-name s3-cdn-site-example --template-body file://s3_website_hostig_with_cloudfront.json

Проверяем:

curl  http://site.azinchenko.com.s3-website-eu-west-1.amazonaws.com
This is The Website

AWS Certificate Manager

Сертификат не будет являться частью стека, делаем его вручную, обязательно в регионе N. Virginia (us-east-1), что бы его можно было использовать в CloudFront:

AWS::CloudFront::Distribution

И последним добавляем ресурс CDN distribution.

Из интересного в его параметрах:

  • TargetOriginId – через Ref указываем ID ресурса S3 корзины
  • ViewerProtocolPolicy – редиректим HTTP запросы на HTTPS
  • Aliases – в алиасе (поле CNAME в CloudFront distribution) указываем DNS имя, которое используем для сайта, в этом примере это site.azinchenko.com, как и имя корзины
  • Origins – тут формируем URL корзины в виде имя-корзины-s3-website.eu-west-1.amazonaws.com, используя Fn::Join и Ref
  • ViewerCertificate – указываем SSL ARN, который создали выше

ARN сертификата выносим в Parameters шаблона:

...
    "SiteSSLARN": {
      "Description": "site.azinchenko.com ACM SSL ARN",
      "Type": "String",
      "Default": "arn:aws:acm:us-east-1:264***286:certificate/601dae7d-***-d29307183727"
    }
...

И добавляем ресурс AWS::CloudFront::Distribution:

...
    "CloudFrontCDN": {
      "Type": "AWS::CloudFront::Distribution",
      "Properties": {
        "DistributionConfig": {
          "Comment": "Example WebSite CDN",
          "Enabled": true,
          "DefaultRootObject" : "index.html",
          "DefaultCacheBehavior": {
            "TargetOriginId": {
              "Ref": "SiteBucket"
            },
            "ViewerProtocolPolicy": "redirect-to-https",
            "MinTTL": 0,
            "AllowedMethods": [
              "HEAD",
              "GET"
            ],
            "CachedMethods": [
              "HEAD",
              "GET"
            ],
            "ForwardedValues": {
              "QueryString": false,
              "Cookies": {
                "Forward": "none"
              }
            }
          },
          "Aliases" : [ {"Ref": "S3SiteBucketName"} ],
          "Origins": [
            {
              "DomainName": { "Fn::Join": [ ".", [ {"Ref": "SiteBucket"}, "s3-website.eu-west-1.amazonaws.com" ] ] },
              "Id": {
                "Ref": "SiteBucket"
              },
              "CustomOriginConfig": {
                "HTTPPort": "80",
                "HTTPSPort": "443",
                "OriginProtocolPolicy": "http-only"
              }
            }
          ],
          "ViewerCertificate": {
            "SslSupportMethod": "sni-only",
            "MinimumProtocolVersion": "TLSv1",
            "AcmCertificateArn": {
              "Ref": "SiteSSLARN"
            }
          }
        },
        "Tags" : [
          {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "site-cdn"] ] } }
        ]
      }
    }
...

Обновляем стек:

aws --profile setevoy-root cloudformation update-stack --stack-name s3-cdn-site-example --template-body file://s3_website_hostig_with_cloudfront.json

Проверяем:

aws --profile setevoy-root cloudfront list-distributions
{
"DistributionList": {
"Items": [
{
"Id": "E15M912IHI41HB",
"ARN": "arn:aws:cloudfront::264***286:distribution/E15M912IHI41HB",
"Status": "InProgress",
"LastModifiedTime": "2018-09-05T08:34:33.994Z",
"DomainName": "d16suo1j0j3qwj.cloudfront.net",
"Aliases": {
"Quantity": 1,
"Items": [
"site.azinchenko.com"
]
},
"Origins": {
"Quantity": 1,
"Items": [
{
"Id": "site.azinchenko.com",
"DomainName": "site.azinchenko.com.s3-website.eu-west-1.amazonaws.com",
...

Ждём, когда дистрибьюция переходит в Status == Enabled, и переходим в DNS.

Route53

Последний шаг – обновить запись site.azinchenko.com в Route53.

Выбираем тип записи IN A, и указываем Alias – CloudFront distribution:

Ждём обновления DNS у провайдера:

dig +short site.azinchenko.com
143.204.10.118
143.204.10.134
143.204.10.200
143.204.10.37

Проверяем:

curl -Lv site.azinchenko.com
* Rebuilt URL to: site.azinchenko.com/
...
< HTTP/1.1 301 Moved Permanently
< Server: CloudFront
...
< Location: https://site.azinchenko.com/
...
* Issue another request to this URL: 'https://site.azinchenko.com/'
...
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
...
* Server certificate:
*  subject: CN=site.azinchenko.com
...
< HTTP/1.1 200 OK
...
This is The Website

Готово.