CloudFormationで作る！独自ドメインを使った静的Webサイト

## この記事で作れるもの

- ウェブサイト、SPAの公開
- 独自ドメインの使用
- SSL証明書を発行しセキュアな通信
- アクセスログ収集
- サーバレスアーキテクチャ

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550573/17df6c05-80a9-bf90-c179-c3366a6f4d7b.png" width=500>

## 料金
- ドメイン取得
- Route 53
- CloudFront
- S3

**構築のみ**であればドメイン取得（1円〜）、Route 53（0.5ドル）で出来ます。

## 事前準備
- [お名前ドットコム](https://www.onamae.com/)でドメイン取得
- AWS ACMで証明書発行（CloudFront使用の為
**米国東部 (バージニア北部) us-east-1**で発行してください！！）
- Route 53にホストゾーン作成

下記の記事を参考に準備して頂きACM発行リージョンを**米国東部 (バージニア北部) us-east-1**にしてください！
[[AWS] 徹底図解！お名前.comで取得したDNSをAWS Route53/Cloudfrontで管理するまでの手順
](https://deep-tacopots.tech/blog/12)
サブドメイン分の発行もお忘れなく！
`example.com`
`*.example.com`
## 事前準備が完了したら実装！
マネージメントコンソールでぽちぽち作るのは面倒なのでCloudFormationで作ります！

まずはCloudFormationを知りたいよって方は↓
[CloudFormationの全てを味わいつくせ！「AWSの全てをコードで管理する方法〜その理想と現実〜」 #cmdevio](https://dev.classmethod.jp/articles/aws-all-iac/)

まずは下記のコードをまるっとコピってyamlファイルをローカルに作成します。
例) example.yaml

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: This CloudFormation template to create static website.(S3, CloudFront, ACM)

Parameters:
  BucketName:
    Type: String

  WebsiteDomainName:
    Type: String

  CFSSLCertificateId:
    Type: String

  HostZoneId:
    Type: String

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      BucketName: !Ref BucketName
      LoggingConfiguration:
        DestinationBucketName: !Ref "S3BucketAccesslogs"
        LogFilePrefix: !Sub "s3/${AWS::StackName}"
      LifecycleConfiguration:
        Rules:
          - Id: NoncurrentVersionExpiration
            Status: Enabled
            NoncurrentVersionExpirationInDays: 45
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: Enabled
      Tags:
        - Key: CloudFormationArn
          Value: !Sub "${AWS::StackName}"

  S3BucketAccesslogs:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      AccessControl: LogDeliveryWrite
      BucketName: !Sub "${AWS::StackName}-accesslogs-${AWS::Region}-${AWS::AccountId}"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - Id: AutoDelete
            Status: Enabled
            ExpirationInDays: 10
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: CloudFormationArn
          Value: !Sub "${AWS::StackName}"

  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Ref AWS::StackName

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

  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Aliases:
          - !Ref WebsiteDomainName
        Origins:
          - DomainName: !GetAtt S3Bucket.DomainName
            Id: !Sub "S3origin-${BucketName}"
            S3OriginConfig:
              OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
        Enabled: true
        DefaultRootObject: index.html
        Logging:
          IncludeCookies: "false"
          Bucket: !Sub "${S3BucketAccesslogs}.s3-${AWS::Region}.amazonaws.com"
          Prefix: !Sub "cloudfront/${AWS::StackName}"
        CustomErrorResponses:
          - ErrorCachingMinTTL: 300
            ErrorCode: 403
            ResponseCode: 200
            ResponsePagePath: /index.html
        Comment: !Sub "${AWS::StackName}-distribution"
        DefaultCacheBehavior:
          TargetOriginId: !Sub "S3origin-${BucketName}"
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
            - GET
            - HEAD
          CachedMethods:
            - GET
            - HEAD
          DefaultTTL: 3600
          MaxTTL: 86400
          MinTTL: 60
          Compress: true
          ForwardedValues:
            Cookies:
              Forward: none
            QueryString: false
        ViewerCertificate:
          SslSupportMethod: sni-only
          AcmCertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CFSSLCertificateId}"
      Tags:
        - Key: CloudFormationArn
          Value: !Sub "${AWS::StackName}"

  DnsRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref HostZoneId
      Comment: "DNS for CloudFront"
      Name: !Ref WebsiteDomainName
      Type: A
      AliasTarget:
        HostedZoneId: "Z2FDTNDATAQYW2"
        DNSName: !GetAtt CloudFrontDistribution.DomainName

Outputs:
  DistributionID:
    Value: !Ref CloudFrontDistribution
  DomainName:
    Value: !GetAtt CloudFrontDistribution.DomainName

```
- ログ用のバケットのオブジェクトはライフサイクルで保存期間を10日にしています。
- [OAI](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html)を使用してアクセスをCloudFrontからのみに制限をしています。
- ※ パブリック設定の為[CloudFrontの料金](https://aws.amazon.com/jp/cloudfront/pricing/)が発生する可能性があります。WAF、バケットポリシーでアクセスを限定的にすることも出来ます。[Amazon S3 バケットにアクセスできる VPC エンドポイントまたは IP アドレスを指定するにはどうすればよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/block-s3-traffic-vpc-ip/)
- DefaultCacheBehaviorなど自分好みで変更して下さい。




## デプロイ :rocket:

マネージメントコンソールにログインし[CloudFormation](https://ap-northeast-1.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/)に移動します。

#### [スタックの作成]をクリック
テンプレートファイルのアップロードで先ほどローカルに保存したyamlファイルを選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550573/491a911d-d166-1a02-d1ee-451d0ac625b4.png" width=500>

### パラメータの入力
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550573/8af417d8-a318-e716-dfbb-3e432a9c0784.png" width=500>
##### スタックの名前
適当につけます

##### BucketName
S3の全リージョン（地域）にてユニークな名前のBucketNameをつけます。
使えない文字などがあるので注意！[Amazon S3 バケットの命名要件](https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/cloudtrail-s3-bucket-naming-requirements.html)

##### WebsiteDomainName
サブドメインをつけ入力します。
例）www.example.com

##### CFSSLCertificateId
ACMで発行したSSL証明書の**識別子**を入力
**米国東部 (バージニア北部) us-east-1**になっていることを確認！！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550573/9273c0f3-08b9-2896-f817-5a4bb2b43a53.png" width=500>

##### HostZoneId
Route 53に作成した**ホストゾーンのホストゾーンID**を入力
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550573/0daab79a-a3b6-0ece-d5dc-d75a9bdebdf9.png" width=500>

残りはデフォルトのまま作成！
５分ちょい位で出来ると思います:smile:

### S3にindex.htmlファイルをアップロード
CloudFormationで作成したバケット（{BucketName}がついてるもの）に適当なindex.htmlをアップロードし、
{WebsiteDomainName}でアクセス出来れば完成！

※ S3にアップロード後すぐに{WebsiteDomainName}アクセスするとアクセス拒否されるのでアップロード後30分ほど（もう少し短くても大丈夫？）待ってからアクセスしてみて下さい。

※ ファイルの更新が反映されない時はCloudFrontのキャッシュをクリアするか、キャッシュの有効期限が切れるのを待つ必要があります。


## CLIでデプロイ :rocket:

ログインの手間も省けるし個人的にはCLIでの実行がおすすめ

```shell:deploy.sh
#!/bin/bash

CFN_TEMPLATE=YOUR TEMPLETE NAME
CFN_STACK_NAME=YOUR STACK NAME
BucketName=YOUR BUCKET NAME
WebsiteDomainName=YOUR DOMAIN NAME
CFSSLCertificateId=YOUR CERTIFICATEID
HostZoneId=ROUTE53 HOSTZONEID

aws cloudformation deploy --template ${CFN_TEMPLATE} --stack-name ${CFN_STACK_NAME} \
    --parameter-overrides \
    BucketName=${BucketName} \
    WebsiteDomainName=${WebsiteDomainName} \
    CFSSLCertificateId=${CFSSLCertificateId} \
    HostZoneId=${HostZoneId}

```

```terminal
$ sh deploy.sh

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - YOUR STACK NAME
```


## CodePipelineを使って自動デプロイを追加実装したい方
マネージメントコンソール実装です↓
[CodePipelineで誰でもお手軽自動デプロイ（静的webページ編）](https://qiita.com/Kouichi_Itagaki/items/8cf3e28d66238ae01422)

## 参考

[CloudFormationの全てを味わいつくせ！「AWSの全てをコードで管理する方法〜その理想と現実〜」 #cmdevio](https://dev.classmethod.jp/articles/aws-all-iac/)
[5分でできるS3とCloudFrontを利用したセキュアな静的Webサイトの作り方](https://dev.classmethod.jp/articles/cfn-s3-webhosting-cloudfront/)
[AWS::CloudFront::Distribution
](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-distribution.html)[AWS::Route53::RecordSet
](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-route53-recordset.html)