カスタマイズで広がるAWS Copilotの実践力

SREチームの橋本です。SRE連載の7月号になります。

カヤック社内では弊社藤原のecspressoをAmazon ECSのデプロイツールとして活用していますが、AWS公式のデプロイツールAWS Copilot(現在v1.29)もそのオールインワン的な性質から、開発・運営リソースが限られるプロジェクトでは選択肢に入るようになってきました。

今回はそのAWS Copilot活用のため、背後にあるAWS CloudFormationテンプレートをカスタマイズする手法を紹介します。

AWS CopilotとCloudFormation

AWS CopilotはECSなどのデプロイを簡単にするCLIツールですが、実態としてはManifestと呼ばれるYAMLの設定ファイルからCloudFormationテンプレートを生成し、各種リソースを作成・管理するものです。

AWS Copilotは内部的にCloudFormationを用いている

ECSでサービスを作るとき、典型的な構成としてはロードバランサーを前段に置くことになりますが、単純にこの2つを作成して構築完了とはなりません。 IAMロール、VPC、セキュリティグループなどなど多くの周辺リソースが必要となり、ロードバランサーに対してもターゲットグループやリスナーなどを設定します。 こうした手間をcopilotコマンド一つに集約できるのがAWS Copilotの利点と言えます。

ただ設定が簡単な分ある程度は決め打ちな部分もあり、例えばCloudFrontは環境につき1つだけといった制約もあります。(具体的には環境のcdnで設定します。) こうした部分をカスタマイズするときに有用なのがAddonやオーバーライドといった機能です。

例えばcopilot inittechblogアプリケーションを作成し、myserviceというLoad Balanced Web Serviceを作成、インストラクションに従ってtest環境も作成すると、以下の4つのCloudFormationスタックが作成されます。

  • techblog-infrastructure-roles
  • StackSet-techblog-infrastructure-*** (末尾にハッシュ的な文字列が付くようです)
  • techblog-test
  • techblog-test-myservice

1つ目はCloudFormation自身が使うためのIAMロール、2つ目は環境間で共通のリソース(KMSキー、成果物を置くS3など)で特に触ることはないので、カスタマイズというと3つ目の環境、4つ目のサービスに関するスタックへ変更を加えることになります。

Addonでリソースを追加する

Addon機能では、単にCloudFormationテンプレートを書くことで追加のリソースを定義することができます。

環境のディレクトリ下のaddons/形式に従うテンプレートを置くと、環境のデプロイ時に一緒にAddonのデプロイが行われます。

例えばCloudFrontディストリビューションを追加で配置する場合、以下のようなテンプレートを書くことになります。

Parameters:
  App:
    Type: String
  Env:
    Type: String

Resources:
  MyCloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          AllowedMethods:
            - HEAD
            - GET
          CachedMethods:
            - HEAD
            - GET
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
          Compress: true
          OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 # AllViewer
          TargetOriginId: s3
          ViewerProtocolPolicy: redirect-to-https
        Enabled: true
        Origins:
          - Id: s3
            DomainName: 'example-bucket.s3.ap-northeast-1.amazonaws.com'
            OriginPath: ''
            OriginAccessControlId: !GetAtt MyDistributionOriginAccessControl.Id
            S3OriginConfig:
              OriginAccessIdentity: ''
            ConnectionAttempts: 3
            ConnectionTimeout: 10
  MyDistributionOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties: 
      OriginAccessControlConfig: 
          Name: example-bucket
          OriginAccessControlOriginType: s3
          SigningBehavior: always
          SigningProtocol: sigv4

これでcopilot env deployすると、内部的には環境のCloudFormationスタックの対してネストされたスタックが作成されます。

Addonはネストされたスタックになる

またv1.27からは差分のプレビュー機能が追加され、deploy --diffによりCloudFormationテンプレートの差分が分かるようになりました。

$ copilot env deploy --diff
Only found one environment, defaulting to: test
~ Resources:
    + AddonsStack:
    +     Metadata:
    +         'aws:copilot:description': 'A CloudFormation nested stack for your additional AWS resources'
    +     Type: AWS::CloudFormation::Stack
    +     Properties:
    +         Parameters:
    +             App: !Ref AppName
    +             Env: !Ref EnvironmentName
    +         TemplateURL: https://stackset-techblog-infras-pipelinebuiltartifactbuc-***.s3.ap-northeast-1.amazonaws.com/manual/addons/environments/***.yml

  Continue with the deployment? (y/N)

これは環境testのスタックにネストされたスタックが追加されるという内容で、TemplateURLのファイルを見るとネストされたスタック(Addonに当たる部分)の内容も確認できます。 この場合上のYAMLと同内容になります。

単に追加のリソースが作られるのが基本ですが、ワークロードのAddonの場合、Outputs:に以下のようなリソースを記述するとECSタスクロールやECSサービスなどに付与することができます。(ドキュメント参照)

  • IAMポリシー
  • セキュリティグループ
  • シークレット
  • 環境変数

元々ワークロードに対してサポートされていたAddonは、v1.25で環境に対してもサポートされ、例えば複数のサービスで利用するS3といったリソースがより自然に扱えるようになりました。(またv1.29ではパイプラインにもサポートされました。) なおcopilot storageというコマンドがあり、これによりRDSやS3をワークロードのAddonであったり、環境のAddonとしても定義することができます。

オーバーライドで生成されるテンプレートを書き換える

例えばCloudFrontディストリビューションへのCloudFront Functions(あるいはLambda@Edge)の紐づけは今のところサポートされていません。 CloudFrontディストリビューションはManifestから生成されるリソースのため、Addonでは触れませんが、v1.27で追加されたoverrideコマンドによりこうしたケースにも対応が可能となりました。

オーバーライドはCDK(AWS Cloud Development Kit)とYAMLパッチの両方に対応しています。ここでは特に複雑なことをしないので、よりシンプルなYAMLパッチによるオーバーライドを試してみます。

まず再びAddonsで、CloudFront関数を用意します。(IPを返すだけのレスポンスに書き換えてしまうので実用性はありませんが、動作確認は簡単にできます)

Parameters:
  App:
    Type: String
  Env:
    Type: String

Resources:
  RewriteFunction:
    Type: AWS::CloudFront::Function
    Properties:
      AutoPublish: true
      FunctionCode: !Sub |
        function handler(event) {
          var request = event.request;
          var clientIp = event.viewer.ip;
          return {
            statusCode: 200,
            statusDescription: '200 OK',
            body: clientIp
          };
        }
      FunctionConfig:
        Comment: !Sub 'Respond with the client IP address'
        Runtime: cloudfront-js-1.0
      Name: !Sub "${AWS::StackName}-RewriteFunction"

Outputs:
  RewriteFunctionARN:
    Description: 'The ARN of the CloudFront function that rewrites the request'
    Value: !Ref RewriteFunction
    Export:
      Name: !Sub ${App}-${Env}-RewriteFunctionARN

最後のExportによってOutputsの値が他のスタックからも利用可能になります。なお名前空間はリージョン単位なので、${App}-${Env}-という風にプレフィックスを付けることが推奨されています。

この関数をCloudFrontディストリビューションに紐づけます。対象となるディストリビューションは、例えばcopilot env packageで環境のCloudFormationテンプレートを出力すると、以下のように見つけることができます。

Resources:
    ……
    CloudFrontDistribution:
        Metadata:
            'aws:copilot:description': 'A CloudFront distribution for global content delivery'
        Condition: CreateALB
        Type: AWS::CloudFront::Distribution
        Properties:
            DistributionConfig:
                DefaultCacheBehavior:
                    AllowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "PATCH", "POST", "DELETE"]
                    CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # See https://go.aws/3bJid3k
                    TargetOriginId: !Sub 'copilot-${AppName}-${EnvironmentName}-origin'
                    OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 # See https://go.aws/3BIE8CP
                    ViewerProtocolPolicy: allow-all
                ……

CloudFront関数を紐づけるには、DefaultCacheBehavior下にFunctionAssociationsの項目を追加します。具体的には以下のようなYAMLパッチでオーバーライドします。

# copilot/environments/overrides/cfn.patches.yml
- op: add
  path: /Resources/CloudFrontDistribution/Properties/DistributionConfig/DefaultCacheBehavior/FunctionAssociations
  value:
    - EventType: viewer-request
      FunctionARN:
        Fn::ImportValue:
          !Sub ${AppName}-${EnvironmentName}-RewriteFunctionARN

あとはデプロイするだけです。

$ copilot env deploy --diff
Only found one environment, defaulting to: test
~ Description: CloudFormation environment template for infrastructure shared among Copilot workloads. -> CloudFormation environment template for infrastructure shared among Copilot workloads using AWS Copilot with YAML patches.
~ Resources/CloudFrontDistribution/Properties/DistributionConfig/DefaultCacheBehavior:
    + FunctionAssociations:
    +     - EventType: viewer-request
    +       FunctionARN:
    +         Fn::ImportValue: !Sub ${AppName}-${EnvironmentName}-RewriteFunctionARN
Continue with the deployment?

便利ですね!

終わりに

AWS CopilotはVPC周りなどの細かい手間が減るので個人的に好きなツールでしたが、最近のアップデートによって大きくカスタマイズ性が向上し実用性が高まりつつあると感じています。 たとえ今AWS Copilotを使っていない、という方にとってもこの記事でその高まりの片鱗を感じて頂ければ嬉しく思います。

カヤックではツールのアップデート情報が大好きなエンジニアを募集しています!

hubspot.kayac.com