アットランタイム

dev ドメイン用のブログ環境を AWS で作成する

概要

dev ドメインを取得したので、AWS でブログホスティングする環境を構築します。 一般的に、静的なサイトの場合、S3 の Website ホスティングで構築すれば良いですが、HTTPS に対応するためには前段に CloudFront を配置する必要があります。dev ドメインではウェブブラウザで HSTS プリロードされているため HTTPS が必須となります(S3 だけでは運用できません) 本記事では、CloudFront + S3 環境を一撃(ほんとは二撃)で作成する CFn を備忘録として紹介します。

前提

  • 運用したい dev ドメインを Route53 でホスティングしている環境を想定しています。

テンプレート

以下、テンプレートとなります。注意点としては、

  • CloudFornt は us-east-1 でしか作成できないので、テンプレート内の全てのリソースは us-east-1 となる
  • ACM で証明書をリクエストしているので、検証レコードをマネジメントコンソールより追加する必要がある

作成するリソースは大体次です。 Lambda@Ege は CloudFront がサブパスの hoge/hoge/index.html に保管しないために作成しています。

  • CloudFront
  • S3
  • ACM 証明書
  • Route53 のレコード
  • Lambda@Edge

パラメータ

  • ValidationDomain には、Route53 のホストゾーンのドメインを指定する(例: konoui.dev
  • DomainName には、Web サイトのドメインを指定する(例: www.konoui.dev
  • AlternativeDomainName には、DomainName とは別に運用したい代替ドメインを指定する(例: konoui.dev
AWSTemplateFormatVersion: 2010-09-09
Description: Static contents distribution using S3 and CloudFront.
Parameters:
  ValidationDomain:
    Description: The domain name which you want to validate
    Type: String
  DomainName:
    Description: FQDN of your web sites
    Type: String
  AlternativeDomainName:
    Description: FQDN of your web sites
    Type: String

Resources:
  ACMCertificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Ref DomainName
      SubjectAlternativeNames:
        - !Ref AlternativeDomainName
      DomainValidationOptions:
        - DomainName: !Ref DomainName
          ValidationDomain: !Ref ValidationDomain
      ValidationMethod: DNS
      Tags:
        - Key: Name
          Value: !Ref DomainName

  AssetsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref DomainName

  AssetsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AssetsBucket
      PolicyDocument:
        Statement:
          - Action: s3:GetObject
            Effect: Allow
            Resource: !Sub "arn:aws:s3:::${AssetsBucket}/*"
            Principal:
              AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}"

  ReservedBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref AlternativeDomainName

  AssetsDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Aliases:
          - !Ref DomainName
          - !Ref AlternativeDomainName
        Origins:
          - Id: S3Origin
            DomainName: !GetAtt AssetsBucket.DomainName
            S3OriginConfig:
              OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
        ViewerCertificate:
          SslSupportMethod: sni-only
          AcmCertificateArn: !Ref ACMCertificate
        Enabled: true
        HttpVersion: http2
        # see https://aws.amazon.com/jp/cloudfront/pricing/
        PriceClass: PriceClass_200
        DefaultRootObject: index.html
        Comment: !Sub "${AWS::StackName} Distribution"
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: false
          ViewerProtocolPolicy: redirect-to-https
          LambdaFunctionAssociations:
            - EventType: origin-request
              LambdaFunctionARN: !Ref OriginRequestLambdaVersion
      Tags:
        - Key: Name
          Value: AWS::StackName

  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Sub "${AWS::StackName} Origin Access Identity"

  OriginRequestLambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          'use strict';
          const path = require('path');
          exports.handler = (event, context, callback) => {
              var request = event.Records[0].cf.request;
              if (!path.extname(request.uri)) {
                // Rewrite URL
                request.uri = request.uri.replace(/\/?$/, '\/index.html');
              }
              return callback(null, request);
          };          
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt OriginRequestLambdaExecutionRole.Arn
      Runtime: nodejs8.10
      Tags:
        - Key: Domain
          Value: !Ref DomainName

  OriginRequestLambdaVersion:
    Type: AWS::Lambda::Version
    Properties:
      FunctionName: !GetAtt OriginRequestLambda.Arn

  OriginRequestLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - edgelambda.amazonaws.com
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  AssetsDNSRecords:
    Type: AWS::Route53::RecordSetGroup
    Properties:
      HostedZoneName: !Sub "${ValidationDomain}."
      RecordSets:
        - Name: !Sub "${DomainName}."
          Type: A
          AliasTarget:
            # see Amazon CloudFront https://docs.aws.amazon.com/ja_jp/general/latest/gr/rande.html
            HostedZoneId: Z2FDTNDATAQYW2
            DNSName: !GetAtt AssetsDistribution.DomainName
        - Name: !Sub "${AlternativeDomainName}."
          Type: A
          AliasTarget:
            HostedZoneId: Z2FDTNDATAQYW2
            DNSName: !GetAtt AssetsDistribution.DomainName

Outputs:
  URL:
    Value: !Sub "https://${DomainName}"

デプロイ

aws cloudformation deploy --stack-name <スタック名> --template <テンプレート名> --capabilities CAPABILITY_IAM --region us-east-1

上記デプロイだけではリソースの作成は完了しません。ACM の証明書は DNS 検証としているため、ACM コンソールより DNS 検証ための DNS レコードを当該ホストゾーンに追加する必要があります。 DNS 検証終了後、CFn にて処理が継続し CloudFront 等が作成されます。