エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

春の terraform お掃除日記

こんにちは、エムスリーエンジニアリングGの榎田です。趣味は数学とゲームです。エオルゼアで耐水綿布の相場が暴落するのを見て経済に興味を持ち始めました。

もうすぐ春、一般的には新たな出会いと別れの季節とされています。私たちのチームでも、新たなサービス・企画に向けて日々開発を進めています。新規サービスの立ち上げということは、インフラの構築も必須です。今日はそのようなサービス構築時のインフラ構築を aws terraform provider で行った際の雑多な思い出を述べようと思います*1

インフラ構築の背景

今回インフラを構築したサービスは、"Docpedia" という「臨床分野のエキスパートの知見をQ&A形式で共有する」コンセプトを持ったサービスを、他の分野に横展開したものです。

Docpedia の説明自体はこちらの記事などをご参照ください。

www.m3tech.blog

このようなサービスなので、インフラ構成は既存のものと自ずと似てきます。ということは、既存の terraform のコピペで最低限動くものは作れます。しかし、この「既存サービスの terraform」をそのままコピペするのは個人的には抵抗があり、いろいろな部分を直したいという気持ちがありました。どのあたりを直したのか、その意図はどのようなものなのかについて述べていくことにします*2

ディレクトリ構成の変化

横展開前も後も、QA環境を2系統、本番環境を1系統用意します。

横展開前の terraform では、概ね以下のようなディレクトリ構成になっていました;

// 実際には backend.tf や variables.tf などなどがあるが省略
docpedia
├── .terraform-version
├── prod
│   ├── infra
│   │   ├── main.tf -> ../../shared/infra/main.tf
│   │   └── output.tf -> ../../shared/infra/output.tf
│   └── app
│       └── main.tf -> ../../shared/app/main.tf
├── qa1
│   ├── infra
│   │   ├── main.tf -> ../../shared/infra/main.tf
│   │   └── output.tf -> ../../shared/infra/output.tf
│   └── app
│       └── main.tf -> ../../shared/app/main.tf
├── qa2
│   ├── infra
│   │   ├── main.tf -> ../../shared/infra/main.tf
│   │   └── output.tf -> ../../shared/infra/output.tf
│   └── app
│       └── main.tf -> ../../shared/app/main.tf
├── module
│   ├── ecs
│   ├── db
│   └── alb
└── shared
    ├── infra
    │   ├── db.tf // module ディレクトリ配下を terraform module 機能で参照
    │   ├── alb.tf // module ディレクトリ配下を terraform module 機能で参照
    │   └── output.tf
    └── app
        └── ecs.tf // module ディレクトリ配下を terraform module 機能で参照

いくつか作りについてコメントします。

  • infra は ALB とか route53 の設定など、 app は ECS クラスタなどを配置しています。infra 側を先に適用する想定で、 appinfraoutput.tf を読みます。
  • module は比較的細かいリソースの単位(ALB とか、cloudfront とか、そういう単位)で切られています。
  • 上記リソース単位の module を shared/infra/db.tf などで参照し、各環境にはこのファイルへのシンボリックリンクを配置します。

これを直した結果、こんな感じになりました。

// 実際には backend.tf や variables.tf などなどがあるが省略
docpedia-yokotenkai
├── .terraform-version
├── common-resource // 複数の QA 環境で共有したいリソースをここに書く
│   ├── prod
│   │   ├── main.tf -> ../shared/main.tf
│   │   └── output.tf -> ../shared/output.tf
│   ├── qa
│   │   ├── main.tf -> ../shared/main.tf
│   │   └── output.tf -> ../shared/output.tf
│   └── shared
│       ├── main.tf // 欲しいリソースをほぼ全部ベタ書き
│       └── output.tf
└── individual-resource
    ├── prod
    │   └── main.tf -> ../shared/main.tf
    ├── qa1
    │   └── main.tf -> ../shared/main.tf
    ├── qa2
    │   └── main.tf -> ../shared/main.tf
    └── shared
        └── main.tf // 欲しいリソースをほぼ全部ベタ書き

自分で書いたコードながらかなりドラスティックな変わり方をしたように見えます*3。具体的に変わった点を挙げると、

  • common-resource は「2つあるQA環境で共有したいリソース」に関する記述をします。例えば DB です*4individual-resource はそれ以外です。common-resource 側を先に適用する想定で、 individual-resourcecommon-resourceoutput.tf を読みます。
  • module ディレクトリはありません。このディレクトリ内のリソースを module で参照することもしません。
  • 欲しいインフラリソースは全部 shared/main.tf にベタ書きして、各環境にはこのファイルへのシンボリックリンクを配置します。

それぞれの変更について、意図したことがらとか、考えていることを述べていくことにします。

terraform project の分割

2つの terraform project が同居しているという作りは同じですが、その詳細は変わっています。

terraform にできることは terraform にやらせよう

infra は ALB とか route53 の設定など、 app は ECS クラスタなどを配置しています。infra 側を先に適用する想定で、 appinfraoutput.tf を読みます。

という以前の作りの大まかな意図は、「infra 側のリソースは事前に作っておいて、 app 側は後で作る」というものになります。ところで、この意図は「リソースの依存関係を指定する」と言い換えることができますが、これは ただ terraform のコードを書くだけで terraform がやってくれること のはずです。言い換えれば、これは terraform project を分割しなくてもできるのですから、他に意図がないなら project を分割しないほうがシンプルでよいです*5

更に、terraform に任せればできることを自前実装している形になる手前、「このリソースは infra に置くか? app に置くか?」ということを迷う余地が生まれます。project の分割がなければ迷わずに済んでいると考えると、これは嬉しくないタイプの自由度と言えそうです。

terraform にできないことを私たちに

他方、「一部リソースは QA 環境で共有したい」という要求は terraform で自動的にケアしてくれるわけではない ものだと判断しています。これに対するアプローチとしては、 terraform project を分割するのがひとつの方法でしょう。恐らく他にも実現方法はあるかもしれませんが、とりあえずこの方法が一番楽そうだったので採用しています。これらの検討点を取り入れた結果、

common-resource は「2つあるQA環境で共有したいリソース」に関する記述をします。例えば DB です。 individual-resource はそれ以外です。common-resource 側を先に適用する想定で、 individual-resourcecommon-resourceoutput.tf を読みます。

という構成を取りました。

module の使いどころ

module の使い方も大きく変えました。

resource の wrapper にしない

module は比較的細かいリソースの単位(ALB とか、cloudfront とか、そういう単位)で切られています。

という使い方は、私がはじめて terraform を書いていた頃によく見かけたものです。具体的には以下のようなものです。

### ECS サービス
module "main_app" {
  source = "../../modules/ecs"  // このディレクトリ内に aws_ecs_service のリソース宣言がある

  task_role_arn      = module.ecs_iam.task_role_arn
  execution_role_arn = module.ecs_iam.execution_role_arn
  // その他いろいろな変数
}

### ECS task role などの IAM ロールの宣言
module "ecs_iam" {
  source = "../../modules/iam" // このディレクトリ内に aws_iam_role のリソース宣言がある
}
// modules/ecs の中身。実際にはこれ以外の変数もたくさん書く必要あり
resource "aws_ecs_service" "default" {
  task_definition = aws_ecs_task_definition.default.arn
}

resource "aws_ecs_task_definition" "default" {
  execution_role_arn = var.execution-role.arn
  task_role_arn      = var.aws_iam_role.task-role.arn
}

「まあそういうものだろう」と思って書いていたのですが、これは variable の受け渡しが増えるというデメリットがあります。事実、「この ECS サービスの task_role って実態は結局何なの?」というのを追いかけるには、

  • module/ecs 内の variables.tf での宣言を確認し、
  • その module が利用されている場所を確認して代入されている変数を確認し、
  • それが ecs_iam という別 module から来ているので、その module のファイルまで飛んで中身を確認する

という手間が必要です。コードを書く人にとっても、レビューをする人にとってもつらいですし、事故の元でもあります。更には初見で構造を把握するのが困難です。仕事は安全でつらくなく、簡単にキャッチアップできるほうがよいです。

module をやめて、欲しいリソースをベタ書きすると、以下のような記述になります。どのリソースを指定しているのか直ちに分かるというメリットがありますし、地味に IntelliJ IDE の補完が効くようになったのもよかったです。

resource "aws_ecs_service" "default" {
  task_definition = aws_ecs_task_definition.default.arn
}

resource "aws_ecs_task_definition" "default" {
  task_role_arn      = aws_iam_role.task-role.arn
  execution_role_arn = aws_iam_role.task-exec-role.arn
}

resource "aws_iam_role" "task-exec-role" {
  name               = "${local.project_name_env}-ecs-task-exec-role"
  assume_role_policy = data.aws_iam_policy_document.ecs-role-policy.json
}

resource "aws_iam_role" "task-role" {
  name               = "${local.project_name_env}-ecs-task-role"
  assume_role_policy = data.aws_iam_policy_document.ecs-role-policy.json
}

この方針でザクザクと module を消してみたところ、

module ディレクトリはありません。

という結果になりました。

テンプレートとして使う

ここまで module の利用を減らす話ばかりしてきましたが、「module は使いません」とは述べて いない ことに注意してください。事実、今回のインフラ構築では何箇所か module を利用しています。例えば security group の構築は aws terraform provider が提供している module を利用していますし、私たちのチーム内で共通して使われる機能はいくつか module 化して別ディレクトリに配置されています。また、今回の構築では利用しなかったのですが、弊社では「よくあるインフラ構築テンプレ」を以下のような OSS の形で公開しています*6

github.com

このような、

  • 複数アプリで全く同じ構成の部品を必要とする
  • その部品の独立性が高い

状況では terraform module がよく機能する印象を受けます。インフラ構築の cookiecutter*7 として使うとうまくいく、と言っても良いかもしれません。

このディレクトリ内のリソースを module で参照することもしません。

という設計は、「module はこのディレクトリで話が完結する程度にローカルな使われ方しかしない場合には利用せず、外部ディレクトリや OSS を参照するための機能と割り切ったほうが良さそう」という現状の考えを反映してのことです。

まとまった tf ファイル

上記リソース単位の module を shared/infra/db.tf などで参照し、各環境にはこのファイルへのシンボリックリンクを配置します。

という以前の書き方は、module という単位ありきのファイル単位でした。module の利用をやめたということは、.tf ファイルに全てのリソースをベタ書きする形になります。

今回の構築では

欲しいインフラリソースは全部 shared/main.tf にベタ書きして、各環境にはこのファイルへのシンボリックリンクを配置します。

という形にしています。ファイルを分割する事も考えたのですが、「人によってファイル分割のセンスが違いすぎるせいで混乱するから半端にやらないほうがよい、やるならばリソース単位で分割するくらい徹底したほうがベター」という意見を受け、一旦見送っています*8

とはいえこのため、現状で main.tf は1000行を超えています。大きいなぁ、とも思うのでいくつか工夫をしています。例えばコメント欄に何に関するリソースなのかを明示したり、関連の深いリソース群はできるだけ main.tf ファイルの中で近い場所に書いたり、という感じです。すごく地味なのは認めます。よりよい運用上の仕組みがあるかもしれませんし、もしかすると実は module の適切な利用で解消される部分もあるのかもしれませんが、とりあえずないよりは良くなるだろうと思っています。

まとめ

既存サービスの横展開のためのインフラ構築をするにあたって、 terraform の書き方をいろいろ変えました。「どのような考え方に則ってこのような書き方をしたのか」という、コードに残しにくいことがらにフォーカスして本稿を書いてみました。

We are hiring!

私たちのチームでは docpedia の横展開だけでなく、その他様々な新規企画が立ち上がっています。それらの企画の実現に向けて開発・運用を担うエンジニアを募集中です。お気軽にお問い合わせください。

jobs.m3.com

*1:雑多な思い出という言葉を選んだ事情は次のようなものです。弊社福林の記事 https://www.m3tech.blog/entry/2020/07/27/150000 にもあるように、terraform の適切な使い方は状況に依るところ大でしょう。そのような技術に対して、本稿は「ベストプラクティス」みたいな大上段なものを提供するつもりはなく、「こういう状況だとこういう感じでした」という具体的なケースを示したいという気持ちがあります。

*2:これが絶対に正しいとか言うつもりはまったくなく、実験の側面も大きいです。今回このような「実験」に踏み切れたのは、既存サービスの横展開ということで、完全新規要素が少なめで済んだという点が大きいでしょう。新規技術を多数導入するような開発であったならば、おそらくそれらの技術のキャッチアップに時間を割くので、今回のような改善の模索までは手は回りません。

*3:とはいえ、これらの変化は決して思いつきで起こそうと思ったわけではありませんし、何より私自身が変化前のコードを書いていた一人です。やや突っ込んだ社内事情を述べると、横展開前の docpedia は AWS 上のプロダクトとしては古参なほうで、terraform も手探りで書かれたものでした。その後、後発組のプロダクトや大量のクラウド移行、そして日々のチーム SRE としての業務を経て terraform の知見が溜まってきました。今回の構成変更はそのような知見を反映した結果です。「天才的な発想で頭の良いことをやってやろう」と思ったわけではなく、色々な現場を経て得られた経験を地道に還元しようとしたら自ずとこうなった、というのが正直なところです。

*4:2系統それぞれに DB を立てる構成も考えられるのですが、現状の docpedia の開発スタイルでは DB を共有しておいたほうが何かと都合が良いことが多く、こうなっています。

*5:この構成になった理由について確かなことはわかりませんが、あり得たかもしれない筋書きとしては次のようなものがあるでしょう;クラウド利用の黎明期においては、各アプリケーション・チームごとに独自で VPC などを立てる terraform を書いていました。この運用においては、VPC を作る infra project とアプリを構築する app project は分けたほうが良いでしょう(極端な話、「アプリが要らなくなったので terraform destroy したら VPC まで消そうとしてしまう」というのは何かがおかしい気がしますし、通常は1つの VPC に複数アプリを載せるはずです)。しかし、クラウド利用・移行が進むにつれて、VPC の構築はコア SRE チームの主導する全社共通基盤のいち機能として提供されるようになったので、アプリチーム側の terraform に VPC などが現れることはなくなり、意識すべきレイヤーはアプリ寄りのものに限られるようになりました。このような事情の変化をディレクトリ構成に取り込みきれていなかった、というのはあるのかもしれません。

*6:今回このテンプレを使わなかったのは、私が単にうっかり忘れていただけです…

*7:もともとは python 製の cli project template に付いている名前です https://github.com/cookiecutter/cookiecutter が、よい名前だと思うので、名称を濫用して一般名詞化してほしいという気持ちがあります。

*8:今考えると、何でもとりあえず module にしてしまうのは「人によって module 分割のセンスが違いすぎるせいで混乱する」という問題を有していた可能性もあるかもしれない、と思っています。module の利用をやめると、この問題の「module 分割」が「ファイル分割」へと置き換わって現れる、という捉え方もできるのではないか、というのが現在の見立てです。