【S3+CloudFront+ServerlessFramework】Reactをサーバレスデプロイしたい

# はじめに
学習のためフロントエンドのサーバレスデプロイに挑戦したので、詰まった箇所も併せて簡単にまとめてみます。

# やりたいこと
個人開発したToDoアプリをサーバレスデプロイしたい。挑戦するタスクを小さくしたいので今回はフロントエンドのみをデプロイする。

# ファイル構成(一部)
```
sample-project
┣ reactapp
┃  ┗ src他
┣ serverless.yml
```

# やりかた
serverless-s3-syncというプラグインを使用します
ここではプロジェクト直下(sample-project)でインストールを行いました。
```
$ npm install --save serverless-s3-sync
```

https://www.serverless.com/plugins/serverless-s3-sync

serverless.ymlは最終的に以下のように設定しました

# serverless.yml
```yaml
service: sample-project
frameworkVersion: "3"

plugins:
  - serverless-s3-sync

provider:
  name: aws
  stage: ${opt:stage, 'dev'}
  region: ap-northeast-1

  iamRoleStatements:
    - Effect: 'Allow'
      Action:
        - s3:GetObject
        - s3:PutObject
        - s3:ListBucket
        - s3:DeleteObject
        - s3:GetObjectTagging
        - s3:PutBucketPolicy
      Resource:
        - arn:aws:s3:::${Bucket}
        - arn:aws:s3:::${Bucket}/*

custom:
  s3Sync:
    - bucketName: ${self:service}-${sls:stage}
      localDir: reactapp/build

  defaultStage: dev
  deploymentBucket:
  blockPublicAccess: true

resources:
  Resources:
    Bucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}-${sls:stage}
        WebsiteConfiguration:
          IndexDocument: index.html
        PublicAccessBlockConfiguration:
          BlockPublicAcls: true
          BlockPublicPolicy: true
          IgnorePublicAcls: true
          RestrictPublicBuckets: true
    BucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref Bucket
        PolicyDocument:
          Statement:
            - Action:
                - s3:GetObject
              Effect: Allow
              Resource: !Sub arn:aws:s3:::${Bucket}/*
              Principal:
                Service: cloudfront.amazonaws.com
              Condition:
                StringEquals:
                  AWS:SourceArn: !Sub arn:aws:cloudfront::${aws:accountId}:distribution/${Distribution}    
    Distribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          DefaultCacheBehavior:
            TargetOriginId: ${self:service}-${sls:stage}
            ForwardedValues:
              QueryString: false
            ViewerProtocolPolicy: https-only
          Enabled: true
          DefaultRootObject: index.html
          Origins:
            -  Id: ${self:service}-${sls:stage}
               DomainName: !GetAtt Bucket.RegionalDomainName
               S3OriginConfig:
                OriginAccessIdentity: ''
               OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id

    CloudFrontOriginAccessControl:
      Type: AWS::CloudFront::OriginAccessControl
      Properties: 
        OriginAccessControlConfig:
          Description: Default Origin Access Control
          Name: !Ref AWS::StackName
          OriginAccessControlOriginType: s3
          SigningBehavior: always
          SigningProtocol: sigv4

  Outputs:
    URL:
      Value: !Sub "https://${Distribution.DomainName}"
    DistributionId:
      Value: !Ref Distribution
```

# 詰まった点
## S3バケットに対するポリシーの設定
作成するS3バケットのブロックパブリックアクセスの設定がオンになっているのに対して、
Principalの設定が{"AWS": "*"}になっていたため、以下のエラーが発生していました
```
CREATE_FAILED: BucketPolicy (AWS::S3::BucketPolicy)
API: s3:PutBucketPolicy Access Denied
```

```yaml
    PublicAccessBlockConfiguration:
      BlockPublicAcls: true
      BlockPublicPolicy: true
      IgnorePublicAcls: true
      RestrictPublicBuckets: true

      Statement:
        - Action:
            - s3:GetObject
          Effect: Allow
          Resource: !Sub arn:aws:s3:::${Bucket}/*
          Principal: {"AWS": "*"}
```

上記エラーについては、PrincipalをCloudFrontからのアクセスのみに指定することで解決できました
```yaml
          Principal:
            Service: cloudfront.amazonaws.com
```

## CloudFrontに対するOACの設定
OACの設定をしていなかったため、デプロイ後にディストリビューションドメイン名でアクセスしても403エラーとなっていました。これはCloudFrontがS3バケットに対するアクセス権限を持っていないためでした。このエラーについては、OACの設定をすることで解決できました。


# デプロイ実行
デプロイ前に以下のコマンドでフロントエンドのプロジェクトフォルダをビルドします
```
$ cd reactapp
$ yarn build
```

するとreactapp/buildが作成されると思います。このフォルダをs3-syncプラグインを利用してS3にアップロードします。この設定はserverless.ymlで行っています。


プロジェクト直下でデプロイコマンドを実行します
```
$ sls deploy
```

デプロイが成功したらAWSコンソールからCloudFormationにアクセスします
「出力」タブからURLにアクセスし、画面が表示されていればOK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2889192/e54bc769-84b2-2471-51fe-562ce834187a.png)




# おわりに
始めはバックエンドも併せてデプロイするつもりでしたが、トライ＆エラーで考える量が多くなってしまったので、フロントエンドに範囲を狭めて挑戦しました。次はバックエンドのデプロイに挑戦してみたいです。

# 参考


https://qiita.com/propella/items/9a16f51ea4d33b5b8e3d

https://dev.classmethod.jp/articles/cloudfront_s3_oac_setup_with_cloudformation/