iigtn-platform 解説書

AWS 初心者向けに、このサイト(lab.iigtn.com)がどう構築されているかを 1 ページで解説します。Terraform / GitHub Actions / 各 AWS サービスの設定が、何のために何処から来ているか、実ファイルベースで追えるようにしています。

本ページに掲載されているコードは、解説目的で実際の構成から抽出しています。AWS アカウント ID・メールアドレス・固有 ID など 個人特定可能 / 機密性のある情報は XXXXXXXXXXXXyour-email@example.com で伏せて あります。

目次

  1. はじめに — このサイトは何をやっているか
  2. AWS 用語の予習
  3. 全体アーキテクチャ図
  4. リポジトリ構造
  5. Terraform モジュール 5 個の詳細
  6. envs/prod の役割(モジュールを呼ぶ側)
  7. CI/CD ワークフロー
  8. 設定値はどこから来てどこへ流れるか
  9. 「git push」が起きた時に何が動くか
  10. 月額コストの内訳
  11. 運用上の知識

1. はじめに — このサイトは何をやっているか

このサイト lab.iigtn.com は、フリーランス・インフラエンジニアのポートフォリオ基盤として AWS 上に構築されたサーバレス構成のウェブサイトです。
特徴は次の 3 つ:

  1. サーバレス(常駐サーバなし): アクセスがない時は 0 円 に近い課金。サーバの OS パッチや常時稼働の心配が不要。
  2. すべて Terraform 管理: 100% コードで構成を記述しており、誰がいつ何を作ったかが Git 履歴に残る。同じ構成を複製・再構築できる。
  3. 静的キーレス CI/CD: GitHub に push したら自動デプロイ。AWS のアクセスキーを GitHub Secrets に置かない。

使われている AWS サービス(一目で)

サービス役割
S3HTML / CSS / JavaScript / 画像の保管庫
CloudFront世界中の Edge から配信する CDN + HTTPS 終端
ACM無料の TLS 証明書発行 (lab.iigtn.com 用)
API Gateway問い合わせフォーム用 HTTP エンドポイント
LambdaAPI のサーバ側処理(Node.js)
DynamoDB問い合わせ内容の保存
SES問い合わせ通知メール送信(オプション)
CloudWatchログ + アラーム + メトリクス
SNSアラーム通知メール
IAM権限管理 (Role / OIDC)
Budgets月額予算超過のアラート

11 サービスありますが、それぞれの責任範囲は明確で、ぼんやり大量に動かしているわけではありません。次の章で順番に解説します。


2. AWS 用語の予習

本ページで頻出する用語を最初にまとめておきます。意味が分からなくなったらここに戻ってください。

2-1. アーキテクチャ系

用語意味
サーバレス「サーバを自分で管理しない」アーキテクチャ。OS パッチ・容量・台数がクラウドベンダー側でケアされる。Lambda / API Gateway / S3 等
マネージドサービスAWS が裏側を運用してくれるサービスの総称。サーバレスに似た概念
リージョン (Region)AWS の地理的データセンター。ap-northeast-1 = 東京、us-east-1 = バージニア北部
アカウント IDAWS アカウントを識別する 12 桁の数字。例: XXXXXXXXXXXX
ARN (Amazon Resource Name)AWS リソースを一意に指す文字列。arn:aws:<サービス>:<リージョン>:<アカウントID>:<リソース> の形
VPC仮想ネットワーク。今回は VPC を使わない構成(Lambda は VPC 外で動く)

2-2. ストレージ・配信系

用語意味
S3 (Simple Storage Service)オブジェクトストレージ。「バケット」という入れ物にファイルを入れる
バケット (Bucket)S3 のファイル置き場の単位。名前は AWS 全体で一意
オブジェクト (Object)S3 の中の 1 ファイル
CloudFrontAWS の CDN。世界中の Edge ロケーション にコピーをキャッシュして高速配信
DistributionCloudFront の設定単位 (1 ドメイン分の構成)
OriginCloudFront が大元のファイルを取りに行く先(S3 や API GW など)
OAC (Origin Access Control)CloudFront だけが S3 を読めるようにする最新の認証方式。S3 を完全非公開のまま CDN 配信できる
OAI (Origin Access Identity)OAC の旧式。新規構築では OAC が推奨
BehaviorCloudFront のルーティング規則 (/api/* は API GW へ など)

2-3. コンピュート・データ系

用語意味
Lambdaサーバレス関数実行サービス。トラフィック 0 時は完全無料
Cold StartLambda の初回呼び出しでコンテナ起動にかかる遅延 (数百 ms〜1 秒)
RuntimeLambda が走らせる言語ランタイム。nodejs20.x など
API Gateway HTTP APILambda の前段で HTTP を喋れるようにする AWS のマネージド API。REST 版より約 1/3 価格
DynamoDBフルマネージド KVS。スキーマレス・自動スケール
PK (Partition Key)DynamoDB のメインキー
On-demandDynamoDB の課金モード。読み書き回数で課金 (アクセス薄なら最安)

2-4. 認証・セキュリティ系

用語意味
IAM (Identity and Access Management)AWS のアクセス権管理
IAM User人間に紐付く永続的な識別子
IAM Role「役割」。一時的に誰でも適切に AssumeRole すればなれる
Trust Policy「誰がこの Role を AssumeRole できるか」を定義
Permissions Policy「Role になった結果、何ができるか」を定義
OIDC (OpenID Connect)認証連携の業界標準プロトコル。GitHub <-> AWS で短期トークンをやりとりする
STS (Security Token Service)AWS の一時認証発行サービス
AssumeRoleIAM Role になる API 呼び出し
ACM (AWS Certificate Manager)無料 TLS 証明書発行サービス
SSE (Server-Side Encryption)S3 / DynamoDB の保存時自動暗号化

2-5. 監視・運用系

用語意味
CloudWatchログ・メトリクス・アラームの一元サービス
CloudWatch LogsLambda などのログ集約先
CloudWatch Alarmメトリクスが閾値を超えたら通知
SNS (Simple Notification Service)イベント通知のハブ。メール送信もできる
AWS Budgets月額予算超過アラート

2-6. Terraform 用語

用語意味
Resourceresource "aws_s3_bucket" "this" { ... } 1 個 = AWS 上の 1 リソース
Moduleリソース群を再利用可能にまとめたもの
Variableモジュールの入力
Outputモジュールの出力
ProviderAWS / GCP などクラウド毎のプラグイン
Provider Alias同じ AWS でも別リージョンを並行で使う設定 (us-east-1 用など)
Data Source既存リソースを読むだけ (作らない)
Localモジュール内の中間計算値
State (tfstate)Terraform が「現状 AWS に何があるか」を記録するファイル
Lock複数人が同時 apply するのを防ぐための排他制御
Backendstate を保管する場所 (S3 + DynamoDB Lock の組合せが定番)

3. 全体アーキテクチャ図

このサイトは大きく分けて 3 つの流れ で構成されています。

① 訪問者がページを見る流れ

② 訪問者がフォームを送る流れ

③ コードを更新してサイトに反映する流れ


4. リポジトリ構造

このサイトのコードは GitHub リポジトリ iigtn/iigtn-platform(現在 private)に集約されています。中身は以下のような構造です。

iigtn-platform/
├── README.md                         # プロジェクト全体の概要
├── LICENSE                           # MIT
├── .gitignore                        # node_modules / *.tfstate / .env を除外
├── .terraform-version                # tfenv 用 (1.10.3)
├── .tool-versions                    # asdf 用 (terraform 1.10.3, nodejs 20.18.1)
│
├── docs/                             # 設計書 v5 (8 ファイル)
│   ├── motivation.md                 #  なぜこの構成にしたか
│   ├── cost.md                       #  コスト試算
│   ├── metrics.md                    #  実運用メトリクス計画
│   ├── runbook.md                    #  障害対応手順
│   ├── lessons.md                    #  失敗・反省・悩み
│   ├── business.md                   #  クライアント案件展開
│   ├── interview.md                  #  面接 Q&A
│   ├── adr/                          #  アーキテクチャ判断記録 (空)
│   └── postmortem/                   #  障害ポストモーテム (空)
│
├── frontend/                         # 静的サイトファイル
│   ├── index.html                    #  トップページ + 問い合わせフォーム
│   └── learn.html                    #  本解説書
│
├── backend/                          # Lambda コード
│   └── functions/
│       └── contact/
│           └── index.mjs             # 問い合わせ処理
│
├── terraform/                        # IaC (Infrastructure as Code)
│   ├── bootstrap/                    # 手動で 1 回だけ実行する初期化
│   │   ├── README.md
│   │   ├── setup.sh                  # bash 用
│   │   └── setup.ps1                 # PowerShell 用
│   │
│   ├── modules/                      # 再利用可能なモジュール 5 種
│   │   ├── network_dns/              #  ACM 証明書発行 + DNS 検証待ち
│   │   ├── frontend_cdn/             #  S3 + CloudFront + OAC
│   │   ├── backend_api/              #  API GW + Lambda + DynamoDB
│   │   ├── ci_oidc/                  #  GitHub OIDC + IAM Role
│   │   └── observability/            #  CloudWatch Alarms + SNS + Budgets
│   │
│   └── envs/
│       └── prod/                     # 本番環境 (lab.iigtn.com)
│           ├── backend.tf            #  state を S3 に置く設定
│           ├── providers.tf          #  AWS provider 2 つ (default + us_east_1)
│           ├── versions.tf           #  Terraform / provider バージョン
│           ├── variables.tf          #  本環境への入力変数
│           ├── terraform.tfvars      #  入力変数の実値
│           ├── main.tf               #  モジュール呼び出し
│           └── outputs.tf            #  apply 後に表示する値
│
└── .github/
    └── workflows/
        └── frontend-deploy.yml       # main push → 自動デプロイ
Terraform は階層構造を持ちます。 モジュール (terraform/modules/) は レゴブロック、環境定義 (terraform/envs/prod/) はそれを 組み立てる側。1 つのモジュールを dev / prod / staging で別々の値で呼び出せる仕組みになっています。

5. Terraform モジュール 5 個の詳細

各モジュールは独立したディレクトリにあり、1 つの責務 を持っています。中身を順番に見ていきます。

5-1. network_dns モジュール — TLS 証明書発行

目的

lab.iigtn.com 用の TLS 証明書を ACM で発行し、検証完了まで待つ。

作成リソース

versions.tf — このモジュールが必要とする道具

terraform {
  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"

      # ACM 証明書 (us-east-1) を扱うため、呼び出し側から
      # aws.us_east_1 alias provider を渡してもらう前提。
      configuration_aliases = [aws.us_east_1]
    }
  }
}

configuration_aliases は「このモジュールは default の AWS プロバイダに加えて、別リージョン用の aws.us_east_1 も外から渡してもらう必要がある」を宣言しています。

variables.tf — 入力変数

variable "domain_name" {
  description = "Hosted zone のドメイン名。例: lab.iigtn.com"
  type        = string

  validation {
    condition     = length(var.domain_name) > 0 && !can(regex("\\.$", var.domain_name))
    error_message = "domain_name は空でなく、末尾のドット (.) を含まないこと。"
  }
}

variable "tags" {
  description = "全リソースに付ける共通タグ"
  type        = map(string)
  default     = {}
}

validation ブロックで「不正な値が来たら apply を止める」入力チェックができます。

main.tf の核心部分

resource "aws_acm_certificate" "this" {
  provider = aws.us_east_1                       # ← 必ず us-east-1 で発行

  domain_name               = var.domain_name    # 例: lab.iigtn.com
  subject_alternative_names = ["*.${var.domain_name}"]  # ワイルドカード
  validation_method         = "DNS"

  lifecycle {
    create_before_destroy = true                 # 差し替え時に瞬断ゼロ
  }
}

resource "aws_acm_certificate_validation" "this" {
  provider = aws.us_east_1

  certificate_arn         = aws_acm_certificate.this.arn
  validation_record_fqdns = [
    for dvo in aws_acm_certificate.this.domain_validation_options :
    dvo.resource_record_name
  ]

  timeouts {
    create = "75m"   # 親 DNS で CNAME 投入する時間を確保
  }
}

変数の流れ

outputs.tf — 出力

output "certificate_arn" {
  description = "Validated ACM certificate ARN (us-east-1)"
  value       = aws_acm_certificate_validation.this.certificate_arn
}

output "validation_records" {
  description = "Squarespace の DNS に追加する CNAME レコード一覧"
  value = {
    for dvo in aws_acm_certificate.this.domain_validation_options :
    dvo.domain_name => {
      name  = dvo.resource_record_name
      type  = dvo.resource_record_type
      value = dvo.resource_record_value
    }
  }
}

validation_recordsterraform apply の結果に表示され、その値を Squarespace の DNS 管理画面に手動で投入することで ACM 検証が完了する仕組み。

5-2. frontend_cdn モジュール — 静的サイト配信基盤

目的

S3 (非公開) + CloudFront (with OAC) で lab.iigtn.com の静的サイト配信を行う。/api/* は API Gateway に振り分ける。

作成リソース (7 個)

  1. aws_s3_bucket — 静的ファイル保管
  2. aws_s3_bucket_public_access_block — 公開ブロック 4 種すべて true
  3. aws_s3_bucket_versioning — ロールバック用
  4. aws_s3_bucket_server_side_encryption_configuration — AES256
  5. aws_cloudfront_origin_access_control — OAC
  6. aws_cloudfront_distribution — CDN 本体
  7. aws_s3_bucket_policy — CloudFront だけに GetObject 許可

main.tf の主要部分

① S3 バケット + 安全装置

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name              # 例: iigtn-lab-web-prod-XXXXXXXXXXXX
  tags   = var.tags
}

resource "aws_s3_bucket_public_access_block" "this" {
  bucket                  = aws_s3_bucket.this.id
  block_public_acls       = true        # 4 つのスイッチを全部 ON
  ignore_public_acls      = true
  block_public_policy     = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
  }
}

② OAC (Origin Access Control)

resource "aws_cloudfront_origin_access_control" "this" {
  name                              = "${var.bucket_name}-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"   # 常に SigV4 署名
  signing_protocol                  = "sigv4"
}

③ CloudFront Distribution

resource "aws_cloudfront_distribution" "this" {
  enabled         = true
  is_ipv6_enabled = true
  http_version    = "http2and3"        # HTTP/3 (QUIC) 有効化

  aliases = [var.domain_name]          # 例: lab.iigtn.com
  price_class = "PriceClass_200"       # 北米+欧州+アジア (日本最適)
  default_root_object = "index.html"

  # Origin (1): S3 バケット
  origin {
    domain_name              = aws_s3_bucket.this.bucket_regional_domain_name
    origin_id                = "s3-${var.bucket_name}"
    origin_access_control_id = aws_cloudfront_origin_access_control.this.id
  }

  # Origin (2): API Gateway (api_origin_domain_name が空でない時のみ)
  dynamic "origin" {
    for_each = var.api_origin_domain_name == "" ? [] : [1]
    content {
      domain_name = var.api_origin_domain_name
      origin_id   = "apigw-origin"
      custom_origin_config {
        http_port              = 80
        https_port             = 443
        origin_protocol_policy = "https-only"
        origin_ssl_protocols   = ["TLSv1.2"]
      }
    }
  }

  # デフォルト behavior: S3 用
  default_cache_behavior {
    target_origin_id           = "s3-${var.bucket_name}"
    viewer_protocol_policy     = "redirect-to-https"
    allowed_methods            = ["GET", "HEAD"]
    cached_methods             = ["GET", "HEAD"]
    compress                   = true
    cache_policy_id            = "658327ea-f89d-4fab-a63d-7e88639e58f6"  # CachingOptimized
    response_headers_policy_id = "67f7725c-6f97-4210-82d7-5512b31e9d03"  # SecurityHeadersPolicy
  }

  # /api/* behavior: API Gateway 用
  dynamic "ordered_cache_behavior" {
    for_each = var.api_origin_domain_name == "" ? [] : [1]
    content {
      path_pattern             = var.api_path_pattern        # 既定 "/api/*"
      target_origin_id         = "apigw-origin"
      viewer_protocol_policy   = "redirect-to-https"
      allowed_methods          = ["DELETE","GET","HEAD","OPTIONS","PATCH","POST","PUT"]
      cached_methods           = ["GET","HEAD"]
      compress                 = true
      cache_policy_id          = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"  # CachingDisabled
      origin_request_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac"  # AllViewerExceptHostHeader
    }
  }

  # SPA 用エラーフォールバック
  custom_error_response {
    error_code         = 403
    response_code      = 200
    response_page_path = "/index.html"
  }
  custom_error_response {
    error_code         = 404
    response_code      = 200
    response_page_path = "/index.html"
  }

  restrictions { geo_restriction { restriction_type = "none" } }

  viewer_certificate {
    acm_certificate_arn      = var.certificate_arn      # network_dns から受け取り
    ssl_support_method       = "sni-only"               # vip だと月 $600
    minimum_protocol_version = "TLSv1.2_2021"           # 古いクライアント切り捨て
  }
}

④ S3 Bucket Policy (OAC からの読み取り限定許可)

data "aws_iam_policy_document" "bucket" {
  statement {
    sid    = "AllowCloudFrontReadViaOAC"
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.this.arn}/*"]

    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = [aws_cloudfront_distribution.this.arn]   # 自分の Distribution のみ
    }
  }
}

resource "aws_s3_bucket_policy" "this" {
  bucket = aws_s3_bucket.this.id
  policy = data.aws_iam_policy_document.bucket.json
}

変数の流れ

変数由来使われ方
var.domain_nameenvs/prod/main.tf "lab.iigtn.com"CloudFront aliases
var.certificate_arnmodule.network_dns.certificate_arnviewer_certificate
var.bucket_namelocal.web_bucket_name = "iigtn-lab-web-prod-${アカウントID}"S3 バケット名
var.api_origin_domain_namemodule.backend_api.api_endpoint_hostAPI GW origin

外向き出力

5-3. backend_api モジュール — 問い合わせフォーム処理

目的

API Gateway HTTP API + Lambda + DynamoDB + (オプション) SES で問い合わせフォームを処理する。

作成リソース (10 個)

  1. aws_dynamodb_table — 問い合わせ保存 (PK=UUID)
  2. aws_lambda_function — フォーム処理 (Node.js 20 / arm64 / 256MB / 10s)
  3. data "archive_file" — Lambda コードの zip パッケージング
  4. aws_cloudwatch_log_group — Lambda ログ (14 日保持)
  5. aws_iam_role — Lambda 実行ロール
  6. aws_iam_role_policy — Lambda 用最小権限 inline ポリシー
  7. aws_apigatewayv2_api — HTTP API
  8. aws_apigatewayv2_stage$default + throttling
  9. aws_apigatewayv2_integration — API GW → Lambda の統合
  10. aws_apigatewayv2_route × 2 — POST と OPTIONS
  11. aws_lambda_permission — API GW から Lambda 呼出許可

Lambda コード本体 (backend/functions/contact/index.mjs)

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
import { randomUUID } from "node:crypto";

// ハンドラ外で初期化 = 同一コンテナ再利用で高速化
const ddbClient = new DynamoDBClient({});
const ddb = DynamoDBDocumentClient.from(ddbClient);
const sesClient = new SESv2Client({});

// 環境変数 (Terraform で渡される)
const TABLE_NAME = process.env.DDB_TABLE;
const SES_FROM = process.env.SES_FROM;
const SES_TO = process.env.SES_TO;
const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN;

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function validate(payload) {
  const errors = [];
  // ... name/email/message のバリデーション
  return errors;
}

export const handler = async (event) => {
  // 1. POST 以外と OPTIONS 弾き
  // 2. JSON パース
  // 3. バリデーション
  // 4. DDB に保存 (UUID + ISO datetime + IP + User-Agent)
  // 5. SES でメール送信 (失敗しても継続)
  // 6. 200 {"ok": true} 返す
};

Lambda 環境変数の出処

環境変数Terraform 側の出処
DDB_TABLEaws_dynamodb_table.contacts.name
SES_FROMvar.ses_from (空なら SES スキップ)
SES_TOvar.ses_to
ALLOWED_ORIGIN"https://${var.domain_name}"

main.tf の Lambda 部分

data "archive_file" "lambda" {
  type        = "zip"
  source_dir  = var.lambda_source_dir       # ../../../backend/functions/contact
  output_path = "${path.module}/.build/contact_lambda.zip"
}

resource "aws_lambda_function" "contact" {
  function_name    = "${var.name_prefix}-contact"
  role             = aws_iam_role.lambda.arn
  handler          = "index.handler"
  runtime          = "nodejs20.x"
  architectures    = ["arm64"]              # 約 20% 安
  timeout          = 10
  memory_size      = 256
  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256

  environment {
    variables = {
      DDB_TABLE      = aws_dynamodb_table.contacts.name
      SES_FROM       = var.ses_from
      SES_TO         = var.ses_to
      ALLOWED_ORIGIN = var.allowed_origin
    }
  }
}

IAM ポリシー (Lambda が AWS リソースにアクセスする権限)

data "aws_iam_policy_document" "lambda_policy" {
  # CloudWatch Logs (このログストリームのみ)
  statement {
    actions   = ["logs:CreateLogStream", "logs:PutLogEvents"]
    resources = ["${aws_cloudwatch_log_group.contact.arn}:*"]
  }
  # DynamoDB (この contacts テーブルのみ。Put のみ)
  statement {
    actions   = ["dynamodb:PutItem"]
    resources = [aws_dynamodb_table.contacts.arn]
  }
  # SES SendEmail
  statement {
    actions   = ["ses:SendEmail", "ses:SendRawEmail"]
    resources = ["*"]
  }
}

API Gateway HTTP API

resource "aws_apigatewayv2_api" "this" {
  name          = "${var.name_prefix}-api"
  protocol_type = "HTTP"

  cors_configuration {
    allow_origins = [var.allowed_origin]      # https://lab.iigtn.com
    allow_methods = ["POST", "OPTIONS"]
    allow_headers = ["Content-Type"]
    max_age       = 300
  }
}

resource "aws_apigatewayv2_integration" "contact" {
  api_id                 = aws_apigatewayv2_api.this.id
  integration_type       = "AWS_PROXY"        # Lambda Proxy 統合
  integration_uri        = aws_lambda_function.contact.invoke_arn
  payload_format_version = "2.0"              # HTTP API 用
}

resource "aws_apigatewayv2_route" "contact_post" {
  api_id    = aws_apigatewayv2_api.this.id
  route_key = "POST /api/contact"
  target    = "integrations/${aws_apigatewayv2_integration.contact.id}"
}
5-4. ci_oidc モジュール — GitHub Actions OIDC 認証

目的

GitHub Actions が 静的アクセスキーなし で AWS にデプロイできるように、OIDC Provider と IAM Role を作る。

作成リソース (3 個)

  1. aws_iam_openid_connect_provider — GitHub を IdP として登録
  2. aws_iam_role — GitHub Actions が AssumeRole する役割
  3. aws_iam_role_policy — 最小権限 inline ポリシー

main.tf の核心

① OIDC Provider 登録

resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

② Trust Policy — 誰が AssumeRole できるか

data "aws_iam_policy_document" "trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }

    # 条件 1: aud (audience) は AWS STS のみ
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    # 条件 2: sub (subject) は指定 owner/repo の指定ブランチのみ
    # JWT の sub claim は "repo:<owner>/<repo>:ref:refs/heads/<branch>" 形式
    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values = [
        for branch in var.allowed_branches :
        "repo:${var.github_owner}/${var.github_repo}:ref:refs/heads/${branch}"
      ]
    }
  }
}

③ Permissions Policy — Role になった結果、何ができるか

data "aws_iam_policy_document" "deploy" {
  statement {
    actions   = ["s3:ListBucket", "s3:GetBucketLocation"]
    resources = [var.deploy_bucket_arn]                   # この 1 バケットだけ
  }
  statement {
    actions   = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:PutObjectAcl"]
    resources = ["${var.deploy_bucket_arn}/*"]
  }
  statement {
    actions = [
      "cloudfront:CreateInvalidation",
      "cloudfront:GetInvalidation",
      "cloudfront:ListInvalidations"
    ]
    resources = [var.cloudfront_distribution_arn]         # この 1 Distribution だけ
  }
}

変数の流れ

変数由来
var.github_ownertfvars の "iigtn"
var.github_repo既定 "iigtn-platform"
var.allowed_branches既定 ["main"]
var.deploy_bucket_arnmodule.frontend_cdn.bucket_arn
var.cloudfront_distribution_arnmodule.frontend_cdn.distribution_arn

外向き出力

5-5. observability モジュール — 監視とアラート

目的

CloudWatch Alarm + SNS + AWS Budgets で「壊れたら気付ける」「コスト暴騰したら気付ける」仕組みを作る。

作成リソース

CloudFront 4xx アラームは us-east-1 必須 だが、CloudWatch アラームの alarm_actions は同一 region の SNS しか参照できないため、CF 用は envs/prod/main.tf で別リソースとして us-east-1 に直接定義している。

main.tf の核心

resource "aws_sns_topic" "alarms" {
  name = "${var.name_prefix}-alarms"
}

resource "aws_sns_topic_subscription" "email" {
  count     = var.alarm_email == "" ? 0 : 1
  topic_arn = aws_sns_topic.alarms.arn
  protocol  = "email"
  endpoint  = var.alarm_email
}

resource "aws_cloudwatch_metric_alarm" "lambda_errors" {
  alarm_name          = "${var.name_prefix}-lambda-errors"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  threshold           = 1
  period              = 300
  statistic           = "Sum"
  metric_name         = "Errors"
  namespace           = "AWS/Lambda"
  treat_missing_data  = "notBreaching"

  dimensions = { FunctionName = var.lambda_function_name }

  alarm_actions = [aws_sns_topic.alarms.arn]
}

6. envs/prod の役割(モジュールを呼ぶ側)

モジュールが「レゴブロック」だとすると、envs/prod/ はそれを 組み立てて 1 つの本番環境を作る 場所です。ここに 7 個のファイルがあります。

6-1. backend.tf — Terraform の状態保存先

terraform {
  backend "s3" {
    bucket         = "iigtn-tfstate-XXXXXXXXXXXX"   # bootstrap で作成
    key            = "envs/prod/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "iigtn-tflock"
    encrypt        = true
  }
}

「terraform apply で何を作ったか」の記録 (tfstate) を S3 に置き、複数人が同時に apply しないよう DynamoDB でロックを取る構成。

6-2. providers.tf — AWS プロバイダ 2 つ

provider "aws" {
  region = var.region   # default: ap-northeast-1

  default_tags {
    tags = local.common_tags
  }
}

provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"  # CloudFront / ACM 用

  default_tags {
    tags = local.common_tags
  }
}

locals {
  common_tags = {
    Project     = "iigtn"
    Environment = "prod"
    ManagedBy   = "Terraform"
    Repo        = "iigtn-platform"
  }
}

default_tags は「すべての AWS リソースに自動で付くタグ」。個別 resource にタグを書かなくて済むので DRY。

6-3. variables.tf + terraform.tfvars — 入力

# variables.tf
variable "domain_name" { type = string }
variable "github_owner" { type = string }
variable "ses_from" { type = string; default = "" }
variable "ses_to"   { type = string; default = "" }
variable "alarm_email" { type = string; default = "" }
variable "monthly_budget_usd" { type = number; default = 10 }
# ...

# terraform.tfvars (実値)
domain_name  = "lab.iigtn.com"
github_owner = "iigtn"

6-4. main.tf — モジュール呼び出し

data "aws_caller_identity" "current" {}      # 自分のアカウント ID 取得

locals {
  web_bucket_name = "iigtn-lab-web-prod-${data.aws_caller_identity.current.account_id}"
}

module "network_dns" {
  source      = "../../modules/network_dns"
  domain_name = var.domain_name
  providers = {
    aws           = aws
    aws.us_east_1 = aws.us_east_1
  }
}

module "frontend_cdn" {
  source                 = "../../modules/frontend_cdn"
  domain_name            = var.domain_name
  certificate_arn        = module.network_dns.certificate_arn
  bucket_name            = local.web_bucket_name
  api_origin_domain_name = module.backend_api.api_endpoint_host
  providers = {
    aws           = aws
    aws.us_east_1 = aws.us_east_1
  }
}

module "backend_api" {
  source            = "../../modules/backend_api"
  name_prefix       = "iigtn-lab-prod"
  lambda_source_dir = "${path.root}/../../../backend/functions/contact"
  allowed_origin    = "https://${var.domain_name}"
  ses_from          = var.ses_from
  ses_to            = var.ses_to
}

module "ci_oidc" {
  source                      = "../../modules/ci_oidc"
  github_owner                = var.github_owner
  github_repo                 = var.github_repo
  allowed_branches            = var.github_allowed_branches
  deploy_bucket_arn           = module.frontend_cdn.bucket_arn
  cloudfront_distribution_arn = module.frontend_cdn.distribution_arn
}

module "observability" {
  source                     = "../../modules/observability"
  name_prefix                = "iigtn-lab-prod"
  alarm_email                = var.alarm_email
  lambda_function_name       = module.backend_api.lambda_function_name
  api_id                     = module.backend_api.api_id
  cloudfront_distribution_id = module.frontend_cdn.distribution_id
  monthly_budget_usd         = var.monthly_budget_usd
}

6-5. outputs.tf — apply 後に表示する値

terraform apply の最後に画面に出る値。重要なのは:


7. CI/CD ワークフロー

GitHub Actions ワークフロー (.github/workflows/frontend-deploy.yml) は、main ブランチに push されたら自動的に S3 sync + CloudFront 無効化を実行します。

ワークフロー全文

name: frontend-deploy

on:
  push:
    branches: [main]
    paths:
      - 'frontend/**'
      - '.github/workflows/frontend-deploy.yml'
  workflow_dispatch: {}

permissions:
  id-token: write       # OIDC token 発行に必須
  contents: read        # actions/checkout に必要

concurrency:
  group: frontend-deploy-${{ github.ref }}
  cancel-in-progress: true

env:
  AWS_REGION: ap-northeast-1
  AWS_ROLE_TO_ASSUME: arn:aws:iam::XXXXXXXXXXXX:role/iigtn-github-actions-deploy
  S3_BUCKET: iigtn-lab-web-prod-XXXXXXXXXXXX
  CF_DISTRIBUTION_ID: EXXXXXXXXXXXXX
  SITE_DIR: frontend
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }}
          role-session-name: gha-frontend-deploy-${{ github.run_id }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Verify caller identity
        run: aws sts get-caller-identity

      - name: Deploy to S3
        run: |
          aws s3 sync ./${{ env.SITE_DIR }}/ s3://${{ env.S3_BUCKET }}/ \
            --delete \
            --exclude ".git/*" --exclude "*.md" --exclude ".gitignore"

      - name: Invalidate CloudFront
        run: |
          INV_ID=$(aws cloudfront create-invalidation \
            --distribution-id ${{ env.CF_DISTRIBUTION_ID }} \
            --paths "/index.html" "/" \
            --query 'Invalidation.Id' --output text)
          aws cloudfront wait invalidation-completed \
            --distribution-id ${{ env.CF_DISTRIBUTION_ID }} \
            --id "$INV_ID"

      - name: Verify deployment
        run: curl -sI https://lab.iigtn.com/ | head -10

各ステップの解説

ステップ動作
Checkoutリポジトリのソースを Runner にダウンロード
Configure AWS credentials (OIDC)① GitHub から OIDC token を発行 → ② AWS STS に渡して AssumeRole → ③ 1 時間有効の AccessKey/SecretKey/SessionToken を Runner の env に注入
Verify caller identity「自分が誰として動いているか」を表示 (デバッグ用)
Deploy to S3aws s3 sync --delete で frontend/ → S3 を完全ミラー
Invalidate CloudFront/index.html/ のキャッシュを無効化 → 即時反映
Verify deploymentHTTPS で取得して動作確認

8. 設定値はどこから来てどこへ流れるか

Terraform の データフロー を理解すると、変数を変える時に何が壊れるかが読めます。

例 1: domain_name ("lab.iigtn.com")

terraform.tfvars
   │ domain_name = "lab.iigtn.com"
   ▼
envs/prod/variables.tf (var.domain_name)
   │
   ├──▶ module "network_dns" の var.domain_name
   │      ▼
   │      aws_acm_certificate.this の domain_name
   │      (ACM が "lab.iigtn.com" の証明書を発行)
   │
   ├──▶ module "frontend_cdn" の var.domain_name
   │      ▼
   │      aws_cloudfront_distribution.this の aliases
   │      (CloudFront に "lab.iigtn.com" を紐付け)
   │
   └──▶ module "backend_api" の var.allowed_origin
          ("https://" + var.domain_name)
          ▼
          API GW の cors_configuration.allow_origins

例 2: certificate_arn (動的に決まる値)

module "network_dns" の出力 certificate_arn
   │ apply 中に確定 (例 arn:aws:acm:us-east-1:XXX:certificate/...)
   ▼
module "frontend_cdn" の var.certificate_arn
   ▼
aws_cloudfront_distribution.this の viewer_certificate

例 3: アカウント ID(実行時に取得)

data "aws_caller_identity" "current" {}
   │ AWS API で取得 (例 XXXXXXXXXXXX)
   ▼
local.web_bucket_name = "iigtn-lab-web-prod-${アカウントID}"
   ▼
module "frontend_cdn" の var.bucket_name
   ▼
aws_s3_bucket.this の bucket
(S3 バケット名がアカウント ID で一意化される)

9. 「git push」が起きた時に何が動くか

  1. 開発者が frontend/index.html を編集
  2. git add . && git commit && git push
  3. GitHub が main ブランチへの push を検知
  4. .github/workflows/frontend-deploy.ymlon.push.paths がマッチ → ワークフロー起動
  5. Ubuntu Runner が起動 (約 5 秒)
  6. actions/checkout@v4 がリポジトリをダウンロード
  7. aws-actions/configure-aws-credentials@v4:
    • GitHub Actions に OIDC token を発行依頼
    • JWT の sub claim には repo:iigtn/iigtn-platform:ref:refs/heads/main
    • JWT を AWS STS に提示して AssumeRoleWithWebIdentity
    • AWS STS が IAM Role の Trust Policy で sub を検証 → 一致 → 一時認証発行
    • Runner の env に AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN 注入 (1h 有効)
  8. aws sts get-caller-identity で確認 (デバッグ用)
  9. aws s3 sync ./frontend/ s3://...:
    • frontend/ 内のファイルと S3 バケットの差分を計算
    • 変更された / 新しいファイルだけ PUT
    • --delete で S3 にあってローカルに無いファイルを削除
  10. aws cloudfront create-invalidation:
    • Distribution の Edge キャッシュから /index.html/ を削除
    • 次のアクセスで Origin (S3) から再取得
    • aws cloudfront wait で完了まで待機
  11. curl -sI https://lab.iigtn.com/ で HTTP 200 を確認
  12. OIDC で取得した一時認証は使い捨て (1h 後に自動失効)
ポイント: AWS のアクセスキーは GitHub にも Runner にも永続的に保存されない。漏れたら困るキーを「最初から発行しない」設計。

10. 月額コストの内訳

サービス無風時の月額(円)備考
Route 530使ってない
ACM0パブリック証明書は無料
S3 (state バケット + web バケット)~5容量数 MB レベル
DynamoDB (state lock + contacts)~2On-demand、ほぼ呼ばれない
CloudFront 転送量~855GB/月想定
CloudFront リクエスト~1050,000 reqs/月
API Gateway HTTP API~0.21,000 reqs/月
Lambda 実行0無料枠で吸収
Lambda CloudWatch Logs~10取り込み数十 MB
SES0未稼働
CloudWatch Alarms (4 個)~601 alarm 約 15 円/月
SNS~0少額
合計約 170〜200 円個人運用の最安レンジ
大幅に増えるシナリオ: ① 大量 Bot アクセス (CloudFront 転送量爆発) / ② Lambda 暴走 (無限ループ) / ③ DynamoDB スキャン無限 など。AWS Budgets で上限通知 を設定しておくのが必須。

11. 運用上の知識

11-1. デプロイのテンプレ

# 1. 編集
vim frontend/index.html

# 2. ローカル確認 (お好みで)
# - 静的なら python -m http.server 8000

# 3. push
git add frontend/index.html
git commit -m "Update homepage copy"
git push

# 4. GitHub Actions が自動デプロイ
# 5. https://lab.iigtn.com/ でブラウザ確認

11-2. CloudFront キャッシュを手動で消したい時

aws cloudfront create-invalidation \
  --distribution-id EXXXXXXXXXXXXX \
  --paths "/*"
/* 全体の無効化は CloudFront の月 1,000 paths 無料枠を消費する。日常的には個別 path を指定するのが安い。

11-3. DynamoDB の問い合わせ内容を見る

aws dynamodb scan --table-name iigtn-lab-prod-contacts --max-items 10

11-4. Lambda のログを見る

aws logs tail /aws/lambda/iigtn-lab-prod-contact --since 30m --follow

11-5. Terraform で何かを変える時

cd terraform/envs/prod

# 1. .tf を編集

# 2. plan で何が変わるか確認
terraform plan

# 3. 問題なければ apply
terraform apply

11-6. 全部消して作り直したい時

cd terraform/envs/prod

# 全リソース削除 (state バケット・ロックは残る)
terraform destroy

# state バケット自体は手動削除 (rm -rf 注意)
terraform destroy は破壊的。S3 バケットの中身を消し、CloudFront を削除し、Distribution の URL も変わる。本番運用中は絶対に走らせないこと。

このドキュメントの裏側

このページ自体も、上で説明した CI/CD パイプラインで配信されています。GitHub の frontend/learn.html を編集 → push すると、5 分以内にこのページが更新されます。

最終更新: 2026-04-25  |  ソース: github.com/iigtn/iigtn-platform (現在 private)