every Tech Blog

株式会社エブリーのTech Blogです。

Terraform のルートモジュールを分割しました

Terraform のルートモジュールを分割しました

エブリーで小売業界向き合いの開発を行っている @kosukeohmura です。

エブリーでは retail HUB という複数のプロダクトからなる小売業界向けのサービスの開発を行っています。以前まではサービス開発チームは単一で、その中で複数のプロダクトを開発を行ってきましたが、今後複数のチームがプロダクトごとに分かれて開発を行うこととなり、その体制の変化に伴って Terraform のルートモジュールを分割した話をします。

分割前のモジュール構成と、その問題点

今回紹介する作業を行う以前は、環境 (dev, prd) ごとに存在する単一のルートモジュールから各プロダクト向けのリソースを含むモジュールを参照する形でした。ルートモジュールが単一の Backend 定義を参照し、State ファイルも単一です。

このような構成だとシンプルな反面ルートモジュールに含むリソースが肥大化しやすく、plan / apply 等にかかる時間が長くなりやすいのですが、DELISH KITCHEN などの他の弊社のプロダクトと比較して、向こう数年のリソースの増え方は限定的になりそうなことと、単一のチームでの作業に最も適した形をとりたくこの形を取ってきました。

ところが開発チームや予算管理を分けようと思うと次の問題点が生じ、構造を見直すことにしました。

  • 他プロダクトの変更との競合への対処
    • Terraform の State と実際のリソースに差分が発生した場合、他チームへの差分の解消を依頼するなどのコミュニケーションコストがかかる
  • 一括で AWS タグを付与することができず、リソースの管理者が不明確になる
    • AWS タグの一括付与のためには AWS provider ブロックの default_tags を使う 必要があり、単一のルートモジュールだとこの方法が採れない
    • AWS の料金をプロダクトごとに分けて確認できないことにもなる

具体的には次のようなディレクトリ構造をしていました:

.
├── README.md
└── aws
    ├── envs
    │   └── dev             # dev 環境のルートモジュールや、また環境全体に関わる設定を配置
    │   │   ├── backend.tf  # AWS S3 を使用しています
    │   │   ├── main.tf     # ルートモジュール。このファイルから aws/modules 配下に定義してある各 Child Module を読み込む
    │   │   ├── provider.tf
    │   │   └── variables.tf
    │   └── prd
    │       ├── -- 省略 --
    │
    └── modules
        ├── common          # 特定のプロダクトに依存しないリソースを配置
        │   └── dev         # 特定のプロダクトに依存しない、dev 環境用のリソースを配置
        │       └── cloudtrail
        │           ├── main.tf     # output values 以外のリソースを定義
        │           └── outputs.tf  # 他 module で使用する output values を定義
        └── product_A               # product_A 用のリソースを配置
        │   └── dev                 # product_A 用の、dev 環境用のリソースを配置
        │   │   ├── ecs
        │   │   │   └── main.tf
        │   │   │   └── outputs.tf
        │   │   │
        │   │   -- 省略 --
        │   │   │
        │   │   └── vpc
        │   │       ├── main.tf
        │   │       └── outputs.tf
        │   └── prd  # product_A 用の、prd 環境用のリソースを配置
        │       ├── -- 省略 --
        │
        └── product_B  # product_B 用のリソースを配置
        │   ├── -- 省略 --

Style Guide - Configuration Language _ Terraform _ HashiCorp Developer に記載例に近しく、あまり特殊な点はありません。Backend に AWS S3 を使用しており、Workspaces を使用せずにモジュールごとにディレクトリを分けて State ファイルも環境ごとに 1 つ存在する構造です。

Terraform ルートモジュールを分割することに

問題点の解消にあたり、プロダクトごとに AWS アカウントを分離し HCL を管理する Git リポジトリごと分割する事も考えましたが、プロダクト同士が将来的に連携/統合する可能性を考えたときに都合が悪くなることを避けたく、AWS アカウントは同一のまま Terraform ルートモジュールをプロダクトごとに分割することとしました。

この変更によって向き合いのプロダクト以外との競合を気にせずに作業を行うことができ、また AWS provider ブロックをプロダクトごとに定義できます。 よって各リソースにプロダクト別のタグを付与できるので プロダクトごとに料金を分けて確認できる ことにもなり、今回の問題点が解消できます。

変更後のディレクトリ構成は下記のようになりました:

.
├── README.md
└── aws
    ├── common  # 特定のプロダクトに依存しないリソースを配置。cloudtrail など
    │   └── dev
    │       ├── cloudtrail
    │       |  ├── main.tf
    │       |  └── outputs.tf
    │       ├── backend.tf
    │       ├── main.tf
    │       ├── provider.tf
    │       └── variables.tf
    ├── product_A   # product_A のリソースを配置
    │   └── dev     # 環境ごとにディレクトリを切る
    │   |   ├── s3  # 各 Child Module は環境ごとのディレクトリ配下へ配置
    │   |   |  ├── main.tf
    │   |   |  └── outputs.tf
    │   |   |       # 他 Child Module ごとのディレクトリがここに並ぶ
    │   |   ├── backend.tf
    │   |   ├── main.tf  # product_A 用のルートモジュール
    │   |   ├── provider.tf
    │   |   └── variables.tf
    │   └── prd
    │       ├── -- 省略 --
    │
    └── product_B
    │   ├── -- 省略 --

以下、その際に行った作業を簡単に説明します。

1. 新ルートモジュールへと移動予定の Child Module を State から取り除く

すでに存在するルートモジュールの State から、新ルートモジュールへと移動予定の module を一旦 terraform state rm します。

$ terraform state rm \
    module.cloudtrail \
    module.s3 \
    # 省略

2. ルートモジュールのファイルを分割し、ディレクトリ構造を変更

単一のルートモジュールだった main.tf を分割し、プロダクトごとにディレクトリを分けて main.tf もそれぞれ置きます。合わせて各 Child Module をそのルートモジュール配下のディレクトリへと移動します。

ディレクトリの移動作業は基本的にはディレクトリを単に mv し、ルートモジュールからの参照先のファイルパスを変更するような単純な内容ですが、他のルートモジュールへ移動してしまったモジュールの Output value への参照はできなくなり、その際は variablelocals を定義して対処療法的にハードコードしました。ここは(試せていませんが) terraform_remote_state Data Source を使用して他の State を参照するように出来ると思います。

3. 新ルートモジュールへと移動するリソースを import する

まだ新ルートモジュールの State は空のままなので、terraform import コマンドを使って先ほど terraform state rm したリソースを新ルートモジュールへと追加していきます。

$ terraform import module.api-gateway.aws_api_gateway_account.this api-gateway-account
$ terraform import module.cloudtrail.aws_cloudtrail.default arn:aws:cloudtrail:<region>:<account_id>:trail/default
# 省略

import したら、最後に terraform plan で差分がないことを確認して作業終了です。

リソースを State から削除する際は terraform state rm コマンドを Child Module ごとに実行できたためコマンドの数も少なくコマンドの内容もシンプルでした。一方 terraform import ではリソースごとにコマンドを実行する必要があり、また引数の指定方法もリソースの種別ごとに違いがあるためにこのコマンドの作成が割と大変です。

私達の場合は import 対象のリソースがまだ 30 個ほどであったため目立った問題にはなりませんでしたが、リソース量が多いとコマンドを作成・実行する量が膨大になると思います。

後日談: 次に同じことをやるなら、、

今回の作業の終わりかけの段階で、公式記事による State ファイルの分割方法を見つけました。

support.hashicorp.com

記事ではモジュール構成は変えず、単に terraform state mv コマンドの -state, -state-out オプションで Child Module ごとに State ファイルを移動させる方法が書かれています。

私達の場合はルートモジュール分割にあたり、

  • 先に terraform state rm で新ルートモジュールにて管理したいリソースを既存のルートモジュールから取り除く
  • 空のルートモジュールを作成する
  • そこへ terraform import し、新ルートモジュールにて管理したいリソースを新ルートモジュールへ import しなおす

ような作業を行いましたが、

  • 先に State ファイルを記事記載の方法で分割する
  • 次に新ルートモジュールを、分割済みの State ファイルを使う形で作成する
  • あわせて、既存ルートモジュールから新ルートモジュールで管理するリソースの記述を削除する

するアプローチを取れると、terraform import コマンドをリソースごとに実行する必要がなく作業量を減らせたかもしれません。もしまた同じようなことを行うのであれば、その方法を試してみたいと思います。

さいごに

ここまでお読みいただきありがとうございました。開発チームも複数になり、retail HUB の開発もこれまで以上に活発になっています。そんなチームに興味がある方、ぜひ一度お話しましょう!

corp.every.tv