Slide 1

Slide 1 text

Reject Day 2023 EC2からのECS移行において IaCとCDをどう変えたか 株式会社スマートラウンド 山原 崇史(@shonansurvivors)

Slide 2

Slide 2 text

自己紹介 株式会社スマートラウンド SRE / コーポレートIT チーム 山原 崇史 (やまはら たかし)  経歴等  ・SIer → 銀行 → Web系ベンチャー数社 → 現職  ・AWS Startup Community Core Member 好きなAWSサービス  IAM Identity Center / Security Hub shonansurvivors

Slide 3

Slide 3 text

事業およびプロダクト紹介 ミッション  スタートアップが可能性を最大限に発揮できる世界をつくる smartroundが実現する世界  統一化・標準化されたデータ管理によって、スタートアップと投資家双方の業務を効率化

Slide 4

Slide 4 text

本日のテーマ EC2からのECS移行においてIaCとCDをどう変えたか  ECS移行において単に基盤を入れ替えることだけを目的とするのではなく、  IaCやCDについても考慮し、運用性や保守性を高めるようにしました。  その実現に向けた考え方や工夫した点についてお話しします。 今回話さないこと ● EC2からECSへ安全に切り替えるための工夫など 👉稼働率やレイテンシー等を見ながら段階的に流量変更を行い、無事に無停止で切替完了

Slide 5

Slide 5 text

アジェンダ 1. ECS移行前の構成と課題 2. 今回のECS移行でコンセプトとしたこと 3. 実際の取り組み ○ ビルド ○ イメージ管理 ○ CDツール選定 ○ IaC ○ デプロイ ○ CDパイプライン 4. まとめ

Slide 6

Slide 6 text

1. 移行前の構成と課題

Slide 7

Slide 7 text

移行前の構成 Elastic Beanstalk(以降、EB)を中心としたベーシックな Webアプリケーション(+ Lambda関数が数個)

Slide 8

Slide 8 text

実際に運用してわかったEBの良かった点 良かった点 ● デプロイ処理の構築が非常に容易 (CodePipeline上でデプロイ対象として指定するだけ ) ● マネージドAMIを使い自動更新を有効化すれば EC2の管理負荷はほぼ無い (脆弱性は自動で解消 ) ● デプロイ時に任意のスクリプトを実行可能で、カスタマイズ性高く、その作り込みが楽しい

Slide 9

Slide 9 text

実際に運用してわかったEBの課題 課題 ● Terraformと相性が悪い ○ applyに10分以上かかることも(EBのAPIを叩く→FatなCFnの更新が走るという流れのためか ) ○ 設定値にSensitive valueを含むと、大半の属性の plan差分が非表示になってしまう ● 将来Pull Request単位で開発環境を作成可能とする予定だが、 EBはそのボトルネックになりそう ○ プロビジョニングに時間がかかり、開発者体験が芳しくないと思われる ● 社外にEBの運用経験者が少なく、 多くの場合で新規参画者のキャッチアップが必要

Slide 10

Slide 10 text

2. 今回のECS移行でコンセプトとしたこと

Slide 11

Slide 11 text

今回のECS移行(コンテナ移行)におけるコンセプト 1. 各種のベストプラクティスを参考にそれらを取り込んだ設計、実装を心掛ける ○ The Twelve-Factor App https://12factor.net/ja/ ■ III. 設定を環境変数に格納する ■ V. ビルド、リリース、実行の 3つのステージを厳密に分離する ○ Best practices for writing Dockerfiles https://docs.docker.com/develop/develop-images/dockerfile_best-practices 2. 今回の移行によって 開発チームがインフラや CDから「遠ざかる」ことの無いように する ○ わかりやすく変えることはあっても、わかりにくく変えることは避ける ○ 開発チーム自身が対応可能な範囲を増やす

Slide 12

Slide 12 text

3. 実際の取り組み

Slide 13

Slide 13 text

3.1. ビルド

Slide 14

Slide 14 text

パッケージ 環境別の設定ファイル (conf)が存在し、環境ごとにそれぞれでビルド、 パッケージングしていた 👉踏襲してしまうと環境ごとに異なるコンテナイメージを作成 することになり、可搬性を損なう パッケージ 環境ごとに異なるパッケージという課題 デプロイ stg環境のEB ビルド ソース stg用conf 本番用conf stg用conf jar デプロイ 本番環境のEB ビルド stg用環境変数 (シークレット値等) 本番用環境変数 (シークレット値等) 相違 本番用conf jar

Slide 15

Slide 15 text

パッケージ コンテナ化の着手前に、まず 設定ファイル(conf)の共通化を図り、環境差異は全て環境変数で吸収 👉同じコンテナイメージを各環境で使うための下地 が出来た(ビルドが別なのは、捨て去る EBにおいては良しとした ) パッケージ 環境差異は全て環境変数で吸収 デプロイ stg環境のEB ビルド ソース stg用conf 本番用conf 共通conf jar デプロイ 本番環境のEB ビルド stg用環境変数 (シークレット値等 +旧confに存在したもの) 本番用環境変数 (シークレット値等 +旧confに存在したもの) 同一 共通conf jar 共通conf

Slide 16

Slide 16 text

コンテナイメージのビルドツールの選定 前提 ● 当社はバックエンドに JVM系言語であるKotlinを使用 選定結果 ● コンテナイメージのビルドツールに Jibを採用 https://github.com/GoogleContainerTools/jib ○ Dockerfileを書く必要が無く、既存のJavaビルドツール(Gradle等)の延長線上で利用できる ○ イメージビルドのベストプラクティスを自動的に適用 し、細かな考慮から解放される 参考:STORES決済におけるEC2からECS Fargateへの移行〜無停止要件も添えて〜 | RyoMa_0923 https://speakerdeck.com/fufuhu/storesjue-ji-niokeruec2karaecs-fargatehefalseyi-xing-wu-ting-zhi-yao-jian-motian-ete

Slide 17

Slide 17 text

3.2. イメージ管理

Slide 18

Slide 18 text

stg環境と本番環境で同一イメージを使用するために ● ECRレプリーションを採用 ○ stg環境と本番環境のインフラ差異が比較的少なくて済む ■ stg環境のみに必要:レプリケーション設定 ■ 本番環境のみに必要:レプリケート受け入れのためのレジストリポリシー ○ ECR自体や、ECRからイメージをプルする ECSタスク実行ロールのポリシーが シンプルで済む

Slide 19

Slide 19 text

ECRレプリケーションを使う場合の注意点 1. ECRのリポジトリ名はレプリケート先も同一 となる ○ 当社はリソース名のプレフィックスに「 {プロジェクト名}-{環境名}-」を付けていたため、 例えば本番環境に「smartround-stg-webapp」のようなECRが作成されてしまう ○ ECRのリポジトリ名からは環境名を外すことで対処した (例:smartround-webapp) 2. レプリケート先でもAmazon InspectorのECRイメージスキャンのコストがかかる ○ 同一のイメージに二重でコストがかかるので、気持ち的には勿体無く感じてしまう

Slide 20

Slide 20 text

(余談)ECRレプリケーションを使わないとしたら CI/CD専用AWSアカウントを用意し、そこで ECRの中央管理も担わせ、 stg・本番環境それぞれに対して、クロスアカウントでデプロイを行うアプローチなど https://docs.aws.amazon.com/whitepapers/latest/organizing-your-aws-environment/deployments-ou.html

Slide 21

Slide 21 text

3.3. CDツール選定

Slide 22

Slide 22 text

Elastic Beanstalk利用時のCDツール ● CodePipelineを全面的に利用 ● 社内ポリシーで本番リリースに 承認を必要としており、CodePipelineの承認アクションで担保 ● 今回移行対象外のDBマイグレーションやLambdaデプロイもCodePipeline上で実行していた

Slide 23

Slide 23 text

ECS移行におけるCDツール選定 検討内容 ● GitHub Actionsを一部(ビルドのみ等)または全部(ビルド&デプロイ)で採用することも考えられた 検討結果 ● 現状と同じく全てをCodePipelineで実施することとした ○ 現状のデプロイフローを大幅に変更しなくて済む ○ 並行稼働期間中における Elastic BeanstalkとECSの同時並行デプロイも組みやすい ○ GitHub ActionsでもEnterpriseであれば承認を組み込めるが、当社プランは Enterprise未満 新たな課題 ● CodePipelineを選択したことで 「stg環境で作成したコンテナイメージを本番環境で使う」ことの実現に工夫を要することに ● 詳細は後半(3.6.CDパイプライン)で改めて説明 (参考:ECS用のCDパイプラインに対する考察 | reireias https://zenn.dev/reireias/articles/8e987af2762eaa)

Slide 24

Slide 24 text

3.4. IaC

Slide 25

Slide 25 text

当社のIaC化状況とリポジトリ管理 IaC化の状況 ● 全AWSリソースをTerraformで管理 リポジトリ管理 ● 全てのTerraformコードを専用のTerraform用リポジトリで一元管理 ● アプリケーションはアプリケーション用のリポジトリ で管理 ※一部例外として、 Lambdaとその周辺リソースの定義を、アプリケーションのリポジトリ内で Serverless Frameworkにて管理している

Slide 26

Slide 26 text

ECSのIaC化で生じる課題 ECSではデプロイの都度、タスク定義の新 revision作成とECSサービスの更新が発生する。 結果、デプロイ後にterraform planを行うと、これらの更新結果が 差分として検知されてしまう。 イメージ:aaa イメージ:bbb タスク定義 revision:1 ECSサービス revision:2 デプロイ対象:rev1 「メドピア AWS勉強会 ECS編 https://speakerdeck.com/reireias/medpeer-aws-seminar-ecs?slide=25」を元に作成 イメージ:aaa イメージ:bbb タスク定義 revision:1 ECSサービス revision:2 デプロイ対象:rev2 イメージ:aaa イメージ:bbb タスク定義 revision:1 ECSサービス デプロイ対象:rev1 新規イメージ作成 タスク定義に新revision作成 ECSサービス更新

Slide 27

Slide 27 text

ecspresso採用による課題解消 ecspressoとは ● ECS用のデプロイツール https://github.com/kayac/ecspresso ● ECSサービスとタスク定義を管理 しつつ、デプロイを行う 👉ECSサービスとタスク定義を Terraformではなくecspressoが管理することによって、  デプロイ後にterraform planで差分が出る問題を解消 できる

Slide 28

Slide 28 text

ecspresso採用のその他の狙い ECSサービスやタスク定義のファイルを アプリケーションのリポジトリに配置 することで 開発チームがECSをメンテナンスしやすくなることが期待 できる(例:ECSへの環境変数の設定など ) アプリケーションのリポジトリ . ├── ... ├── batch ├── db ├── ... └── web ├── ... └── ecspresso ├── ecspresso.yml # ecspresso全体の設定ファイル ├── ecs-service-def.json # ECSサービスの定義 ├── ecs-task-def.json # タスク定義 └── README.md

Slide 29

Slide 29 text

3.5. デプロイ

Slide 30

Slide 30 text

ecspressoをCodeBuildで実行する 当社はCDにCodePipelineを選定したため、CodeBuildでecspressoを実行 version: 0.2 env: shell: bash variables: ECSPRESSO_VERSION: 2.0.1 phases: install: commands: - curl -sL -O https://github.com/kayac/ecspresso/releases/download/ v${ECSPRESSO_VERSION}/ecspresso_${ECSPRESSO_VERSION}_linux_amd64.tar.gz - tar xzvf ecspresso_${ECSPRESSO_VERSION}_linux_amd64.tar.gz - sudo install ecspresso /usr/local/bin/ecspresso - ecspresso version # 略 build: commands: - cd path/to/ecspresso - ecspresso deploy --envfile=${SR_ENV}.env . └── ecspresso ├── buildspec.yml ├── ecspresso.yml ├── ecs-service-def.json ├── ecs-task-def.json └── README.md

Slide 31

Slide 31 text

ECSサービスやタスク定義ファイルを環境間でDRYに ● 機密性の無い値は環境別の envファイルに記述し、--envfileオプションで読み込ませる ○ ecspresso deploy --envfile=${SR_ENV}.env ※SRは、smartroundの意 ● 機密性のある値はenvファイルに記述せず、パラメータストアから読み込ませる (ECS標準の機能) . └── ecspresso ├── buildspec.yml ├── ecspresso.yml ├── ecs-service-def.json ├── ecs-task-def.json ├── stg.env ├── prd.env └── README.md # stg.envより抜粋 FOO=xxxxx BAR=xxxxx # ecs-task-def.jsonより抜粋 { "containerDefinitions": [ { "environment": [ { "name": "FOO", "value": "{{ must_env `FOO` }}" }, # 略 ], "secrets": [ { "name": "BAZ_API_KEY", "valueFrom": "/smartround/{{ must_env `SR_ENV` }}/baz_api_key" }, # 略

Slide 32

Slide 32 text

関連AWSリソースのARNやIDの取得について ECSサービスの定義には以下の指定が必要 ● ターゲットグループの ARN : arn:aws:elasticloadbalancing:region:account_id:targetgroup/name/xxxxxxxxxxxxxxxxxx ● サブネットID : subnet-xxxxxxxxxxxxxxxxx ● セキュリティグループ ID: sg-xxxxxxxxxxxxxxxx  👉これらをファイルに値を直接記述すると可読性やメンテナンス性が落ちる懸念がある tfstate読み込み機能について ● ecspressoは、Terraformのtfstate(状態管理ファイル)の読み込みをサポート する ● そのため、値をファイルに記述すること無く tfstateから前述のARNやIDを取得することが可能 (※) ※補足:tfstate(状態管理ファイル)は、S3やTerraform Cloudなどに保管されるもので、 Terraform用のリポジトリでもGit管理はされるものではない

Slide 33

Slide 33 text

tfstate読み込み機能の利用の検討 しかし、以下の観点から tfstateの読み込み機能は「現段階では」利用を見送った ● Terraformへの一定の理解も必要で、 当社の開発チームが ECSを保守する敷居が上がると判断 ● アプリケーションのリポジトリに、 別リポジトリのTerraformの知識を持ち込むことへのためらい 👉利用することで開発者に Terraformへの関心を持たせる下地となるとの考えもあり、今後継続検討  参考:DevOps実装初期フェーズの組織がTerraformとecspressoで求めるAmazon ECS CI/CDの最適解 | Tocyuki     https://speakerdeck.com/tocyuki/aws-ecs-cicd-with-terraform-and-ecspresso?slide=37

Slide 34

Slide 34 text

AWS CLIの利用 前述の値は、CodeBuild上でAWS CLIを実行して取得することとした (AWSリソースのnameプロパティやNameタグの値を元にリソースを特定して取得 ) CodeBuild上でAWS CLIを実行して取得することとし # buildspec.ymlより、ターゲットグループ ARN取得部分を抜粋 pre_build: commands: # 略 - TARGET_GROUP_ARN=$(aws elbv2 describe-target-groups \ --names "smartround-${SR_ENV}-webapp" \ --query 'TargetGroups[0].TargetGroupArn' --output text) - export TARGET_GROUP_ARN - echo "$TARGET_GROUP_ARN" # ログ出力用 # 略 build: commands: - cd path/to/ecspresso_dir - ecspresso deploy \ --envfile=${SR_ENV}.env # ecs-service-def.jsonより抜粋 { # 略 "loadBalancers": [ { "containerName": "nginx", "containerPort": 80, "targetGroupArn": "{{ must_env `TARGET_GROUP_ARN` }}" } ], # 略 }

Slide 35

Slide 35 text

配列を取得する場合の考慮 配列を取得する場合、 json側ではvalueの前後にダブルクォートを付けないようにする # buildspec.ymlより、サブネットID取得部分を抜粋 pre_build: commands: # 略 # JSONに埋め込み可能な配列形式で取得したいので、 --output textは付けない - SUBNET_IDS=$(aws ec2 describe-subnets \ --filters "Name=tag:Name,Values=smartround-${SR_ENV}-private-*" \ --query 'Subnets[*].SubnetId') - export SUBNET_IDS - echo "SUBNET_IDS" # ログ出力用 # 略 build: commands: - cd path/to/ecspresso_dir - ecspresso deploy \ --envfile=${SR_ENV}.env # ecs-service-def.jsonより抜粋 { # 略 "networkConfiguration": { "awsvpcConfiguration": { "assignPublicIp": "DISABLED", "subnets": {{ must_env `SUBNET_IDS` }}, # 略 } }, # 略 } [ "subnet-xxxxxxxxxxxxxxxxxx", "subnet-xxxxxxxxxxxxxxxxxx" ] "{{ … }}"とはしない。 ファイルとしてはIDEに怒られるが最終的に正しい jsonとしてrenderされる。

Slide 36

Slide 36 text

3.6. CDパイプライン

Slide 37

Slide 37 text

現状のブランチ戦略とデプロイ releaseブランチをmainブランチにマージすると、本番デプロイが開始される

Slide 38

Slide 38 text

想定するイメージビルドとECSデプロイの流れ stg環境で作成したコンテナイメージを本番環境でも使いたい (イメージタグはコミットハッシュとする ) ※補足:CodePipelineでは他にDBマイグレーションやLambdaへのデプロイも行なっているが、上図含め以降の図では記載を割愛

Slide 39

Slide 39 text

生じる課題 mainのマージコミット(ccc)が本番デプロイされるため、 stg環境で作成したイメージを使う流れにならない

Slide 40

Slide 40 text

解決案 releaseブランチのHEADにバージョンタグを付け、これをトリガーに本番デプロイする

Slide 41

Slide 41 text

解決を阻む制約 CodePipelineの制約 ● 接続先を特定のブランチに固定して使用する必要がある ● そのため、任意のタグをデプロイするようなことができない ○ 単発のCodeBuildは起動の都度柔軟にブランチやタグを選べるが、 CodePipelineは不可 (2024年2月追記) 上記はCodePipeline v1の制約。 本登壇資料は2023年6月当時の情報を元に作成されているが、 2023年10月に発表されたCodePipeline v2で はタグを起動トリガーとすることが可能になり、以降も起動トリガーまわりで選べる条件が順次アップデートされ ていっている。

Slide 42

Slide 42 text

CodePipelineの制約を乗り越える ● CodePipelineの入力ソースをGitHubからS3に変更 ○ S3変更検知用のCloudTrailとEventBridgeが作成される(※) ● GitHub ActionsからS3にアップロード 👉任意のタグでCodePipelineを起動可能となる ※マネコンで変更した場合の標準の構成。  これらを削除して、新規 EventBridgeとS3イベント通知の組み合わせでも  実現可能。

Slide 43

Slide 43 text

デプロイ運用の変化に対する検討 デプロイ開始トリガーが mainブランチへのマージだったのが releaseブランチへのタグ付けに変更 される ● 結果としてBranch Deploymentと呼ばれる手法となる ○ 参考: https://github.com/github/branch-deploy#branch-deployment-core-concepts- ● デプロイ後、対象の環境の安定を確認できたら releaseブランチをmainブランチへマージする運用

Slide 44

Slide 44 text

デプロイ運用の変化に対する検討 一方で、SREとして当社の開発チームの メンタルモデルを考えた ● mainブランチへのマージによるデプロイの方が 直感的ではないか?(※1) ● releaseブランチへのタグ付け後の、 mainブランチへのマージを忘れる というリスクもある(※2) 👉検討の結果、デプロイ開始のトリガーを 引き続きmainブランチへのマージとする ことにした ※1 ※2 開発チームがこうした意見を言って運用の変更に反対したということではありません

Slide 45

Slide 45 text

GitHub Actionsの見直し マージ後のmainブランチの直前のコミットにタグ付け し、そのタグのソースを S3にアップロード

Slide 46

Slide 46 text

GitHub Actionsの仕様に関する補足(AWSとは関係無し) なぜS3アップロードのWFのトリガーをタグプッシュではなく、上のタグ付け WFの完了としたのか? 👉各WFでは標準のGITHUB_TOKENを使っており、トリガーが on:pushのWFは連続起動しない。  on:workflow_runなら起動する。 2つのWFを1本化すれば良いのではないか? 👉任意のタグを指定して S3アップロードのみを  手動起動(workflow_dispatch)できるようにも  しておきたかったため、 WFを分離している。  失敗時の回復性を高めたい という意図。 main

Slide 47

Slide 47 text

完成したCDパイプライン CodePipelineで運用体験を変えず に、stgで作成したイメージを本番環境で使用するパイプラインを実現 ※説明を省略したが、上図の通り、 stgデプロイ時についても S3アップロード方式に併せて変更している

Slide 48

Slide 48 text

4. まとめ

Slide 49

Slide 49 text

まとめ 1. stg環境で作成/検証済みのイメージを本番環境でも使用するために ○ 設定を環境変数で吸収 ○ ECRレプリケーションの利用 ○ GitHub Actions、S3、CodePipelineを活用したCDパイプラインの構築 2. イメージビルドのベストプラクティスに負担無く沿うために ○ Jibを採用しDockerfileレスでベストプラクティスに追随 3. ECSのIaC化で生じる課題を解消しつつ開発チームでも ECSをメンテしやすくするために ○ ecspressoの採用 4. 開発チームが運用しやすいように ○ 各所で現状の知識やメンタルモデル等を考慮した運用設計

Slide 50

Slide 50 text

スマートラウンドでは新しいメンバーを募集中です 私たちと一緒にスタートアップが可能性を最大限に発揮できる世界をつくりませんか? jobs.smartround.com