0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terraformモジュール構成をリファクタした話 — 個人開発でもIaCは裏切らない💪

0
Last updated at Posted at 2026-04-06

Terraformモジュール構成をリファクタした話 — 個人開発でもIaCは裏切らない💪

個人開発でモバイルアプリを作っていたら、Terraformの管理ファイルが膨れ上がって手に負えなくなりました🥹
リソース単位の薄いモジュールからAWSサービス単位のモジュールにリファクタした話をメモメモ・・・

はじめに — 個人開発でもこの規模になる

保護犬・保護猫のマッチングアプリ「Anipal」を個人で開発しています。技術スタックはReact Native (Expo) + AWSサーバーレス + Terraformです。

気づけばAWSリソースがこんな量になっていました。。。

  • Lambda関数: 数十個
  • DynamoDBテーブル: 10以上 + 複数のGSI
  • Cognito User Pool + Identity Pool + ソーシャルログイン(Google/Apple)
  • API Gateway (HTTP API) + Authorizer
  • S3 + CloudFront (OAC)
  • CloudWatch Alarms + CloudTrail + Slack通知
  • KMS(暗号化用CMK)

「個人開発にTerraformは大げさでは?」と思う人もいるかもしれませんが、この規模を手動管理するのは無理です。terraform plan で差分を確認してから terraform apply できる安心感は、一度味わうと手放せません🤤

Before: リファクタ前の問題

リファクタ前のディレクトリ構成はこうでした。(お恥ずかしい)

infra/
├── envs/dev/
│   ├── main.tf           # provider + ほぼ全リソース定義
│   ├── auth.tf            # 認証設定(200行超)
│   ├── api.tf             # API設定
│   ├── storage.tf         # ストレージ + CDN(300行超)
│   ├── monitoring.tf      # 監視 + アラーム
│   ├── lambda_xxx.tf      # ┐
│   ├── lambda_yyy.tf      # │ Lambda定義群(10ファイル以上)
│   ├── ...                # ┘
│   ├── variables.tf
│   └── outputs.tf
│
└── modules/
    ├── lambda/            # 単体Lambda作成ユーティリティ
    └── dynamodb/          # 単体DynamoDBテーブル作成ユーティリティ

問題点は明確でした。

  1. envs/dev/ に20ファイル以上が詰まっている — 認証、API、ストレージなどのサービスリソースが環境ディレクトリに直接書かれている
  2. modulesが薄すぎる — Lambda1個、DynamoDBテーブル1個を作るだけのラッパーで、サービス全体の設定がモジュール化されていない
  3. prod環境を追加するとき、大量コピペが必要 — サービスリソース定義を丸ごと複製することになる

合計数千行のTerraformコードが envs/dev/ に集中していて、見通しが悪くなっていました。

設計判断: リソース単位 vs サービス単位

リファクタにあたって2つの方針を検討しました。

方針A: リソース単位のモジュール化

modules/
├── cognito_user_pool/
├── cognito_identity_pool/
├── cognito_identity_provider/
├── s3_bucket/
├── cloudfront_distribution/
└── ...

AWSリソース1つ1つをモジュール化する方法。粒度は細かいけれど、モジュールの数が爆発して呼び出し側が複雑になります🧐

方針B: AWSサービス単位のモジュール化(採用)

modules/
├── auth/          # User Pool + Identity Pool + Providers + IAM
├── api/           # HTTP API + Stage + Authorizer
├── storage/       # S3 + CloudFront + OAC
└── ...

関連するリソースをサービス単位でまとめる方法。クラスメソッドさんの記事の設計思想を参考にしました。

方針Bを選んだ理由は、"「このサービスの設定を見たい」と思ったときに1ディレクトリで完結"するからです。認証の設定を確認するのに3つのモジュールを行き来するのは辛い。

ただし、Lambda関数群は envs/dev/ に残すことにしました。Lambda関数はサービスリソースというより「ビジネスロジックの定義」で、環境ごとに差異が出やすいためです。

After: サービス単位モジュールの構成

リファクタ後はこうなりました。

infra/
├── envs/dev/
│   ├── main.tf           # provider + 全モジュール呼び出し
│   ├── variables.tf      # 環境固有の変数
│   ├── outputs.tf        # モジュールoutput経由
│   ├── lambda_xxx.tf     # ┐
│   ├── lambda_yyy.tf     # │ Lambda定義群(変更は参照先のみ)
│   └── ...               # ┘
│
└── modules/
    ├── lambda/            # 既存ユーティリティ
    ├── dynamodb/          # 既存ユーティリティ
    ├── auth/              # 【新規】認証基盤
    ├── api/               # 【新規】APIゲートウェイ
    ├── database/          # 【新規】全テーブル定義
    ├── storage/           # 【新規】ストレージ + CDN
    └── monitoring/        # 【新規】監視・通知

envs/dev/main.tf がすっきり!

# envs/dev/main.tf(イメージ)

module "storage" {
  source       = "../../modules/storage"
  project      = local.project
  cors_origins = var.cors_origins
}

module "database" {
  source  = "../../modules/database"
  project = local.project
}

module "auth" {
  source              = "../../modules/auth"
  project             = local.project
  # ソーシャルログイン設定はSSM経由で取得した値を注入
  social_login_config = var.social_login_config
  storage_bucket_arn  = module.storage.bucket_arn
}

module "api" {
  source      = "../../modules/api"
  project     = local.project
  auth_config = module.auth.config
}

各モジュールが何を必要としているか、依存関係が一目でわかります。素晴らしい👏

storageモジュールの設計思想

storageモジュールには以下をまとめています。

  • S3バケット + パブリックアクセスブロック + バージョニング + 暗号化
  • CloudFront Distribution + OAC(Origin Access Control)+ キャッシュポリシー
  • S3バケットポリシー(CloudFront OAC連携)

ポイントは、CloudFront OACとS3バケットポリシーの相互参照がモジュール内で完結すること。呼び出し側は project と cors_origins を渡すだけで、CDN付きのセキュアなストレージが出来上がります。

databaseモジュールの工夫

テーブル定義モジュールでは、既存の単体テーブル作成ユーティリティを内部で再利用しています。

設計ポイント:

  • PIIを含むテーブル(ユーザー情報、申請情報等)はKMS CMKで暗号化
  • それ以外のテーブルはAWS管理キー(デフォルト暗号化)
  • GSIの設計は各テーブルのアクセスパターンに応じて定義
  • 暗号化キーの使い分けをモジュール内に閉じ込める

こうすることで、「どのテーブルがPII暗号化されているか」の管理が1ファイルで完結します。

terraform state mv との戦い

モジュール化するとリソースアドレスが変わります。

# Before(環境ディレクトリに直接定義)
aws_xxx_resource.main

# After(モジュール経由)
module.xxx.aws_xxx_resource.main

このままだとTerraformは「旧リソースを削除して新リソースを作成する」と判断します。本番環境で認証基盤やデータベースが再作成されたら大事故です。

対策は terraform state mv で既存リソースを新しいアドレスに移動することです。全モジュール合わせて数十回の state mv が必要でした。1つでも漏れるとplanに差分が出るので、モジュール1つずつ移行 → plan確認を繰り返しました。

ハマったこと・学び

モジュール間の依存解決

モジュール化で最も気を使ったのが依存関係の設計です。

storage    → 依存なし
database   → 依存なし
auth       → storage(IAMポリシーでバケットARN参照)
api        → auth(JWT Authorizerで認証設定参照)
monitoring → 全Lambda関数名

認証モジュールがストレージに依存するのは、IAMポリシーでS3アクセス権を設定するためと、PreSignUpトリガーにLambda ARNが必要だからです。

循環依存を避けるポイントは**「SSMやシークレットのdata sourceは envs/dev/ 側に残して、値だけをモジュールに渡す」**こと。data sourceまでモジュールに入れると、環境ごとのSSMパス差異に対応できなくなります。

Lambda定義ファイルの参照書き換え

Lambda定義ファイル(10ファイル以上)内の参照を一括で書き換える必要がありました。

変更前 変更後
aws_xxx_resource.main.id module.xxx.resource_id
aws_xxx_authorizer.main.id module.api.authorizer_id
module.dynamodb_xxx.table_name module.database.xxx_table_name

参照パターンが module.モジュール名.output名 に統一されたことで、新しいLambda関数を追加するときに「この値どこから取るんだっけ?」と迷わなくなりました。

リファクタ後の効果

  • envs/dev/ のファイル数: 20+ → Lambda定義 + main/variables/outputs に集約
  • prod環境の追加: モジュール呼び出し + 変数変更だけで済む
  • 新サービスの追加: modules/ にモジュールを作って main.tf から呼ぶだけ
  • コードレビュー: 「このPRは認証モジュールだけの変更」と範囲が明確になる

まとめ

  • 個人開発でもTerraformは裏切らない — リソースが数十個を超えると手動管理は不可能
  • リソース単位ではなくサービス単位でモジュール化 — "「この設定どこ?」が1ディレクトリで解決"する
  • terraform state mv は慎重に — 1モジュールずつ移行してplan確認を繰り返す
  • 依存関係の設計が肝 — data sourceは環境側に残し、値だけモジュールに注入する
  • Lambda関数は環境側に残す判断もアリ — ビジネスロジックの定義は環境固有になりやすい

個人開発だからこそ、インフラが壊れたときに助けてくれる人はいません。terraform plan が「No changes」と返してくれる安心感は、何物にも代えがたいです🙂‍↕️


このプロジェクトについて

Anipal(アニパル) は、保護犬・保護猫と里親をつなぐマッチング&SNSアプリです。
「つなぐ、届ける、見届ける」をコンセプトに、保護団体の業務負担を軽減しながら、動物たちに新しい家族との出会いを届けます。

現在CAMPFIREにてクラウドファンディング挑戦中です。応援いただけると嬉しいです🙇

👉 CAMPFIRE: https://camp-fire.jp/projects/934033/view


著者プロフィール

yuto
AWSエンジニア。保護犬猫の「出会いにくい」をゼロにしたいという想いから、個人でAnipalを開発中。
React Native (Expo) + AWS + Terraformで構築しています。

👉 X: @Anipal_app

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?