TerraformのコーディングをしていくとDRY(Don’t Repeat Your Self:重複させない)原則を保ちつつ開発、本番環境の違いを、どう吸収するかの壁にぶち当たると思います。
更にコーディングする中での別の問題として、以前の記事のように単純に1つのディレクトリのみでコード量が増えていくと構成を把握しづらくなってしまうという点があります。
これらの対応策としてTerrafomにはModuleという概念があります。
また開発、本番環境が相互に影響を与えないよう分離する(それぞれ別々の状態(tfstate)を管理する)手法として
- ディレクトリ管理
- ワークスペース管理(ここではTerraformCLIのワークスペースのこと。Terraform Cloudのワークスペースとは別の概念)
の2つが提示されています。
今回はModuleを使用しつつディレクトリで環境を分けて、Terraformの構成を整理していきたいと思います。
前提
ディレクトリ管理 と ワークスペース管理 どちらが良いか
公式ドキュメントで開発、本番環境を分離する手法としてディレクトリ管理とワークスペース管理が提示されていますが、以下の点から僕はディレクトリ管理を採用しました。
- ドキュメントでワークスペースは、そもそも異なる開発段階(ステージングと本番など)の分離に適したものではないと言及されている。
- バイブル 実践Terraform AWSにおけるシステム設計とベストプラクティス やググる限りワークスペース構成は、あまり利用されてなさそう。
- こちらの方が言及されておりましたが、if分岐などで差分を吸収するためのロジックを書きまくって、かえって可読性が悪くなりメンテが大変になるのは避けたい。(と思ったのですが、オレオレベストプラクティスに書いたとおり、今はif分岐で運用してます)
例えば開発にのみビルドサーバがあるとか、WAFでIP制限掛けているなどの環境差異は環境に応じたmoduleを利用しつつプラガブルにつけ外しできたら運用しやすいかと考えました。
moduleについて
moduleの利点
moduleの利点はこちらにまとめられています。以下な感じです。
- 関連機能を論理的にまとめて、全体をわかりやすく構造化する。
- 関連機能をカプセル化して、名前の重複を防いだり、バグなどのエラー範囲を限定する。
- カプセル化したものを他のチームや組織外に一般公開して再利用できるようにする。
分割の粒度
moduleを関連機能ごとに分割する粒度はプロジェクトの状況によるところはあると思いますが、ここで一応目安が示されています。
またTerraform同じくIaCをするためのAWSサービスのCloudFormationのベストプラクティスでも「ライフサイクルと所有権によるスタックの整理」として目安が示されています。
これらを参考に以下の観点から分類すると運用しやすいかと思います。
ボラティリティ(揮発性)、ライフサイクル
例えばVPCやsubnetは一回設定したらそうそう変更は発生しませんが、その上で稼働するアプリケーション関連(ECS、EC2やLB)の追加・削除などの頻度は、比較的に高くなります。
責任範囲、影響範囲
AWSインフラを複数のチームで運用している場合(例えばVPC、SubnetはNW管理チーム、DBはDB管理チーム、CodeBuildやECSはアプリ開発チームでと)、ミスした場合の影響を自分達の責任の範囲に留められます。
またサービスへの影響範囲の視点から分割すると、ロードバランサやDBなど実サービスに直結するものはミスすると影響が大きいですが、ECRやCodeBuildなど裏方のDevOpsな部分はミスしてもエンドユーザーには関係ありません。
Moduleとディレクトリ管理で実行環境の分離
それでは以前の記事のソースコードをもとに、module+ディレクトリ管理化していきたいと思います。
整理後のソースコードはこちらのGithubに置きました。
最終的に以下の構成ができます。
ディレクトリ構成
% tree .
.
├── environments
│ ├── dev ・・・・・・ 開発環境ディレクトリ。この配下でterraform を実行
│ │ ├── main.tf ・・・・・・ 開発環境用の profile や 呼び出すmodule を設定
│ │ └── variables.tf ・・・・・・ 開発環境用の値を設定。t2.microとか
│ ├── prod ・・・・・・ 本番環境
│ │ ├── main.tf
│ │ └── variables.tf
│ └── stg ・・・・・・ ステージング環境
│ ├── main.tf
│ └── variables.tf
└── modules ・・・・・・モジュール本体。機能ごとにそれぞれ分けたもの
├── hello-app-server
│ ├── main.tf ・・・・・・主処理を記載
│ ├── outputs.tf ・・・・・・ルートや他のmoduleで、このmoduleの出力値を使用する場合、定義
│ └── variables.tf ・・・・・・このモジュールのインプットとなる値を定義
├── network
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── security
├── main.tf
├── outputs.tf
└── variables.tf
%
environmentsディレクトリ
3環境分離
例としてenvironments 配下に3環境準備します。
main.tf、varables.tf を環境に合わせて修正していく形です。
各環境のディレクトリ配下でterraformを実行するので、(デフォルトのローカル管理であると)状態ファイルのterraform.tfstate がそれぞれのディレクトリ配下に作成されます。=環境ごとに分離できます。
環境により変動する値はvariables.tfで定義
moduleに渡す値は、直接main.tfのmoduleブロック内に定義しても良いですが、僕は編集箇所をわかりやすくするためvariablesに切り出してます。
機密情報はtfvarsに書いてコミットしない
variables.tfに関して、機密情報などはdefault定義せずに、tfvars に切り出してください。
GitHubへアップロードしないように忘れずに.gitignore でtfvarsを除外してください。
GitHub公式リポジトリに参考となるgitignoreが提供されています。
modulesディレクトリ
moduleは前述の分割の考え方を参考に3つに分けてみます。
network
vpc,subnetなど文字通りネットワーク系を定義します。
hello-app-server
ec2 や albなどのリソースを定義します。
ec2,ecsやtargetgroup ,albはセットで運用することが多いため、この単位でまとめます。
security
securitygroupを定義します。
リソースをまたがって共通で使用することが多いため、切り出します。
moduleの書き方
module呼び出し側
本構成では、[dev|stg|prod]/main.tf からmoduleを呼び出します。この呼び出す元(terraform を実行する場所) をルートモジュールと呼びます。
source で呼び出しファイルから相対パスでmodule本体があるディレクトリを指定します。source にはローカルパス以外に、Terraform公式のRegistryやGithubなど指定できます。
またvariables.tfの値をインプットとしてmoduleに引数を渡せます。
## module "識別名"
module "hogehoge" {
source = "../../modules/network"
## module に my-ip という名前で引数を渡します
my-ip = var.my-ip
## 他のmodule のアウトプットをこの module のインプットにできます
## module.モジュール名.output名
vpc-id = module.network.vpc-id
## 値を直接渡せます
oreore = "fugafuga1234"
}
これ以外にもmoduleブロックには、countやfor_eachなどのMeta-argumentsをつかって柔軟に記述できます。
module構成
module本体は基本的に3つのファイルからなります。ファイル名は以下に示す名前が推奨されています。
main.tf
主処理を記載します。module特有の記載方法は特になく普通にresourceブロックなど定義していきます。
variables.tf
モジュールのインプットとなる変数をvariableブロックで定義します。
ルートモジュールからの必須の引数としたい場合は、default定義を記載しないでください。
逆にオプション引数の場合は、default定義を記載します。
outputs.tf
モジュールのアウトプットとなる変数をoutputブロックで定義します。関数の戻り値のようなものです。
このアウトプットをルートモジュールや他のmoduleで引き回して使用します。
module分割の注意点
moduleをわけることで前述のメリットがあるのですが、粒度を細かくしすぎるとmodule同士をつなぎ込むために
- 出力側でのoutput
- 入力側でのvariable
- その間のルートモジュールで値の受け渡し
の3箇所の設定がどんどん増えてきて逆にソースが追いづらくなってくるため、分割の大きさには気をつけたほうがいいかもしれません。
補足
Terraform実行の前にEC2に接続するためのssh鍵を準備してください。
% cd environments/dev
% ssh-keygen -N "" -f test-key
Generating public/private rsa key pair.
...
% ls test-key*
test-key test-key.pub
%
今回は以上ですーノシ
参考
アリガト━━━ヾ(´∀`)ノ━━━━♪
公式ドキュメント Module Blocks
公式ドキュメント Reuse Configuration with Modules
Terraformのベストなプラクティスってなんだろうか
Terraform 0.12 のコードを黒魔術にしないために心がけたこと ~ 自分への戒めを込めて ~