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

概要

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

前提

テンプレート

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

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

パラメータ

  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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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 等が作成されます。