こんにちは! ジモティーにてインフラ開発・運用を担当している斎藤です。
ジモティーでは主に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
でさらにインフラを定義する
- 依存する別モジュールを宣言する。例えばサーバをデプロイするためにnetwork名が必要になるため、
- 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
モジュールなどがこの環境でインスタンス化されます。
構成自体は他の環境と全く同様です。
最終的なデプロイ方法
結果として、以下のように modules
と environments
が宣言され、Google Cloudリソースが生成されることになります。
実際に terraform apply
を実行するのはデプロイしたい環境のシステム単位ごとに行うようにします。
まとめ
この記事ではTerraformを使ってプロジェクトを構成する際に弊社で考えたことを書きました。
以下が肝だったのではないかと考えていますが、この考え方はデプロイ先がGoogle Cloudに限らず、適用できる考え方かと思います。
これからTerraformプロジェクトを立ち上げようとする方の参考になれば幸いです。
- モジュールには環境固有の設定値を入れないことで、環境の複製を容易に、かつ環境差異が生じにくいようにした
- モジュールの入力(
variable
) と出力(output
) を意識しモジュールに対するインタフェースと捉えることで依存関係を明確にし、かつ一方向にのみ依存するように整理した
今回の共有は以上です。今後もテックブログを定期的に更新し、ジモティーの開発事例やプラクティスを共有していきたいと思います。
次回もまたお楽しみに!
弊社では一緒にプロダクトを改善していただける仲間を探しています!
こちらでお気軽にお声がけください!