aws terraform

Terraform Moduleとディレクトリ管理で開発・本番環境を分ける

TerraformのコーディングをしていくとDRY(Don’t Repeat Your Self:重複させない)原則を保ちつつ開発、本番環境の違いを、どう吸収するかの壁にぶち当たると思います。

更にコーディングする中での別の問題として、以前の記事のように単純に1つのディレクトリのみでコード量が増えていくと構成を把握しづらくなってしまうという点があります。

これらの対応策としてTerrafomにはModuleという概念があります。

また開発、本番環境が相互に影響を与えないよう分離する(≒それぞれ別々の状態(tfstate)を管理する)手法として、 ディレクトリ管理 ワークスペース管理(ここではTerraformCLIのワークスペースのこと。Terraform Cloudのワークスペースとは別の概念) の2つが提示されています。

今回はModuleを使用しつつディレクトリで環境を分けて、Terraformの構成を整理していきたいと思います。(整理後のソースコードはこちらのGitHubに置きました。)

スポンサーリンク

前提説明

ディレクトリ管理 と ワークスペース管理 どちらが良いか

公式ドキュメントで開発、本番環境を分離する手法としてディレクトリ管理とワークスペース管理が提示されていますが、以下の点から自分はディレクトリ管理を採用しました。

  • ドキュメントでワークスペースは、そもそも異なる開発段階(ステージングと本番など)の分離に適したものではないと言及されている。
  • バイブル 実践Terraform AWSにおけるシステム設計とベストプラクティス やググる限りワークスペース構成は、あまり利用されてなさそう。
  • こちらの方が言及されておりましたが、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+ディレクトリ管理化していきたいと思います。
整理後のソースコードはこちらの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ディレクトリ

例としてenvironments 配下に3環境準備します。
main.tf、varables.tf を環境に合わせて修正していく形です。

各環境のディレクトリ配下でterraformを実行するので(デフォルトのローカル管理であると)状態ファイルのterraform.tfstate がそれぞれのディレクトリ配下に作成されます。=環境ごとに分離できます。

なおvariables.tfに関して、秘匿情報などはdefault定義せずに、tfvars に切り出してください。
※要 .gitignore でtfvarsを除外

またmoduleに渡す値は、直接main.tfのmoduleブロック内に定義しても良いです。
自分は編集箇所をわかりやすくするためvariablesに切り出してますがmodule自体の変数宣言と2重定義しているのは否めない。。

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など指定できます。

更にインプットとして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やdepends_onなど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 のコードを黒魔術にしないために心がけたこと ~ 自分への戒めを込めて ~

関連書籍