Serverless×Terraformモジュール設計のベストプラクティスの検討~IoTデータ収集基盤の例~

2020.12.23

はじめに

CX事業本部の佐藤智樹です。

今回はTerraformでServerless構成を含んだモジュール設計をする際に検討した内容を記載します。表題はモジュール設計としていますが、modules配下の話だけでなくディレクトリやstate全体を通したモジュール設計について紹介しております。

基本的には公式ドキュメントに乗っ取って、不足している部分を独自に補っています。またモジュール設計の紹介後に、stateの管理方法や命名規則などのTIPSも記載しております。

この記事はTerraformでServerlessな設計を含むシステムや例としてあげるIoTデータ収集基盤などの構築を検討している方へ参考になるかと思いますのでご一読ください。

また記事へのフィードバックも募集しておりますので、社内ならSlack、社外ならはてぶコメントやTwitterなどでご意見をいただけると助かります。

構築するシステム例

本記事では、IoTデータの収集/可視化システムの構築を例とします。具体的には以下のイメージです。

IoTデバイス側から上がってきたデータを、最終的にAuroraに格納します。今後AuroraのデータをECS on Fargate や AppSyncを使って可視化したり、分析に使用することを予定しています。

上記のシステムをTerraformのモジュール設計に落とし込むならという内容で話を進めていきます。また上記の設計に合わせてロールの管理や監視ツールの管理もTerraform上で行う想定で記載します。

公式の推奨パターン

Terraformでは、公式にモジュール設計の推奨パターンが公開されています。本章ではまずこちらについてご紹介します。

まず上記のドキュメントに一通り目を通して欲しいのですが、今回は筆者の方で内容をピックアップして紹介します。

モジュール作成のワークフロー

モジュール設計では2つのステップでモジュールの構成を検討します。

  1. 要件に対して適切なスコープのモジュールを検討
  2. モジュールをMVPとして作成

要件に対して適切なスコープのモジュールを検討

モジュールは最小の構成で簡単に開始できるような設計が必要です。また以下の3つの原則が重要になります。(かなり意訳しているので、違和感ある場合は原文を読んでください)

カプセル化

モジュールに複数のインフラ構成をまとめると展開は便利です。しかしモジュール全体の目的や要件は理解しにくくなります。

1つのモジュールに全ての構成を入れるような過度なモジュール化は良くない。かといって全てのリソース構成をモジュールごとに分割するのも良くないので適切に設定してくださいという内容かと思います。

特権

モジュール内のリソースを複数グループで使用している場合、1つのグループでのリソース修正が別のグループのリソースに影響する可能性があります。リソース修正がグループ内のみの影響となるようにモジュールを設計してください。

モジュールという独立した単位に分割することで、いくつかのグループでモジュールを使い回すことができます。反面使い回すことで、本来分離すべき箇所が密結合になってしまう可能性があるので、注意して設計することが必要です。

ボラティリティ

データベースのリソースは比較的変更頻度は少ないですが、アプリケーションのリソースは1日に複数回デプロイするなど変更頻度が高いです。上記の変更頻度が異なるリソースを同じstateで管理すると不要なタイミングで誤った内容がリリースされるリスクがあります。

インフラやアプリだけでなく、変更頻度が同じものでモジュールを設計するのが重要かと思います。

モジュールをMVPとして作成

モジュールはコードと同じように完全ではありません。最初は以下を満たすことを考えます。

  • ユースケースの80%での機能する
  • エッジケースを作り込まない
  • MVPでは条件式の使用は避ける
  • 最も一般的な引数のみを変数として公開する
  • 最初は最も必要になる可能性の高い変数のみサポートする

全体的に、過度なエッジケースの作り込み防止。変数による入力はAWSならリージョン名や環境名(itg,stg,prdなど)など必要最低限で作ることなどが言われています。

後、モジュールMVPからはできるだけ多くの情報を外部に出しておくことで、結合する次のモジュールでの使用が楽になることが書かれています。

モジュール設計の例

上記の公式ドキュメントでは上記の設計方針の例として、インフラ層、Webアプリ層、アプリ層で構築する場合の設計について紹介しています。

アプリケーションはVPC上に配置されて、従来型の3層構造で構築される前提です。

大まかには以下の分類になるということが記載されています。

項目名 詳細
ネットワークモジュール VPC、NCL、NGWなどを構成
アプリモジュール EC2、ELB、Auto Scaling Groupなど
Webモジュール EC2、ELB、Auto Scaling Groupなど
DBモジュール RDSなど
ルーティングモジュール Route53など
セキュリティモジュール IAM ユーザ、IAM Roleなど

以下公式ドキュメントから画像を引用します。モジュールごとの詳細については公式をご確認ください。

引用元: Module Creation - Recommended Pattern | Terraform - HashiCorp Learn

変更頻度や役割に応じてモジュールを分けることが明文化されています。 基本的には上記の方針に乗っ取って開発することで、リスクが少なく、スムーズに開発を進められると思います。

問題点

従来通りのWebアプリケーションの設計なら上記の設計で、後はモジュール内をVPC、RDSなどの単位で分割しても問題ないと思います。

しかし、Serverlessアプリケーション開発の場合はサービス間の結合が分かりづらくなる問題があるかと考えます。例えば以下のようにIAM、IoT Core、Lambda、S3をそれぞれ別のモジュールに分割して構築するとします。

上記の構築の場合、初期は問題ないのですが後で機能単位で処理をソースから確認したい場合に問題がでます。

例えばLambdaやIAM Roleなどの実態は以下の画像のようにそれぞれ別の機能で使用するとします。

本来は機能Aで確認したいソースは赤枠部分だけですが、AWSリソースごとにファイルが分かれていると機能BやCに関するソースも余分に読むことになります。上記は3機能程度なので問題ないですが、これが20~30と機能が増えていくことによってどんどん複雑さは増します。

またLambdaに関してはデプロイフローが整っているCDKやSAMを利用するのも一つの手です。

検討した解決策

上記を踏まえてアプリ部分は以下のモジュール構成にしました。以下の例はIoT Core(IoT Rule)でデータを受けてKinesis Firehose経由でS3に保存する機能です。

カプセル化の観点からAWSリソースと関連するロール/ポリシーは1つのモジュールとすることにしました。例えば上の図だとIoT Coreと関連するIAM Roleを紐づけています。

main.tfにイベント順でモジュール呼び出しを書けば、開発者は上から順番にソースを読むだけで処理を把握することができます。

iot-rule.tfとfirehose-to-s3.tf間では、リソース名やARNの受け渡しが必要なので、output.tfでリソースの内容を出力して受け渡します。上記の設計のようにモジュールをAWSリソース単位で切り出すのではなく、機能レベルで切り出すことで可読性が上がるのではと考えています。

またLambdaについてはCDKやSAMなど別のIaCツールの方がデプロイ周りの処理で優れているため別のツールを採用することとしました。

最終的な設計

上記を踏まえてアカウント設定や監視設定なども含めたモジュール構成を考えました。以下の例はSECURITY MODULEがログイン時のIAM Roleなどの管理。DATA COLLECTION MODULEが今回のシステムで作成するデータ収集機能。APPLICATION MODULEはDATA COLLECTIONなどのプロジェクトごとで共通で使用するリソースをまとめています。

将来的に監視システムの導入やデータ可視化基盤など新しいプロジェクトの構築、Terraform Cloud設定のコード化など行う場合は、画像下部のように新しくモジュールを作成してボラティリティを考慮した設計にしようと考えています。

またほぼ全ての設定をコードで管理することで、変更差分の用意な把握や引継ぎ時の情報共有などがスムーズに行えます。(もちろん細かいモジュールごとのReadmeの記載か大枠での情報を共有できる資料は必要です)

上記を踏まえてディレクトリの構成は以下のようにしました。プロジェクトや役割に応じてリポジトリを分散する方法もありますが、色々検討した結果モノレポで作成することとしました。理由については後述します。

terraform
├── aws_iam_users
│   ├── environments
│   │   ├── development
│   │   ├── staging
│   │   └── production
│   └── modules
│       └── iam-users
├── aws_data_collection
│   ├── environments
│   │   ├── development
│   │   ├── staging
│   │   └── production
│   └── modules
│       ├── iot-rule
│       └── kinesis-firehose-to-s3
└── aws_application
    ├── environments
    │   ├── development
    │   ├── staging
    │   └── production
    └── modules
        ├── vpc
        └── aurora

SECURITY MODULEは、aws_iam_usersのディレクトリにまとめて、APPLICATION MODULEはaws_applicationのディレクトリにまとめています。

DATA COLLECTION MODULEは、aws_data_collectionで作成しています。データ収集基盤のように新規でプロジェクトを作る際は、terraform配下に新しくディレクトリを切ってプロジェクトと環境単位でstateをまとめます。

機能が増えるとterafromディレクトリ直下のディレクトリごとにmodulesが増えて多少冗長になります。しかし、機能ディレクトリ内部でソースが完結するので可読性が上がり、変更による影響をディレクトリ内で完結させられる方が開発効率も向上し引継ぎなどもしやすいと考えています。

また、terraform直下のディレクトリ名にクラウドプロバイダーの名前を入れておきます。これでGCPやAzureなどをTerraformで併用する場合も、どのクラウドプロバイダーのための処理か分かりやすくなります。

モノレポを選択した理由

Terraformの公式ページでは、個別リポジトリでもモノレポでも大丈夫だが注釈で個別リポジトリを推奨しています。

これは予想ですが複数チームで並行して別のプロジェクトの開発を進めることを想定している面もあるのかと思います。大人数で分かれて開発を行う場合は、issueやコードが1つのリポジトリに集中することでissueが何に対しての話か分かりづらくなったり、コードの構成の概要が分からないとどのコードを見れば良いか分からなくなったりと混乱を招きます。

AWS CDKでもリポジトリを1つにしたことで、構成が複雑になりversion2で分割しようという話も出ていました。(CDK Dayで聞いた話です)大規模な開発になっていくたび上記のような問題は起きます。

ただ今回自分たちが開発を行うチームは小規模であるため、1つのリポジトリで簡単に全体象を確認できるような構成にしようと決定しました。単純に小規模チームで細かい処理が各リポジトリに分散していると関連が分かりづらいという理由もあります。

コンウェイの法則に従うように、大規模な業務システムやOSS開発で複数チームが動く場合は個別リポジトリ。小規模なチームでアプリからインフラまで全て見る場合はモノレポの方が相性が良いのではと考えています。こちらについても半年~1年ほどで、メリット・デメリットの振り返り記事を上げます。

モジュールディレクトリ内部の構成

モジュール内のファイル構成は以下に記載します。 ファイル構成はhashicorp社のリポジトリの例を参考にしています。

.
├── README.md
├── main.tf
├── outputs.tf
└── variables.tf
ファイル名 詳細
README.md モジュールに関する説明
main.tf モジュールの設定
outputs.tf モジュール内の設定を外部に変数として公開する用
variables.tf モジュール外部から受け渡される変数

モジュール外部からアクセスする変数や、外部に公開する設定を分かりやすく構成しています。

TIPS

前章でモジュール設計に関する検討の記述は終了です。ただ、Terraformでサービス全体を設計している際にstateの管理方法やCI/CD設定、命名規則などの検討もあったので、そちらについては本章で記載します。

tfstateの管理

tfstateはシステムの機能やセキュリティ設定、共通で使用するリソース周りでグルーピングしています。

環境ごとに1つのtfstateで管理すると、変更のたびに誤って修正した箇所をデプロイする危険性が残ります。planで変更を確認することもできますが、リソースが大量になると目視での確認は厳しいです。

なので1グループ単位で環境ごとに3つのtfstateを作ることにしました。

多くのstateを作って、複数のstateをデプロイしたい場合はterragruntなどのツールを使用することを考えています。

Terraform Cloudの使用

別のCI/CDツールも検討したのですが、IAMユーザのキー情報やCredentialをCI/CDツールヘ置かないためにTerraform Cloudを選択しました。CredentialをAWS外部に出さないためにCode Buildも検証したのですが柔軟な設定を行うには時間が取られそうだったので考慮から外しました。

Terraform Cloud単体の場合、一部設定をコード管理できなく、tfnotifyと連携できないなどいくつか問題はありそうなので、今後も最適な方法の検討を進めます。

Terraform Cloud Workspacesの使用

グループと環境単位でTerraform CloudのWorkspacesを分けます。

※最初Terraform CLIのWorkspacesとTerraform CloudのWorkspacesは同じものかと思っていましたが若干異なるので注意が必要です。CLIの方はローカルの1つのファイルで複数のグループのリソースを管理する機能です。詳細は公式ドキュメントをご確認ください。

従来のドキュメントでは環境分離にworkspaceを使用するのは良くないと書かれておりました。

また以下の記事で環境ごとに差分がある場合の注意はあります。

しかし、最近の公式のドキュメントやチュートリアルではworkspacesで環境を分ける方法が紹介されています。

今回は書籍「Infrastructure as Code」の1ソース複数環境の原則に近づけるため、環境差分は環境ごとのディレクトリで最小限に記載して、基本全環境でグループ内の同一モジュールを使用します。

環境ごとの差分はそこまで発生しないと結論づけ上記の設計にしました。数ヶ月後に採用して良かった点/悪かった点も記事で紹介したいと思います。

Workspaceの命名規則

公式では以下のパターンが紹介されています。

今回のプロジェクトでは上記そのままでなく、監視ツールなどを使用することを視野に入れて、クラウドプロバイダー名も含んで以下の命名規則としました。

${サービス名}-${クラウドプロバイダー名}-${機能名}-${環境名}
例:
workspaces {
    name = "xprj-aws-featurename-development"
}

各リソースの命名規則

module内リソースの命名規則は、project名や環境名をprefixとしてつけることとしました。アカウントごとに環境を分けていますが、リソース名で判別しやすいように環境名を付与しています。

${project名}-${環境名}-${役割}
例:
# system_name_prefixは${system名}-${機能名}で構成 

resource "aws_iam_role" "iot_to_firehose" {
  name = replace("${var.system_name_prefix}_iot_rule_to_kinesis_firehose_role", "-", "_")
 …
}

基本的には以下の記事と同様です。

所感

Serverlessだと現在はSAM、CDK、Serverless Frameworkを使うケースが多いためか、あまりTerraformでの設計の話がなかったので記事にしました。

またTerraform Cloudを利用した例もあまりネットで見なかった気がしたので、お試しの部分もあります。今回の設計を試して、数ヶ月後に実際どうだったのか振り返り記事も今後あげます。

Terraformに関してはまだまだ理解できてない部分も多いため、有識者の方で「ここがおかしい」という内容があれば是非コメントいただきたいと思っておりますのでよろしくお願いします。

参考資料

文中で参照した以外のもので参考にさせてもらった情報を記載します。