AWS リソース管理の Terraform 移行

技術部 SRE グループの鈴木 (id:eagletmt) です。クックパッドでは Codenize.tools を用いて様々なリソースをコードで管理してきましたが、現在では大部分が Terraform へと移行しています。Terraform の使い方等については既に沢山のドキュメントや紹介記事があるので本エントリでは触れず、なぜ Terraform へと移行しているのか、どのように Terraform を利用しているのかについて書いていきます。

Terraform 移行の理由

クックパッドでは自分と同じく SRE グループに所属している菅原 (id:winebarrel) によって開発された Codenize.tools のツール群を利用して IAM、Route 53、CloudWatch Alarm、CloudWatch Events 等をコードで管理し、いわゆる GitOps を実践してきました。Codenize.tools による AWS リソース管理は基本的に1アカウント内のすべてのリソースを対象に動作します *1。これに従うと、ある AWS サービスに属する全てのリソースの管理は1つのリポジトリに集約されることになります。実際、社内には cloudwatch というリポジトリや iam というリポジトリが存在します。この構成は1つのチームが1つの AWS アカウント内のすべてのリソースを管理しているような場合は抜け漏れ無くコードで管理できるため非常に有効です。cloudwatch と iam を1つのリポジトリにまとめるか別のリポジトリに分けるかという選択肢はありますが、1つの AWS アカウント内のすべてのリソースを SRE グループが管理していたクックパッドでは自然な構成でした。

しかしマイクロサービス化が進みセルフサービス化が進むと、様々なチームで様々なリソースが必要になり、SRE グループがあらゆる AWS リソースを管理することが困難になっていきました。新しくアプリケーションをデプロイしたい人たちにとっても、複数のリポジトリに別々の pull-request を出す必要があり面倒に感じられていました。また、ELB + ECS/EC2 + RDS という伝統的な構成ではなく AWS SAM (Serverless Application Model) を利用したサーバレスな構成も選択されるようになり、CloudFormation で管理されるリソースも増えていきました。このような状況では Codenize.tools の「1アカウント内のすべてのリソースを対象に動作」という挙動は次第にフィットしなくなっていきました。

そこで、選択的にリソースを管理することができ、多くの現場で利用されている Terraform へと移行する方針になりました。これまでも Codenize.tools の対象外だった Auto Scaling Group や RDS インスタンス等の管理に Terraform は使われていましたが、Codenize.tools の対象でも Terraform を利用するように移行が始まりました。現時点では IAM、Route 53 以外の AWS リソースは一通り Terraform への移行が完了しています。

Terraform 運用

全面的に Terraform へと移行するにあたって、いくつか工夫した点があるのでそれぞれ紹介します。

tfstate の単位

Terraform では管理対象のリソースに関する情報を state ファイル (以下 tfstate と呼ぶ) にまとめているわけですが、Terraform を利用するにあたってこの tfstate をどのような単位で分割するのかという話題があります。1つの tfstate ですべての AWS リソースを管理するのは少なくともクックパッドの規模では無謀で、もしそうしたら terraform plan の時間が非常に長くなってしまいます。 クックパッドでは1つのリポジトリに Terraform ファイルを集約し、その中でプロジェクト単位でディレクトリを分けて記述していくことにしました。1つのディレクトリが1つの tfstate に対応します。

.
├── service-1
│   ├── aws.tf
│   ├── backend.tf
│   └── rds.tf
├── service-2
│   ├── acm.tf
│   ├── aws.tf
│   ├── backend.tf
│   ├── iam.tf
│   ├── rds.tf
│   ├── security_group.tf
│   └── vpc.tf
└── service-3
     ├── acm.tf
     ├── aws.tf
     ├── backend.tf
     ├── elb.tf
     ├── s3.tf
     ├── sg.tf
     └── vpc.tf

1つのリポジトリにしたのは一覧性を確保するためと、後述する CI の整備を楽にするためです。

linter の整備

AWS リソースの追加、削除、変更は Terraform 用のリポジトリへの pull-request で行うわけですが、pull-request に対する CI として terraform plan の結果を表示したり terraform fmt 済みかチェックしたりすることに加えて、独自に用意した linter を適用しています。Terraform 向けの linter というと tflint が既に存在していますが、tflint がカバーしているような一般的なルールではなく、社内独自のルールを強制したかったため自作しました。 ルールとしては現時点では

  • タグ付け可能なリソースには Project タグを必ず設定する
    • クックパッドの AWS アカウントではコスト分配タグとして Project というタグが設定されており、コスト管理のために Project タグを設定しなければならない
  • Aurora MySQL を使うときに特定のエンジンバージョンを禁止
    • クックパッドでの典型的なワークロードで致命的な問題が発生するエンジンバージョンがあるため、そのバージョンの指定を避ける

を強制しています。

ちなみにこの linter を pull-request に対して実行するにあたって、見易さの観点から GitHub の Checks の機能を利用することにしました。linter のように行単位で指摘する箇所が分かる場合、Checks を使うと見易く表示できます。 https://developer.github.com/v3/checks/

remote state の取り扱い

tfstate の保存場所としては S3 を使っていますが、RDS インスタンスの master user のパスワードのようなセンシティブな値の取り扱いを考慮する必要があります。たとえば aws_rds_cluster を新規に作成するとき、master_password に直接パスワードを書くと GitHub リポジトリで社内全体に公開されてしまいます。そこで SSM の Parameter Store に SecureString として保存して aws_ssm_parameter で参照したり、Vault の KV backend に保存して vault_generic_secret で参照したりといった方法を思い付きますが、これにより Terraform ファイル上からはパスワードが消えても tfstate にパスワードが平文で記録されてしまいます。この問題は upstream でも認識されていて tfstate 自体をセンシティブなデータとして扱うことを推奨しています。 https://www.terraform.io/docs/state/sensitive-data.html

しかしながら社内のエンジニアであればどのプロジェクトでも terraform plan は実行できるという状態を目指したかったので、パスワードのようなセンシティブな値は tfstate にはダミーの値を指定するという方針を試してみています。たとえば aws_rds_cluster を新規作成する場合は

resource "aws_rds_cluster" "my-awesome-app" {
  ...
  master_password = "pasuwa-do"
  ...
}

のように記述して terraform apply し、その後 mysql コマンド経由や ModifyDBCluster API で正式なパスワードに変更します。API を通じて master_password を得る手段が無いので Terraform は tfstate にある値を信じるしかなく、tfstate にも Terraform ファイルにも pasuwa-do と書かれているので差分が発生せず、センシティブな値を tfstate にも Terraform ファイルにも書き込まずに Terraform でリソースを管理することができています。

今後

Codenize.tools から Terraform への移行は進んでいるものの、最初の移行時にはプロジェクト単位に分割することを諦めたため、Terraform 管理へと変更はできていても適切なプロジェクト内の tfstate で管理させることはまだ十分にはできていません。現在はたとえば cloudwatch のような tfstate に様々なプロジェクト向けの CloudWatch Alarm が混ざって管理されている状態です。これを分解していくことは地味な作業ではありますが、今後も少しずつプロジェクト単位で管理された状態へと移していこうとしています。

また、多くの現場で実践されていそうな Terraform の自動適用もまだ実践できていません。Terraform 管理への移行や Terraform 管理内での tfstate の変更も徐々に落ち着いていくと思われるので、master にマージされたら自動的に terraform apply される状態を目指したいです。

*1:--exclude や --target で対象を限定できるようになっているものもあります

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/