ENECHANGE Developer Blog

ENECHANGE開発者ブログ

【AWS ECS】チーム間の責任分界点とタスク定義ファイルの扱い方

ENECHANGE所属のエンジニア id:tetsushi_fukabori こと深堀です。
愛犬のバーニーズマウンテンドッグを飼い始めてから1年経ってやっと餌をあげすぎなことに気が付きました。
食べすぎてお腹壊し気味だったんだね…ごめんね…。

私はというとお腹は壊さないものの食べすぎて簡単に太ってしまうタチなので2022年の夏からあすけんを始めています。
現時点で明確に言えることは「脂質を摂るは易し、蛋白質を摂るは難し」です。


今回は「既存アプリケーション基盤のコンテナ化プロジェクト」を推進する中で考えたクラウドインフラの管理に対する社内のチームの責任分界点と、実運用を考えたタスク定義ファイルの取り扱いについてです。
会社ごとの事情に依存する話だと思うのでいち事例としてご紹介です。

背景

コンテナ化プロジェクト開始前にプロダクトに関わるチームやリソースがどのような構成だったのかを書きます。
また、この話はコンテナ化プロジェクトがどのような構成であったかも重要なポイントなので併せて説明します。

チームの構成

今回コンテナ化をすすめるアプリケーションは弊社のエネルギーデータ事業部のプロダクトでした。

enechange.co.jp

この事業部の中はいくつかのチームに分かれていて、それぞれのチームがそれぞれ1つ以上のプロダクトを開発・管理しています。
1つのプロダクトには複数人のエンジニアが担当をしていますが、複数のプロダクトを担当しているエンジニアが多いです。
クラウドインフラを利用したプロダクト開発を行っていますが、このチーム内にクラウドインフラの管理を専門とするエンジニアはいません。
このチームを以降 アプリチーム と呼びます。

アプリチームにクラウドインフラの専門エンジニアがいないため、インフラの構築や運用は別のチームが行っています。
インフラの管理チームは事業部やプロダクトを横断してクラウドインフラの構築・運用を行っています。
このチームを以降 インフラチーム と呼びます。

基本的には1つのプロダクトに対してアプリチームとインフラチームの両方が関わってプロダクトを開発し、運用保守を担っています。
深堀はアプリチームとして稼働し、その後インフラチームに異動したため、弊社では比較的珍しいアプリもインフラもどちらも触るエンジニアとしてキャリアを築いています。

チーム構造

チーム間の役割の分担

アプリチームとインフラチームの役割分担は比較的明確です。

アプリチーム インフラチーム
アプリケーションの設計〜構築・運用とクラウドインフラの要件決定 クラウドインフラの設計〜構築・運用

アプリケーションについては基本的にはアプリケーションチームが責任を持ちます。
アプリケーションによってクラウドインフラの要件が決まるので、クラウドインフラをどのように利用したいかもアプリチームが決定する範囲です。

インフラチームはアプリチームの要件からクラウドインフラを設計し構築・運用することに責任をもちます。
アプリケーションチームで決めにくいクラウドインフラの詳細な設計やセキュリティなどもインフラチームが行いますし、アプリチームの要件に対してセキュリティ上の懸念などを伝えて設計変更を促したりすることもあります。

GitHubリポジトリの構成

アプリチームとインフラチームはそれぞれ管理するものが異なるGitHubリポジトリを保有しています。
構成は以下のとおりです。

アプリチームのGitHubリポジトリ インフラチームのGitHubリポジトリ
アプリケーションのソースコード インフラ構成管理のソースコード

アプリチームのGitHubリポジトリにはアプリケーションコードが主に入っており、少量のデプロイ関連の設定ファイルなどが含まれます。
インフラチームのGitHubリポジトリにはterraformのインフラ構成管理のソースコードが主に入っており、一部Lambdaなどのアプリケーションコードなどが含まれます。

それぞれのチームは主としてそれぞれのチームのリポジトリを管理しており、互いのリポジトリに対して変更を加えに行くことはまれです。

コンテナ化プロジェクトの推進体制

今回のコンテナ化プロジェクトの推進体制は、基本的には作業者は深堀一人でした。

直接的に開発に携わったことのないプロダクトだったので、当該プロダクトを担当しているアプリチームのメンバーにサポートをもらったり、インフラチームのメンバーにアドバイスやレビューを頂いたりしましたが、基本的には全て一人で推進していました。
これはアプリチームに体制上の余力が少なかったことが大きな要因で、アプリチームと学びながら双方のコードベースに変更を入れていく、という動きがとれませんでした。

このためアプリチームはこのプロジェクトの推進段階ではクラウドインフラに関する新たな知識を手に入れる活動はできていませんでした。

やりたいこと

今回のコンテナ化プロジェクトでは前述の通りアプリケーションチームの新たなスキル獲得はプロジェクトの中に織り込めませんでした。
もちろん最後に教育ステップは設けましたが、やはり自分で設計して触ってみないことにはなかなか勘所は身につきません。

この状態でアプリチームは実プロダクトの運用ができなければいけないので、アプリチームが管理するGitHubリポジトリには大きな変更をもたらさない対応が必要でした。
また、前述のアプリチームとインフラチームの役割分担を変えることも負荷が高くなると考え、役割分担を変えずに運用を回せる必要がありました。

コンテナ化後のシステム構成

コンテナ化プロジェクト後、アプリケーションのデプロイフローは以下のようにGitHub Pushをトリガにしたデプロイパイプラインを構築しました。

コンテナ化後のデプロイフロー

この構成自体はかなり一般的な構成で、特段の工夫をしていません。

この構成を取ることで一般的にはアプリリポジトリに以下のような変更が入ります。

  • Dockerfileの追加
  • デプロイ用設定ファイル(appspec.yml)の追加
  • ビルド用設定ファイル(buildspec.yml)の追加
  • タスク定義ファイル(taskdef.json)の追加

これらのファイルは今回のプロダクトのアプリチームは通常の開発では利用しないものです。
こうした普段使わないファイルで、馴染みがなく複雑なものがリポジトリに含まれていくとアプリチームとしてはノイズになりがちであり、馴染みがない分事故も起こしやすくなります。

これらのファイルのうち、Dockerfileは比較的馴染みがあり、デプロイ用設定ファイルは固定的な短い内容で済むものでしたのであまり問題にはならないと予想できました。

docs.aws.amazon.com

ビルド用設定ファイルも大部分は上から順にシェルのコマンド実行が書かれているYAMLファイルで、CIなどでよく使われている形式である分馴染みがありました。

docs.aws.amazon.com

問題はタスク定義ファイルです。

タスク定義ファイル(taskdef.json)の特殊性

タスク定義ファイルはECSで稼働するタスク(複数のコンテナをまとめた論理グループで、感覚的には1つのアプリケーションサーバーに相当)の設定ファイルです。
記載内容は

  • タスクを構成するコンテナの指定
  • コンテナで使うイメージの指定
  • コンテナ間のボリューム共有設定
  • タスクやコンテナに割り当てるメモリ・CPU量の指定
  • 環境変数の設定
  • ロギング方法の指定

など多岐にわたり、ECSの機能に密接に関連しています。

docs.aws.amazon.com

このファイルは内容からするとアプリケーションの要素そのものでもあり、ファイルを理解し編集するためにはクラウドインフラの運用や振る舞いについての知識が求められるものです。
つまり既存のアプリチームとインフラチームの両方の領域にまたがったファイルです。

このファイルは内容からすると明確にアプリチーム管理であるべきですが、現状のアプリチームの体制やナレッジではこのファイルの管理は困難であり、誤った設定や必要以上にコストがかかる設定変更が行われるなどの懸念がありました。
一方でインフラチームの管理とするとアプリチームが変更を加えたいときに整合性の管理が困難です。

DevOpsが体現できたチームであれば扱いやすいファイルですが、チームが分かれた体制である弊社では扱いに苦慮するポイントになりました。

やったこと

結論として、今回のプロジェクトでは以下のような管理とする決定を行いました。

  • タスク定義ファイルの管理主体はアプリチームとする
  • 初期のタスク定義はプロジェクト推進担当(深堀)が作成し、アプリチームに引き渡す
  • インフラチームのGitHubリポジトリにはアプリチームに引き渡した段階のタスク定義のみIaCとして残し、以降更新はしない
    • あくまで初期状態の記録のため
  • アプリチームのGitHubリポジトリではタスク定義ファイルそのものは管理に含めない
  • タスク定義はビルド実行時に最新のタスク定義とアプリチームのリポジトリの内容から生成する
    • アプリチームのGitHubリポジトリでは環境変数のみファイル管理する

ポイントは最後の2つです。
タスク定義ファイルはデプロイに必要なのでデプロイまでに用意ができていればよいです。
一方でタスク定義ファイルの中でアプリチームのデプロイとともに変更したいポイントは限定的であることから、アプリチームのGitHubリポジトリで管理するものも限定的なものとした形です。

タスク定義ファイルを管理しない運用方法の設計

アプリチームがタスク定義に入れたい変更は以下のように限定されたものでした。

  1. 最新のソースコードからビルドされたイメージをタスク内のコンテナに反映したい
  2. タスク内のコンテナに与える環境変数を任意に設定しデプロイと同タイミングで適用したい

アプリチームは基本的に上記がコントローラブルであれば良く、また1.に関しては特に意識もせず自動的にアプリケーションに反映されていてくれれば良い状態でした。
このためアプリチームのリポジトリからはタスク定義ファイルを除き、代わりに環境(production, stagingなど)毎・コンテナ毎の環境変数を指定するJSONファイルのみ配置しました。
これをもとにビルド時に最新のタスク定義を参照し、環境変数とコンテナイメージを注入することで新たなタスク定義ファイルを生成するようにビルド設定ファイルで工夫する形にしました。

実装内容

環境変数ファイルは以下のような構成です。

$ pwd
/Users/tetsushifukabori/project/application_name/.container

$ tree
.
├── appspec.yml
├── buildspec.yml
├── environment_variable
│   ├── production.json
│   └── staging.json
├── container1
│   └── Dockerfile
└── container2
    └── Dockerfile

$ cat environment_variable/staging.json
{
  "container1" : [
    {
      "name": "HOGE",
      "value": "fuga"
    }
  ],
  "container2" : [
    {
      "name": "PIYO",
      "value": "🐣"
    }
  ]
}

アプリチームが管理しなければならないのは環境変数ファイルのみで、ファイル自体が命名からして指定されている内容が自明であり扱いやすいです。
また、デプロイと同期した環境変数の変更が可能になります。

これらをもとにbuildspec.ymlでは以下のようにビルドとタスク定義の生成を行っています。
なお、ここで利用している環境変数 TASK_DEFINITIONTARGET_ENVIRONMENT などはCodeBuildプロジェクトの環境変数として指定しています。
ビルドプロジェクトは環境と1:1で対応しているので固定値が指定可能です。

❯ cat buildspec.yml
version: 0.2

phases:
  pre_build:
    on-failure: ABORT
    commands:
      - # ビルドイメージのタグの決定など。省略。
  build:
    on-failure: ABORT
    commands:
      - docker image build . -f .container/container1/Dockerfile -t ${ECR_REPOSITORY_URL}:${CONTAINER1_IMAGE_TAG}
      - docker image build . -f .container/container2/Dockerfile -t ${ECR_REPOSITORY_URL}:${CONTAINER2_IMAGE_TAG}
  post_build:
    on-failure: ABORT
    commands:
      - docker image push ${ECR_REPOSITORY_URL}:${CONTAINER1_IMAGE_TAG}
      - docker image push ${ECR_REPOSITORY_URL}:${CONTAINER2_IMAGE_TAG}
      # 直近のタスク定義を取得
      - aws ecs describe-task-definition --task-definition ${TASK_DEFINITION} | jq '.taskDefinition' > .container/taskdef.json
      # コンテナイメージにビルドしたものを指定
      - jq --arg imageUrl "${ECR_REPOSITORY_URL}:${CONTAINER1_IMAGE_TAG}" '.containerDefinitions |= map(select(.name=="container1").image = $imageUrl // .)' .container/taskdef.json > .container/taskdef.json.1
      - jq --arg imageUrl "${ECR_REPOSITORY_URL}:${CONTAINER2_IMAGE_TAG}" '.containerDefinitions |= map(select(.name=="container2").image = $imageUrl // .)' .container/taskdef.json.1 > .container/taskdef.json.2
      # 各コンテナの環境変数を置換。
      - jq --argjson environmentVariables "$(<.container/environment_variable/${TARGET_ENVIRONMENT}.json)" '.containerDefinitions |= map(select(.name=="container1").environment = $environmentVariables.container1 // .)' .container/taskdef.json.2 > .container/taskdef.json.3
      - jq --argjson environmentVariables "$(<.container/environment_variable/${TARGET_ENVIRONMENT}.json)" '.containerDefinitions |= map(select(.name=="container2").environment = $environmentVariables.container2 // .)' .container/taskdef.json.3 > .container/taskdef.json.4
      # artifactにわたすためにルートディレクトリに移動
      - mv .container/taskdef.json.4 ./taskdef.json
      - mv .container/appspec.yml ./appspec.yml

artifacts:
  files:
    - appspec.yml
    - taskdef.json

CodeBuildのビルド環境(のうち少なくとも aws/codebuild/amazonlinux2-x86_64-standard:4.0 のビルド環境)では jq コマンドが使えるのでこれをゴリゴリに使ってJSONの編集が可能です。
CodeBuildの実行ロールにECSタスクの DescribeTaskDefinition アクションの許可があれば最新のタスク定義が取得可能です。
DescribeTaskDefinition APIは最新のアクティブなタスク定義を返してくれるため比較的扱いやすいです。

メリットとデメリット

今回の実装のメリットとデメリットを検討します。

メリットは「アプリチームが管理可能なファイルのみをアプリチームのGitHubリポジトリに配置できる」ことと、「環境変数以外の設定ファイルの先祖返りが防げる」です。
前者はもともと目指していたものですが、これに加えて例えばアプリチームが緊急対応でマネコンから設定変更を行ったとしても、デプロイ時に最新のタスク定義を参照するので先祖返りしません。
GitHubリポジトリで管理しているとマネコンからの変更に追従が必要ですが、その点は楽になると思います。

デメリットは「アプリチームがタスク定義そのものを管理していない」「最新のタスク定義を参照してほしくない場合でも意図せず参照する」です。
アプリチームは主体的にタスク定義を管理・更新するのが望ましいと思いますが、この方式では知らないうちにタスク定義が決定される部分があるため、主体的に管理する意識にはなりにくいと思います。
また、環境変数以外のタスク定義に変更を加えたい場合はマネコン操作になるため、マネコンの操作習熟やマネコンから変更しにくい内容の変更は大変になります。

加えて最新のタスク定義を自動で参照してしまうため、例えば最新のタスク定義として壊れたものを作ってしまった場合に次のデプロイで参照されてしまいます。
タスク定義をInactiveに変更すれば対処可能ですが、やはりブラックボックスにはなってしまっています。

まとめと想い

ECSのタスク定義をアプリチームのGitHubリポジトリから隠蔽し、ビルド時に生成することで現在のチーム体制・ソースコード管理体制に一致したクラウドインフラ設定管理を行いました。

私個人の理想としてはタスク定義を全てアプリリポジトリ管理とし、アプリチームもインフラチームもお互いの専門領域がクロスするようなチームにできれば嬉しいです。
アプリチームの中で「タスク定義を全て理解してアプリチームGitHubリポジトリで管理したい」という要望が出て、そのような管理体制になっていくことが最も望ましいです。
なんならこの管理方法が深堀のおせっかいで、最初からタスク定義をアプリチームが管理することになんの障壁もなく、即管理方法が変更されてくれるのが一番嬉しいかもしれません。

そのようなチーム体制になっていけるよう引き続き努力していきます!