🐳

1年間本番運用してわかった、スタートアップこそAWS Copilot CLIを使うべきNつの理由

2022/09/05に公開

Leaner 開発チームの黒曜(@kokuyouwind)です。

先日開催された AWS Startup Community Conference 2022 に登壇させていただきました。

https://aws-startup-community.connpass.com/event/247548/

AWS Startup Community に合わせた若干釣り気味のタイトルですが、内容としては Copilot CLI の使い方や利用の際のポイントをまとめたものでした。とはいえ新規アプリケーション構築に向いていることやマルチアカウント運用に癖があることから、スタートアップでよりハマる局面が多いだろうというのも本心です。

ちなみに CFP 提出時点の仮タイトルは「1 年間本番運用してきた AWS Copilot CLI の悲喜こもごも」で、もう少しエピソードベースでの Tips に比重を置いた内容を考えていました。

今回は大まかな発表内容や、発表で触れられなかった個別 Tips などをあわせて記事にまとめます。

Leaner での事例紹介と Tips の節で発表になかった内容を追加しているため、発表を既に知っている方はそこから読んで頂くと良さそうです。

発表スライド

アスペクト比があってなくて embed だと横が切れてしまっているので、できれば SpeakerDeckslides.com で直接見ていただいたほうが良さそうです。後者がオリジナルデータになっています。

以下に大まかな内容をまとめていきます。

Copilot CLI について

  • AWS Copilot CLI は AWS 上でコンテナアプリを構築するツール
  • copilot init 一発でいい感じの構成が作れる
  • チュートリアル以外では copilot init は使わない方が良い
    • Copilot を使う上で意識したほうがいいリソースの概念が隠蔽されている
    • ドメインオプションやマルチアカウント指定など、詳細オプションが使えない
    • 実用上は copilot app init などのサブコマンドを用いる
  • Copilot では独自に抽象化した単位でリソースを扱う
    • Application: Web アプリケーション全体を表すトップレベルのリソース
    • Environment: テスト・ステージ・本番など、稼働環境を表すリソース
    • Service: API サーバ・ワーカーなどアプリを構成する個々のサービスを表すリソース
    • Pipeline: Environment ごとの Service をデプロイするパイプラインを表すリソース


Copilot CLI から扱う、抽象化されたリソース構成の例

各リソースの作成について

  • Application は copilot app init で作成する
    • このとき AWS Profile で指定された AWS アカウントが、 Copilot 利用時の中央アカウントになる
      • 以降 copilot コマンドを使う際は同じ Profile の指定が必要
      • アカウント横断のリソースがこのアカウントに作られるので注意が必要
    • 以下の AWS リソースやファイルが作られる
      • Application のメタデータを格納する Parameter Store パラメタ
      • copilot/.workspace ファイル(ローカル)
    • app init --domain 'leaner.jp' のようにオプションを指定すると、ドメインを紐付け可能
      • [svcName].[envName].[appName].[domain] というドメインでサービスにアクセスできるよう、 DNS などが自動設定される
      • ex: copilot という App 名のとき、 test 環境の api サービスであれば api.test.copilot.leaner.jp ドメインでアクセスできるようになる
      • 指定したドメインの Route 53 ホストゾーンが中央アカウントに必要(上記例であれば leaner.jp レベルのホストゾーン)
  • Environment は copilot env init で作成する
    • 中央アカウントとは別に、環境構築先の AWS アカウントを対話的に指定できる
    • 以下の AWS リソースが作られる
      • VPC, Subnet などネットワーク周りのリソース
      • [envName].[appName].[domain] の Route 53 ホストゾーン(ドメインを紐付けた場合)
        • 上述の例だと test.copilot.leaner.jp のホストゾーンが作られる
      • Environment のメタデータを格納する Parameter Store パラメタ(中央アカウント上)
  • Service は copilot svc init で作成する
    • Service のタイプなどを対話的に入力する
      • タイプによって作られる AWS リソースや設定ファイル形式が異なる
      • ECS 上で常時待ち受けさせるサービスであれば Load Balanced Web Service を選択する
    • 以下の AWS リソースやファイルが作られる
      • copilot/[svcName]/manifest.yml ファイル(ローカル)
      • Service 用の ECR Repository (中央アカウント上)
      • Service のメタデータを格納する Parameter Store パラメタ(中央アカウント上)
      • Environment に紐付いた ALB
    • 別途デプロイコマンドである copilot svc deploy を実行するか、デプロイパイプラインが起動しない限りコンテナは起動しない
  • Pipeline は copilot pipeline init で設定ファイルを作成し、 copilot pipeline deploy で作成する
    • init では以下のファイルが作られる
      • copilot/pipelines/[pipelineName]/{manifest.yml,buildspec.yml} (ローカル)
    • deploy で設定ファイルに基づいて AWS リソースが作成される
      • CodePipeline, CodeBuild と関連リソース (中央アカウント上)
  • CodePipeline 実行でサービスがデプロイされる
    • CodeBuild が Service ごとにイメージをビルドし、 ECR に push する
      • 複数環境にデプロイする場合でも、イメージは Service ごとに 1 つのみ
      • 同じビルド設定でも、イメージは Service ごとにビルドと push が行われる
    • CodeBuild 完了後、サービス稼働用の CloudFormation Stack が CreateOrUpdate される
      • 存在しない場合は ECS Service に加えて ALB Listener Rule や Target Group もこのときに作られる
      • 既に存在する場合はローリングアップデートで更新される


Copilot CLI の抽象リソースと、実際の AWS リソースやファイルとの関係

Copilot CLI の便利な点・癖のある点

  • 便利な点
    • ベストプラクティスに沿った構成を簡単に作れる
      • DNS 設定やネットワークの構築なども全部任せられるので手軽
      • 環境ごとのマルチアカウント分割や Multi-AZ での冗長化も対応している
    • 直感的な単位でリソースを管理できる
      • 普通に AWS を使うと、 ECS だけでも Cluster, Service, TaskDefinition, Service などの粒度を意識しないといけない
      • 環境やサービスなど、直感的な粒度で管理できるのは特にバックエンドエンジニアにとって扱いやすい
    • デフォルトでセキュリティ設定が担保される
      • 特に意識しなくてもある程度適切なセキュリティルールが設定される
      • ex: コンテナに接続できるのは ALB や他のコンテナからのみ
    • Manifest ファイルで簡易的な IaC が行える
      • Service Manifest で ALB のヘルスチェック設定や VPC Subnet のプライベート配置などが設定できる
    • 定期実行ジョブも Service として一元管理できる
      • Service Type: Scheduled Job でスケジュール実行するサービスが作成できる
      • 内部的には Step Function + ECS OneShot Task になるため、既存コンテナと同居しない独立リソースを確保する
    • copilot svc exec コマンドで簡単にコンテナ内作業が行える
      • Docker Exec した状態になるため、手軽にコマンドが打てる
      • 内部的には Session Manager での接続になるが、 Service と Environment を指定して接続するためコンテナ ID を調べる必要はない
    • Addon で追加の AWS リソースを管理できる
      • 各サービスに付随するリソースを CloudFormation YAML で直接定義できる
      • RDS や S3 などのデータストアレイヤーや、 FireLens で使う IAM などもコードで管理できる
  • 癖のある点
    • リソース管理単位が独自のため、内部挙動を把握していないとトラブルシューティングが難しい
    • ドメイン紐付けが Application 作成時にしかできない
      • copilot init からは指定できない問題との合わせ技により、知らないと確実に踏む罠
    • 意識しないとデフォルト AWS アカウントに色々作られてしまう
      • デフォルトを管理アカウントにしていると ECR や CodePipeline が全部管理アカウントにできて悲しい気持ちになれる
    • ルートドメインホストゾーンと Application の中央アカウントを別の AWS アカウントに分けることができない
      • 例えば leaner.jp ホストゾーンと copilot.leaner.jp のホストゾーンは同じ AWS アカウントになる
      • 合わせて ECS や CodePipeline も同じアカウントに置かれる
      • ルートドメインホストゾーンが管理アカウントにあると悲しい気持ちになれる
      • 複数 Copilot アプリを作った場合も、 CodePipeline が同じアカウントに並ぶので非常に管理しづらくなる
    • サービスごとに Docker Build や ECR Repo Push が行われるため、同じビルド設定で複数サービス・ジョブが必要だと非効率
      • 特に Rails だと Rake や Thor タスクでスケジュール実行ジョブを書くので、同一の Rails イメージが複数 ECR に push されてビルド時間・帯域費用ともに増加してしまう
    • デプロイが CloudFormation で行われるため、ローリングアップデート固定で Blue-Green Deploy できない
      • CodeDeploy 使わないのでこのあたりの融通が効かない

Leaner での事例紹介と Tips

この項目のみ、発表時に触れなかった内容を追記しています。

移行事例

移行事例はAWS Copilotで本番環境をコンテナ化する にまとめて書いてあるので、こちらの記事をご参照ください。

https://zenn.dev/leaner_tech/articles/20210607-migrate-rails-copilot

なお当時に比べると Copilot のバージョンがかなり上がっており、当時つらかった Arm 対応や複数パイプラインなどはサポートされているため、このあたりは当該記事に追記・修正をおこなっています。

Tips: 既存環境の移行時は、動作環境用と割り切って全く別のドメインを紐付けておくと動作確認が楽

例えば myapp.leaner.jp というサービスを Copilot 移行する場合、 myapp.leaner.jp ドメインは既に既存サービスの A レコードなどが設定されているはずです。
このケースでは myapp という Copilot App 作成時に --domain leaner.jp としても myapp.leaner.jp の NS レコードが作れず失敗します。

そこで copilot-myapp.leaner.jp ホストゾーンをあらかじめ作っておき、 --domain copilot-myapp.leaner.jp 指定によって myapp.copilot-myapp.leaner.jp ホストゾーンを作る形で app init を行います。
こうすると、 test 環境の api サービスには api.test.myapp.copilot-myapp.leaner.jp でアクセスできるようになります。

もちろん既存環境は api.test.myapp.leaner.jp のようなドメインになっているはずなので、動作確認が完了したら既存の DNS レコードを変えて向き先 ALB を変えることで移行します。
この際 Copilot から認識されていないドメインを向けることになるため ALB Listener Rule で振り分けを指定する必要があること、証明書の設定が必要なことには注意が必要です。
DNS を書き換える前に hosts 書き換えで動作を確認しておくとより安全です。

Tips: Service の healthcheck 周りの設定を変更するとデプロイが速くなる

デフォルトでは 30 秒間隔でヘルスチェックを行い、 5 回の成功で Healthy と判定されます。
また古いバージョンのコンテナを落とすまでに既存接続の終了を待つため 60 秒の猶予期間が設定されています。
このため、最短でもデプロイ完了までに 210 秒 = 3 分半の待ち時間が発生します。

公式ドキュメントに書かれていますが、このあたりの設定は Service の healthcheck から設定可能です。
Leaner ではデプロイタイミングでそこまで長時間残っている既存接続はない想定で、デプロイ効率を高めるために以下のような設定にしています。

http:
  path: '/'
  healthcheck:
    path: '/health' # サービス起動のみを確認できる低負荷エンドポイントに向ける
    healthy_threshold: 3 # 3回 OK で Healthy にする
    unhealthy_threshold: 2 # 2回 NG で Unhealthy にする
    interval: 10s # 10 秒間隔でヘルスチェックを行う
    timeout: 5s
    grace_period: 300s # migration で時間がかかることがあるため、起動時のヘルスチェック失敗は長めに無視する
  deregistration_delay: 10s # API サーバーの既存接続は長くて数秒程度のため、 10 秒で接続を打ち切りコンテナを落とす

Tips: platform を linux/arm64 にする場合は CodeBuild の環境も arm に変更する

Service の platform 属性を指定することで、 CPU アーキテクチャを Arm に変更できます。
AWS 上では Graviton での起動になるためコストパフォーマンスがよくなり、 M1 Mac からビルドしたイメージもそのまま使えるようになり便利です。

しかしながら、標準で作られる CodeBuild はベースイメージが x86_64 系のものになっているため、そのままだとビルドされるイメージが x86_64 用になり、 ECS Task 起動時のイメージフォーマット不一致で必ず失敗するようになってしまいます。
これを直すためには CodeBuild の環境設定からベースイメージを aws/codebuild/amazonlinux2-aarch64-standard:2.0 など Arm 系のものに変更する必要があります。

マルチプラットフォーム向けのビルドが行える Buildx を使ってくれると良いのですが、残念ながら CodeBuild の標準イメージでは Buildx がインストールされていません。
CodeBuild で Buildx を使ってクロスプラットフォームビルドを行う のように buildspec.yml で Buildx をインストールしてもよいのですが、ビルド時間が伸びてしまうため素直にアーキテクチャを統一するほうが良いでしょう。

Tips: サービス初回デプロイでデプロイ失敗した場合は、 ECS Service 含む CloudFormation Stack の削除が必要

copilot svc deploy コマンドや CodePipeline の初回実行時、 ECS Service を含む CloudFormation Stack が作成されます。

この際、 ECS Service の起動に失敗すると CloudFormation Stack が CREATE_FAILED 状態になります。
一度この状態になると、再度デプロイを行おうとしても即座に失敗してしまいます。

これは CREATE_FAILED から再度同じリソースの作成状態に遷移できないことが原因で、こうなると当該 Stack を一度削除する必要があります。
特に前述の CPU アーキテクチャ不一致イメージで起動しようとするとこの状態に陥るため注意が必要です。

なお、一度作成に成功していれば以降はロールバックを行い ROLLBACK_COMPLETE 状態になるため、このような挙動は起こらなくなります。

Tips: vpc.placement を private にするとセキュリティ的には安心だが、 NAT Gateway が作られるのでコストは高くなる

Copilot は 1 つの VPC に対し、 public と private の 2 種類の subnet を AZ 分散して 2 つずつ作成します。

各サービスは標準で public subnet に配置されますが、 ALB でアクセスを受けるため public に配置する必要はありません。
必要であればマニフェストの vpc.placement 属性を private とすることで private subnet に配置できます。

このとき、 1 つでも private subnet で稼働するコンテナが存在する場合は private subnet から外部アクセスが行えるよう NAT Gateway が自動配置されます。
NAT ゲートウェイは起動しているだけで料金がかかり、東京リージョンであれば 0.062USD USD / h であるため 1 ヶ月でおおよそ 45 ドルほどかかることになります。
NAT ゲートウェイは private subnet ごとに必要なため、 1 環境あたり AZ 分散した 2 subnet があるため最低でも 90 ドルです。
仮に本番・ステージ・テストの 3 環境が存在する場合は 270 ドルとなり、プライベート配置に変更しただけで結構なコストが追加されてしまいます。

public subnet に配置した場合でも前述のとおりセキュリティグループで ALB と他コンテナ以外からは接続が遮断されるため、特に本番環境以外では public subnet に配置しなくてもそこまでのリスクはないはずです。
本番のみ private subnet の配置とすれば、すべて private subnet に配置する場合と比べて月に 180 ドルを節約できます。
(ただし本番とそれ以外の環境で構成が不一致となるため、動作環境を完全に揃えたい場合は素直にすべて private subnet に配置する必要があるでしょう)

Tips: Dockerfile の FROM イメージは ECR Public Repository にすると良い

Docker Hub には Rate Limit があり、特に非ログイン状態では 6 時間あたり 100 pull が上限になっています。
この制限は IP 単位で適用されるため、インスタンスが共用される CodeBuild では非常に引っかかりやすいです。

これを避けるため、 Dockerfile の FROM イメージには ECR Public Repository のイメージを指定するのがおすすめです。
例えば Ruby の場合、 FROM ruby ではなく FROM public.ecr.aws/docker/library/ruby を指定します。
これにより Rate Limit での pull 失敗を避けられる上、ネットワーク配置が近いため高速に pull できます。

まとめ

タイトルの「N つの理由」については、 Web アプリを完全に 1 から構築する前提で 7 つのおすすめポイントを書いているため N = 7 となります。
既にある Web アプリを移行する場合は DNS や VPC 周りで受けられる恩恵が減りますし、 AWS アカウントの構成によっては逆に面倒が増えることもあるため、 N は小さい値になるでしょう。

多少癖があるとはいえ Copilot CLI はとても良いツールなので、機会があれば触ってみて、癖のあるポイントが受け入れられるようであれば導入をおすすめします。

宣伝

本記事の内容や他の AWS 構成について、カジュアルになんでもお話する Meety を作っています。興味があればぜひ気軽にお話しましょう!

https://meety.net/matches/pABFTFpaORla

Leaner Technologies では Copilot CLI を本番運用して悲喜こもごも味わいたいエンジニアを募集しています!

https://careers.leaner.co.jp/

リーナーテックブログ

Discussion