[アップデート] AWS Copilot に静的サイト用のサービスデプロイ機能が追加されました

2023.05.26

いわさです。

AWS Copilot という CLI ツールを使うことで、ECS や App Runner などのコンテナワークロードのインフラストラクチャ一式を迅速に構築することが出来ます。

本日の AWS Copilot のアップデートで Copilot で作成するサービスタイプに「静的サイト」を選択することが出来るようになりました。

これまでは AWS Copilot でサポートしていたのはサーバーサイドの動的ウェブサイトまででした。
そのため、Copilot のワークショップなどでも、次のように SPA のフロントエンド部分はコンテナ化してデプロイしていました。(あるいは手動で CloudFront や S3 を構成してフロントエンド部分を個別にデプロイする必要がありました)

Copilot Primer Workshop より

今回追加されたサービスタイプ「静的サイト」を使うと、従来のインフラストラクチャに加えて静的コンテンツを CloudFront + S3 へデプロイすることが出来るようになります。

単純な静的コンテンツをホスティングしてみる

静的サイトタイプは AWS Colito v1.28.0 以上から利用が可能です。
事前に以下に従ってインストール or アップデートしましょう。

% copilot --version               
copilot version: v1.28.0
% copilot init --help             
Create a new ECS or App Runner application.

Usage
  copilot init [flags]

Flags
  -a, --app string          Name of the application.
      --deploy              Deploy your service or job to a "test" environment.
  -d, --dockerfile string   Path to the Dockerfile.
                            Cannot be specified with --image.
  -h, --help                help for init
  -i, --image string        The location of an existing Docker image.
                            Cannot be specified with --dockerfile or --build-context.
  -n, --name string         Name of the service or job.
      --port uint16         The port on which your service listens.
      --retries int         Optional. The number of times to try restarting the job on a failure.
      --schedule string     The schedule on which to run this job. 
                            Accepts cron expressions of the format (M H DoM M DoW) and schedule definition strings. 
                            For example: "0 * * * *", "@daily", "@weekly", "@every 1h30m".
                            AWS Schedule Expressions of the form "rate(10 minutes)" or "cron(0 12 L * ? 2021)"
                            are also accepted.
      --tag string          Optional. The container image tag.
      --timeout string      Optional. The total execution time for the task, including retries.
                            Accepts valid Go duration strings. For example: "2h", "1h30m", "900s".
  -t, --type string         Type of job or svc to create. Must be one of:
                            "Request-Driven Web Service", "Load Balanced Web Service", "Backend Service", "Worker Service", "Static Site", "Scheduled Job".

まず、ローカルフォルダに静的サイトサービスで使用する静的コンテンツを用意しておきます。
今回は次のような単純なコンテンツindex.htmlを用意しました。

あとはcopilot initコマンドでタイプにStatic Siteを指定します。
途中、コンテンツのパスを聞かれるので先程静的コンテンツを格納したディレクトリを指定します。

% copilot init -t "Static Site"                  
Note: It's best to run this command in the root of your Git repository.
Welcome to the Copilot CLI! We're going to walk you through some questions
to help you get set up with a containerized application on AWS. An application is a collection of
containerized services that operate together.

Use existing application: No
Application name: hoge0526staticapp
Service name: hoge0526staticservice
Custom Path to Source: static-content
Another: No
Ok great, we'll set up a Static Site named hoge0526staticservice in application hoge0526staticapp.

✔ Proposing infrastructure changes for stack hoge0526staticapp-infrastructure-roles
- Creating the infrastructure for stack hoge0526staticapp-infrastructure-roles                  [create complete]  [63.1s]
  - A StackSet admin role assumed by CloudFormation to manage regional stacks                   [create complete]  [26.2s]
  - An IAM role assumed by the admin role to create ECR repositories, KMS keys, and S3 buckets  [create complete]  [27.5s]
✔ The directory copilot will hold service manifests for application hoge0526staticapp.

Docker daemon is not responsive; Copilot won't detect and populate the "platform" field in the manifest.
✔ Wrote the manifest for service hoge0526staticservice at copilot/hoge0526staticservice/manifest.yml
Your manifest contains configurations like your container size and port.

- Update regional resources with stack set "hoge0526staticapp-infrastructure"  [succeeded]  [0.0s]
All right, you're all set for local development.

  Would you like to deploy a test environment? [? for help] (y/N)

testという名前の環境にデプロイするか聞かれていますね。
この時点で次のように静的サイト用のマニフェストファイルが作成されています。

copilot/hoge0526staticservice/manifest.yml

# The manifest for the "hoge0526staticservice" service.
# Read the full specification for the "Static Site" type at:
# https://aws.github.io/copilot-cli/docs/manifest/static-site/

# Your service name will be used in naming your resources like S3 buckets, etc.
name: hoge0526staticservice
type: Static Site
files:
  - source: static-content
    recursive: true

# You can override any of the values defined above by environment.
# environments:
#   test:
#     files:
#       - source: './blob'
#         recursive: true
#         destination: 'assets'
#         exclude: '*'
#         reinclude:
#           - '*.txt'
#           - '*.png'

対話の中で指定した静的コンテンツへのパスがコンテンツソースとして設定されていますね。
デフォルトで設定されるのはそれだけです。

静的コンテンツのマニフェストファイルの詳細は以下を確認してください。
この内容から推測するに格納してあるコンテンツをアップするだけのように見えるので、フロントエンドのビルドとかはさすがに無理そうです。
そのあたりは事前に実行しておく必要がありそうですが、別途試してみたいと思います。

作成されるスタック

このあとデプロイまで実行します。

:
All right, you're all set for local development.
Deploy: Yes

✔ Wrote the manifest for environment test at copilot/environments/test/manifest.yml
- Update regional resources with stack set "hoge0526staticapp-infrastructure"  [succeeded]  [0.0s]
- Update regional resources with stack set "hoge0526staticapp-infrastructure"  [succeeded]        [128.9s]
  - Update resources in region "ap-northeast-1"                                [create complete]  [127.0s]
    - KMS key to encrypt pipeline artifacts between stages                     [create complete]  [123.8s]
    - S3 Bucket to store local artifacts                                       [create complete]  [2.0s]
✔ Proposing infrastructure changes for the hoge0526staticapp-test environment.
- Creating the infrastructure for the hoge0526staticapp-test environment.  [create complete]  [60.3s]
  - An IAM Role for AWS CloudFormation to manage resources                 [create complete]  [26.3s]
  - An IAM Role to describe resources in your environment                  [create complete]  [24.6s]
✔ Provisioned bootstrap resources for environment test in region ap-northeast-1 under application hoge0526staticapp.
✔ Provisioned bootstrap resources for environment test.
✔ Proposing infrastructure changes for the hoge0526staticapp-test environment.
- Creating the infrastructure for the hoge0526staticapp-test environment.     [update complete]  [73.0s]
  - An ECS cluster to group your services                                     [create complete]  [7.3s]
  - A security group to allow your containers to talk to each other           [create complete]  [0.0s]
  - An Internet Gateway to connect to the public internet                     [create complete]  [14.7s]
  - Private subnet 1 for resources with no internet access                    [create complete]  [0.0s]
  - Private subnet 2 for resources with no internet access                    [create complete]  [3.4s]
  - A custom route table that directs network traffic for the public subnets  [create complete]  [11.3s]
  - Public subnet 1 for resources that can access the internet                [create complete]  [2.1s]
  - Public subnet 2 for resources that can access the internet                [create complete]  [2.1s]
  - A private DNS namespace for discovering services within the environment   [create complete]  [43.1s]
  - A Virtual Private Cloud to control networking of your AWS resources       [create complete]  [8.8s]
✔ Proposing infrastructure changes for stack hoge0526staticapp-test-hoge0526staticservice
- Creating the infrastructure for stack hoge0526staticapp-test-hoge0526staticservice        [create complete]  [274.6s]
  - A bucket policy to grant CloudFront read access to the Static Site bucket               [create complete]  [0.0s]
  - An S3 Bucket to store the static site's assets                                          [create complete]  [22.3s]
  - A CloudFront distribution for global content delivery                                   [create complete]  [226.6s]
  - Access control to make the content in the S3 bucket only accessible through CloudFront  [create complete]  [7.7s]
  - CloudFront Function to rewrite viewer request to index.html                             [create complete]  [4.4s]
  - An IAM Role for the state machine that moves source files to the S3 bucket              [create complete]  [44.6s]
  - A state machine that moves source files to the S3 bucket                                [create complete]  [0.0s]
  - A policy that gives the Env Manager role access to this site's S3 Bucket                [create complete]  [40.0s]
  - A custom resource that starts the process of moving files to the S3 bucket              [create complete]  [3.8s]
  - An IAM Role for the lambda that starts the process of moving files to the S3 bucket     [create complete]  [45.2s]
  - A lambda that starts the process of moving files to the S3 bucket                       [create complete]  [8.8s]
✔ Deployed service hoge0526staticservice.
Recommended follow-up action:
  - You can access your service at d1qtih0mnc42v2.cloudfront.net over the internet.
- Be a part of the Copilot ✨community✨!
  Ask or answer a question, submit a feature request...
  Visit ? https://aws.github.io/copilot-cli/community/get-involved/ to see how!

デプロイが完了しました。

AWS Copilot からデプロイを行うと CloudFormation スタックが作成されます。
今回の機能でいくつか作成されるスタックはいくつかあるのですが、主要なものは以下の 2 つです。

上記のhoge0526staticapp-testは Copilot でデプロイする環境のベースとなる共通ネットワークや ECS クラスターなどがデプロイされます。
今回は静的コンテンツのため VPC リソースは使わんだろうと思っていたのですが、環境リソースについては従来同様にデプロイされるということがわかりました。

今回の機能で新しくデプロイされるようになったのはhoge0526staticapp-test-hoge0526staticserviceのほうですね(名前...)
こちらは次を見て頂くとわかるように主に CloudFront + S3 で構成されています。

上記の赤枠のリソースで StepFunctions などが登場していて、おや?という感じだと思いますが、これはローカルコンテンツをデプロイするためのカスタムリソース用に使われているもののようです。内容としてはコンテンツコピーのためだけに使われていました。

デプロイされたスタックから CloudFront + S3 関係だけを抜粋してみました。
次のようなものがデプロイされています。

:
Resources:
  Bucket:
    Metadata:
      aws:copilot:description: An S3 Bucket to store the static site's assets
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled
      AccessControl: Private
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced

  BucketPolicyForCloudFront:
    Metadata:
      'aws:copilot:description': 'A bucket policy to grant CloudFront read access to the Static Site bucket'
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref Bucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: ForceHTTPS
            Effect: Deny
            Principal: "*"
            Action: s3:*
            Resource:
              - !Sub ${Bucket.Arn}
              - !Sub ${Bucket.Arn}/*
            Condition:
              Bool:
                aws:SecureTransport: false
          - Sid: AllowCloudFrontServicePrincipalReadOnly
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource:
              - !Sub
                - arn:${AWS::Partition}:s3:::${bucket}
                - bucket: !Ref Bucket
              - !Sub
                - arn:${AWS::Partition}:s3:::${bucket}/*
                - bucket: !Ref Bucket
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub
                  - arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${cfDistributionID}
                  - cfDistributionID: !Ref CloudFrontDistribution

  CloudFrontOriginAccessControl:
    Metadata:
      'aws:copilot:description': 'Access control to make the content in the S3 bucket only accessible through CloudFront'
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Description: !Sub 'Access control for static s3 origin for ${AppName}-${EnvName}-${WorkloadName}'
        # Truncate the name to allow at most 64 characters.
        Name: hoge0526staticapp-test-hoge0526staticservice
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  CloudFrontViewerRequestRewriteFunction:
    Metadata:
      'aws:copilot:description': 'CloudFront Function to rewrite viewer request to index.html'
    Type: AWS::CloudFront::Function
    Properties: 
      AutoPublish: true
      FunctionCode: |
        function handler(event){var request=event.request;var uri=request.uri;if(uri.endsWith('/')){request.uri+='index.html'}else if(!uri.includes('.')){request.uri+='/index.html'}return request}
      FunctionConfig: 
        Comment: CloudFront Function to rewrite viewer request to index.html
        Runtime: cloudfront-js-1.0
      # Truncate the name to allow at most 64 characters.
      Name: hoge0526staticapp-test-hoge0526staticservice

  CloudFrontDistribution:
    Metadata:
      'aws:copilot:description': 'A CloudFront distribution for global content delivery'
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          Compress: true
          AllowedMethods: ["GET", "HEAD"]
          FunctionAssociations:
            - EventType: viewer-request
              FunctionARN: !GetAtt CloudFrontViewerRequestRewriteFunction.FunctionARN
          ViewerProtocolPolicy: redirect-to-https
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # See https://go.aws/3bJid3k
          TargetOriginId: !Sub 'copilot-${AppName}-${EnvName}-${WorkloadName}'
        Enabled: true
        IPV6Enabled: true
        Origins:
          - Id: !Sub 'copilot-${AppName}-${EnvName}-${WorkloadName}'
            DomainName: !GetAtt Bucket.RegionalDomainName
            OriginAccessControlId: !Ref CloudFrontOriginAccessControl
            # Workaround for using Origin Access Control as Origin Access Identity is still
            # required when the origin is an S3 bucket.
            S3OriginConfig:
              OriginAccessIdentity: ''
:

S3 ではウェブホスティング機能は使われていないですね。
通常の構成に CloudFront から OAC でアクセス制御しています。

そしてウェブに必要なリダイレクトなどの処理は上記のCloudFrontViewerRequestRewriteFunctionで作成されている CloudFront Functions の機能でエッジ処理させる仕組みをとっています。
こういうスタック見ると、静的ホスティングの際の参考にもなるのでおもしろいですね

コンテンツへアクセスする

最後に、AWS Copilot CLI で出力された URL へアクセスしてみます。

% curl https://d1qtih0mnc42v2.cloudfront.net/
hoge0526

問題なくアクセス出来ましたね。

さいごに

本日は AWS Copilot に静的サイト用のサービスデプロイ機能が追加されたので使ってみました。

基礎のクラスターなどもデフォルトでデプロイされるので単純な静的ウェブサイトをホスティングする用途ではなく、あくまでも ECS や App Runner などのコンテナバックエンドを利用するフロントエンドをホスティングするためのオプションとして今回の機能が追加された感じですね。