Terraformプロジェクトの構造を考える

こんにちは! ジモティーにてインフラ開発・運用を担当している斎藤です。

ジモティーでは主にAWSを活用してシステムを作っているのですが、分析基盤としてBigQueryを利用するなど、一部の用途でGoogle Cloudも活用してきました。

今回、BigQueryにリアルタイムでログを蓄積していく必要があり、APIサーバなどのコンピューティングリソースもGoogle Cloudに構築する必要性が出てきました。

Google Cloudでインフラを管理するなら将来的な拡張も見据えてやはりTerraformか、、、? という考えに至り、実際にTerraformプロジェクトを1から立ち上げてみましたので、そのときに考えたことを共有したいと思います。

Terraformってなに?

パブリッククラウドのリソース(サーバ、ネットワーク、ストレージ等々)をコードで管理するためのIaCツールです。

Hashicorp社製で、HCLという独自言語によってインフラを宣言することができます。

例えば以下のような記述をすると、Google Cloud上にCloud Storageバケットをひとつ定義できます。

resource "google_storage_bucket" "static_contents_bucket" {
  name                        = "jmty-static-contents"
  location                    = "ap-northeast1"
  uniform_bucket_level_access = true
}

IaCツールにおいてはAWSだとCloud Formationが有名ですね。

最近はCloud Formationをより抽象化してより開発者がコーディングに集中できるようなAWS CDK(Cloud Development Kit)というものもあります。

TerraformがAWSのそれらと違うのは、プロバイダというライブラリを入手することで多種多様なインフラをコード化対象にできる点です。

AWS、Azure、Google Cloudなどの主要パブリッククラウドだけでなく、GitHubやHerokuなどのSaaS、PaaSに対してもプロバイダが提供されています。

提供されているプロバイダはこちらから一覧することが可能です。

プロジェクト構造

さて、Terraformでインフラを管理することを決めたのはいいのですが、まず何をすればいいのか。。。

Terraformプロジェクトの構成の仕方は基本的に自由です。拡張子を .tf にさえしておけば、どこでどんなリソースを定義することもできます。

ただしプラクティスは既に無数に存在します。

なんならGoogle自身がTerraformでインフラを管理する際のプラクティスも提供してくれています。

重要な点は一つのディレクトリが「モジュール」として扱われる点です。

基本的にデプロイはモジュール単位で行われ、モジュールの中で別のモジュールを呼び出すこともできます。

このような特徴から、Terraformプロジェクトでは以下のようなプロジェクト構造を採用しました。

結果、プロジェクトは大きくわけて modules ディレクトリと environments ディレクトリを持つ形に落ち着きました。

.
├── README.md
├── environments → ★各環境ごとに設定値を格納したファイル群
│   ├── global → ●環境間共有リソース
│   │   ├── common
|   |   ...
│   ├── prod → ●本番環境
│   │   ├── api_server
|   |   ...
│   └── stg → ●検証環境
│       ├── api_server
|       ...
└── modules → ★各システムごとのリソース定義ファイル群(モジュール)
    ├── api_server
    │   ├── enable_apis.tf
    |   ...
   ...

以降で modules ディレクトリと environments ディレクトリについて説明します。

modulesディレクトリ

modules ディレクトリには各環境を構成するシステム単位毎に、Google Cloudリソース群がモジュールとして定義されています。

例えば api_server モジュールにはAPIサーバとそれが動くために必要なインフラ、 management_console モジュールには管理画面のインフラを定義するためのリソース群が定義されています。

木構造で表すと、以下のような構造になっています。

modules/
├── api_server
│   ├── enable_apis.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── provider.tf
│   └── variables.tf
├── management_console
│   ├── enable_apis.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── provider.tf
│   └── variables.tf
...
└── networking
    ├── enable_apis.tf
    ├── main.tf
    ├── outputs.tf
    ├── provider.tf
    └── variables.tf

各モジュールは以下のようなファイル群で構成するようにしました。(各モジュールによっては、利用しないファイルも存在します)

  • enable_apis.tf
    • モジュールの実行に必要なGoogle CloudのAPIの有効化を行う
  • main.tf
    • モジュールの実行によって作成したいGoogle Cloudリソースを定義する
  • outputs.tf
    • モジュールの出力を定義する。モジュール実行の戻り値(出力インタフェース)として機能する
  • provider.tf
    • モジュールで利用するTerraformプロバイダを宣言する
  • variables.tf
    • モジュールで利用する変数を定義する。モジュール実行の引数(入力インタフェース)として機能する

environmentsディレクトリ

modules ディレクトリでは環境固有の設定値を引数にしたモジュールを定義していました。environments ディレクトリでは modules で定義したモジュールに対して、各環境の設定値を入力として与えモジュールをインスタンス化します。

結果としてGoogle Cloudにリソースをデプロイすることができます。

stg 環境、つまり検証環境を例にとると以下のような構成になっており、各モジュール名のディレクトリで実際にモジュールがインスタンス化されるようになっています。

environments/stg/
├── api_server
│   ├── backend.tf
│   ├── data.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── terraform.tfvars
│   └── variables.tf
├── common
│   ├── backend.tf
│   ├── outputs.tf
│   ├── terraform.tfvars
│   └── variables.tf
├── management_console
│   ├── backend.tf
│   ├── data.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── terraform.tfvars
│   └── variables.tf
...
└── networking
    ├── backend.tf
    ├── data.tf
    ├── main.tf
    └── outputs.tf

各モジュールは以下のような構成を取るようにしました。

  • backend.tf
    • Terraformステートを格納するTerraformバックエンドを宣言する
  • data.tf
    • 依存する別モジュールを宣言する。例えばサーバをデプロイするためにnetwork名が必要になるため、 networking モジュールに依存するなど。ここで定義したモジュールの出力を使って main.tf でさらにインフラを定義する
  • main.tf
    • モジュールをインスタンス化する宣言をする
  • outputs.tf
    • モジュールの出力を all というキーに全て詰め込む宣言をする。これにより外部モジュール側の data.tf で利用宣言すると、もれなくこのモジュールの出力を利用させることができる
  • terraform.tfvars
    • variables.tf に実際に渡す値を書く
  • variables.tf
    • モジュールで利用されている変数を定義しておく
      • モジュール側と環境側で二重管理になってしまっているが、モジュール側の variables.tf はあくまで環境に依存せず必須にしたい変数を必須宣言しておき、環境によっては任意にしたい変数にはデフォルト値を付与して任意宣言としてある。これにより、モジュール側は必須パラメータだけを最低限定義すればよく、環境によるチューニングは任意パラメータで実施することが可能になる

commonモジュール

各環境には modules ディレクトリにはないモジュールである common モジュールが定義されています。

これは特殊なモジュールで main.tf を持たないモジュールです。

つまり common モジュールをインスタンス化することによってGoogle Cloud上にリソースがデプロイされることはなく、Terraformステートだけがデプロイされることになります。

commonモジュールの役割は、各システム間で値を共有することです。

commonモジュールの出力にあらかじめ環境内の各システム間で共有したい値(環境名など)をあたえておきます。

これにより各モジュールは common モジュールに依存すればそれらにアクセスすることができるようになり、先の役割を全うするようになっています。

global環境

environments ディレクトリには global 環境が定義されています。

これは各環境に直接属さない、環境間共有のリソースを定義する環境として利用されます。

例えばIAMを管理する project_members モジュールなどがこの環境でインスタンス化されます。

構成自体は他の環境と全く同様です。

最終的なデプロイ方法

結果として、以下のように modulesenvironments が宣言され、Google Cloudリソースが生成されることになります。

実際に terraform apply を実行するのはデプロイしたい環境のシステム単位ごとに行うようにします。

まとめ

この記事ではTerraformを使ってプロジェクトを構成する際に弊社で考えたことを書きました。

以下が肝だったのではないかと考えていますが、この考え方はデプロイ先がGoogle Cloudに限らず、適用できる考え方かと思います。

これからTerraformプロジェクトを立ち上げようとする方の参考になれば幸いです。

  • モジュールには環境固有の設定値を入れないことで、環境の複製を容易に、かつ環境差異が生じにくいようにした
  • モジュールの入力(variable) と出力(output) を意識しモジュールに対するインタフェースと捉えることで依存関係を明確にし、かつ一方向にのみ依存するように整理した

今回の共有は以上です。今後もテックブログを定期的に更新し、ジモティーの開発事例やプラクティスを共有していきたいと思います。

次回もまたお楽しみに!


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです