Terraform リポジトリをマージして CI/CD を改善した話

こんにちは。 SRE の @suzuki-shunsuke です。

Terraform のコードを管理する複数のリポジトリ (以下 Terraform リポジトリ) を一つにまとめて CI/CD の品質およびメンテナンス性を改善した話をします。

弊社のこれまでの CI/CD 改善の取り組み

弊社ではこれまで Terraform の CI/CD を日々改善してきました。 その幾つかは既にブログで紹介していますのでそちらをご参照ください。

また、上記の記事以外で以下のようなこともやってきました。

  • lint の実行と失敗した場合に PR コメントへの通知
    • terraform fmt, validate
    • tflint, tfsec, conftest
    • shfmt, shellcheck
    • etc

CI がなぜ失敗したのかわざわざ CI のページまで見に行かなくても PR コメントで分かりますし、 CI のページで確認するのに比べ、失敗したコマンドとその出力が抜粋されているのでかなりわかりやすくなっています。 github-comment を使っています。

image

  • tfcmt で plan の結果をわかりやすくコメント

image

  • 古いコメント(CIの結果)を自動で非表示化

CI の結果をコメントしていると、古いコメントは邪魔になるので自動で非表示にしています。

image

  • apply に失敗したら PR の author にメンション付きで PR にコメント

image

PR でメンションされたら Slack にも通知が来るようにしておくと便利です。

image

課題: 複数の Terraform リポジトリの CI/CD をメンテするのが大変

上述のような取り組みのおかげで、 CI/CD はかなり快適なものになりましたが、課題もありました。

元々弊社には管理対象に応じて複数の Terraform リポジトリがありました。

すべてのリポジトリのリリースフローや CI/CD は基本的にやることが同じです。 GitHub Flow を採用しており、 PR の CI で terraform plan を実行し、 PR がマージされたら terraform apply を実行しています。

上述の改善は sapuri-terraform を中心に行われており、他のリポジトリが追随できていないケースが良くありました。 新しい仕組みを最初に導入するのはとても楽しい仕事ですが、それを他のリポジトリに横展開するのは面白みに欠ける仕事でした。 横展開と言っても単にコピペで終わりではなく、レビューしやすいように PR の体裁を整えたり、ハードコードされている部分をリポジトリごとに微妙に変えたり、問題がないか動作確認したり、どの変更が追随できていてどの変更が追随できていないのか管理したりする必要があり、かなり大変でした(というかできてませんでした)。 なお今回は対象のリポジトリが 5 つですが、自分の経験上、これが 2 つだけだったとしても程度の差はあれど同様の問題があると思っています。

そこでリポジトリを一つに統合することでメンテナンス性を改善するとともに、 一つの改善がすべてのリソースの CI/CD に行き届くようにしました。

なお、リポジトリをマージすることで Git の履歴が引き継がれないという点に関しては、最初から諦めることにしました。

ディレクトリ構成

大雑把に言うとこんな感じになりました。

deadmanssnitch/ # deadmanssnitch リポジトリをコピー
slo/ # slo リポジトリをコピー
pingdom/ # pingdom リポジトリをコピー
aws/
  studysapuri/ # リポジトリ直下にあったものを aws/studysapuri 配下に移動
    services/
      <サービス名>/<環境>/
        versions.tf
        ...
  quipper/ # quipper-terraform リポジトリをコピー
    services/
      <サービス名>/<環境>/
        versions.tf
        ...

基本的にはリポジトリを sapuri-terraform にまるごとコピーして不要なファイルを消すだけですむようにしました。

もし既存のリソースを無視してまっさらな状態から始めるのであれば、 Terraform Provider 単位で分けるのではなく、プロダクト単位で分けるのもありだったと思います。 しかし今回のように元々 Provider 単位で分かれていたものをわざわざプロダクト単位に構成を変更するのは、得られるメリットの割にあまりにも工数がかかりすぎます(Terraform の場合、 State の操作が必要ですからね)。 State の数が少なければそこまで工数はかからないかもしれませんが、 100 個以上 State がある状況ではかなり厳しいです。

マージの手順

いきなり全部マージするのではなく、まず deadmanssnitch だけを sapuri-terraform にマージし、上手くマージできるか確認しました。 このときに既存の CI/CD のスクリプトをより汎用的な形に色々修正したりしました。

その後、 slo, pingdom, quipper-terraform といった順に一つずつマージしていきました。 deadmanssnitch をマージする際にスクリプトを修正していたので、以降は大きくスクリプトを修正する必要はなく、比較的簡単でした。 quipper-terraform に関しては state の数が多かったので、何回かに分けて移行を行いました。

Terraform の CI/CD を CodeBuild に移行した話 - Quipper Product Team Blog でも紹介したように、 sapuri-terraform では対象の state を動的に検出して CI を実行しているため、 新しくコピーして state を追加したところで CI/CD の設定を大幅に更新する必要はありませんでした。

そのままだとコピーしたコードが tflint などの linter に引っかかるケースもあったため、随時修正しました。 リポジトリをマージすることで、今までちゃんと lint されてなかったコードも lint されるようになりました。

複数の AWS アカウントのサポート

Terraform の CI/CD を CodeBuild に移行した話 - Quipper Product Team Blog でも紹介したように、 Terraform の CI には AWS CodeBuild を使っています。 sapuri-terraform と quipper-terraform では AWS のアカウントが違うため、複数の AWS アカウントをサポートできるようにする必要がありました。 2 つの AWS アカウントでそれぞれ CodeBuild が実行されるようにしつつ、 aws/studysapuri 配下の CI/CD は sapuri 側の CodeBuild で実行し、それ以外(deadmanssnitch, pingdom, slo なども含む)は quipper 側の CodeBuild で実行するようにしました。

この辺は環境の差異を環境変数としてパラメータ化し、それ以外の CI/CD のコード(シェルスクリプト) は共通化することで実現しました。

だいたいこんな感じです。

case "${CODEBUILD_BUILD_ARN}" in
arn:aws:codebuild:ap-northeast-1:xxx:*) . ci/studysapuri.env ;;
arn:aws:codebuild:us-east-1:xxx:*) . ci/quipper.env ;;
esac

ci/quipper.env

export LIST_STATE=ci/list-states-quipper-codebuild.sh
export BUILDSPEC_S3_BUCKET=xxx
# ...

ci/list-states-quipper-codebuild.sh

#!/usr/bin/env bash
# 対象の state の一覧を出力するスクリプト

set -eu
set -o pipefail

cd "$(dirname "$0")/.."

echo deadmanssnitch
echo pingdom
git ls-files slo | grep -E "/terraform\.tf$" | xargs dirname
git ls-files aws/quipper/services | grep -E "/terraform\.tf$" | xargs dirname

Require branches to be up to date before merging の無効化

リポジトリを統合する上では、 Pull Request の terraform plan の実行結果を S3 に保存して安全に apply - Quipper Product Team Blog でも紹介した Require branches to be up to date before merging の設定の無効化が非常に重要でした。 これが無効化出来ていなければ、リポジトリをマージしたところで update branch しないと merge 出来ない頻度が増え、 Developer Experience を大きく損なうことになっていたでしょう。

結果

リポジトリを一つにマージしたことで sapuri-terraform 以外で管理されていたリソースの CI/CD が大きく改善されました。

加えて、今後新たな Provider への対応(CI/CD のセットアップ)も非常に容易になりました。 例えば新たに foo という Provider を使ってリソースを管理したいとなった場合、 deadmanssnitchslo などと同様に foo ディレクトリを作ってそこに Terraform の設定ファイルを置き、 CI/CD の設定を少し修正するだけで作業は完了します。 多少大袈裟に言うと、 Terraform による IaC のプラットフォームを作り上げることが出来ました。