📑

TerraformでS3,CloudFront環境をアカウント跨ぎの移行

に公開

今までの記事

目次

/infraを深く考えて調査し、AWS構成図を.drawioファイルで作成してください。
・アイコン図形はAWS 2025を利用
・リソース間の通信の流れ、データの流れ
・AWSアカウントや、リージョンなどの関係性
・Terraformデプロイの関係性
・軽い説明

Terraform使用してのインフラ構築お勉強

作りたいもの
前回ブログで作成した環境をアカウント跨ぎで、同環境を作成
https://zenn.dev/tanoyusuke/articles/10fde333a63af2

2025/08/13〜 Terraform backend-setupの作成

まずbackend-setupが必要なのでprdの設定をします。

フェーズ 1:terraform.tf

git操作(ファイル編集始まり)

これからファイルの編集を行うので、何か間違えた時にある地点に戻せるようにgitのブランチを切ってファイルの編集を行う

$ git status #現在のgit状況を確認
$ git branch #現在のブランチの状況を確認
$ git checkout -b feature/terraform-backend-setup #ブランチの作成

Terraformのバージョンなどの取り決め

このファイルで、このディレクトリで使うTerrafromのバージョンを指定する
またAWSプロバイダのバージョンも指定する

# backend-setup/terraform.tf

terraform {
  required_version = ">= 1.5.0"

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

バージョン指定の演算子によって意味が変わる

演算子 意味
= 1.5.0 1.5.0 のみ固定
>= 1.5.0 1.5.0 以上
~> 1.5.0 1.5.X(1.6.0は除外)
>= 1.5, < 2.0 .5以上2.0未満

フェーズ 2:provider.tf

awsのプロバイダのリージョンを指定

ここでどのプロバイダーをTerraformで動かすか記述する

provider "aws" {
  region = var.aws_region  # variables.tfで定義された変数を参照
}

例えば複数のAWSアカウントを使い分ける場合はプロファイルを指定出来る。
全リソースに自動適用させるデフォルトタグの作成も可能。

フェーズ 3:variables.tf

main.tfで使う変数を定義する

ここで変数に値を入力することもあれば、terraform.tfvarsで入力することもある
terraform.tfvarsで変数に値を入力する方が柔軟性がある

variable "aws_region" {
  description = "The AWS region to create resources in."
  type        = string
  default     = "ap-northeast-1"
}

typeで型を指定することによって、意図しないデータの受け渡しを防ぐ
main.tfでの使用例:

name = "${var.project_name}-tfstate-lock"

フェーズ 4:main.tf

ここに記載されているリソースを作成する

variables.tfで定義した変数を使うことも可能
モジュールを使う際はここで、使うモジュールを指定する

1. データソース:AWSアカウントID取得

data "aws_caller_identity" "current" {}

・現在のAWSアカウントIDを動的に取得
・ハードコーディングを避けれる
取得出来る情報は様々

# 使用例
data.aws_caller_identity.current.account_id  # "123456789012"
data.aws_caller_identity.current.arn         # "arn:aws:iam::123456789012:user/terraform"
data.aws_caller_identity.current.user_id     # "AIDACKCEVSQ6C2EXAMPLE"

2. メインのS3バケット

resource "aws_s3_bucket" "terraform_state" {
  bucket = "${var.project_name}-tfstate-${data.aws_caller_identity.current.account_id}"
  
  lifecycle {
    prevent_destroy = true
  }
  
  tags = var.tags
}

構文の分解

resource "aws_s3_bucket" "terraform_state"
   ①         ②              ③

resourceキーワード

これからAWSリソースを作成すると言う宣言

resource # Terraformに新しいリソースを作ることを伝える
data # 既存リソースから情報を取得する場合
variable # 変数を定義する場合
output # 出力値を定義する場合

"aws_s3_bucket"リソースタイプ

"aws_s3_bucket"aws     = プロバイダー(AWS)
s3      = サービス名(Simple Storage Service)
bucket  = リソースの種類(バケット)

"terraform_state"リソース名(ローカル名)

Terraform内部での識別名(自由に命名可能)

# ✅ 良い例:用途が明確
resource "aws_s3_bucket" "terraform_state"
resource "aws_s3_bucket" "application_logs"
resource "aws_s3_bucket" "user_uploads"

# ❌ 悪い例:意味が不明確
resource "aws_s3_bucket" "bucket1"
resource "aws_s3_bucket" "test"
resource "aws_s3_bucket" "tmp"

誤ってterrafrom destroyをしても削除されない

prevent_destroy = true

variables.tfで定義しているタグが付与される

tags = var.tags

4. バージョニング設定

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

バージョニングの設定を作成

resource "aws_s3_bucket_versioning" "terraform_state"

バージョニングの設定を行うバケットを指定

bucket = aws_s3_bucket.terraform_state.id

5. 暗号化設定

resource: 作成するリソース、ローカル名

bucket: 対象のバケットを指定

apply_server_side_encryption_by_defaultはAWS APIで設定可能な項目

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

6. パブリックアクセスブロック

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  
  block_public_acls       = true  # パブリックACLをブロック
  block_public_policy     = true  # パブリックポリシーをブロック
  ignore_public_acls      = true  # 既存のパブリックACLを無視
  restrict_public_buckets = true  # パブリックバケットを制限
}

https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html?utm_source=chatgpt.com

7. DynamoDBテーブル(ステートロック)

resource "aws_dynamodb_table" "terraform_lock" {
  name         = "${var.project_name}-tfstate-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"
  
  attribute {
    name = "LockID"
    type = "S"
  }
  
  tags = var.tags
}

resource:リソース宣言

name = "${var.project_name}-tfstate-lock"

name:テーブル名
${var.project_name}:variables.tfで定義されてる変数を導入

課金モード

billing_mode =      # 課金方式の設定
"PAY_PER_REQUEST"   # オンデマンド課金(使った分だけ)

フェーズ 5:outputs.tf

出力させる値を決めるファイル

リソースが作成されないと決まらないもの(ID)などを出力させて
他のTerraformでのリソース作成の際に使用する

# backend-setup/outputs.tf

output "s3_bucket_id" {
  description = "The ID (name) of the S3 bucket for Terraform state."
  value       = aws_s3_bucket.terraform_state.id
}

output "dynamodb_table_name" {
  description = "The name of the DynamoDB table for Terraform state locking."
  value       = aws_dynamodb_table.terraform_lock.name
}

出力の宣言
output:出力値の宣言キーワード
"s3_bucket_id":出力の名前(任意)

value =                              # 実際に出力する値
aws_s3_bucket.terraform_state.id    # S3バケットのID(名前)を参照
     ↓              ↓          ↓
リソースタイプ  リソース名   属性

DynamoDBテーブル名の出力

output "dynamodb_table_name"           # 出力名
description = "..."                    # 説明文
value = aws_dynamodb_table.terraform_lock.name  # テーブル名を参照
              ↓                ↓         ↓
        リソースタイプ    リソース名   属性

なぜOutputが必要?

この出力値をモジュール間の連携などで使用する
これから難しいことをしていく時にどんどん活用される

git操作(ファイル編集終わり)

一区切りのファイル編集が終わったので、その編集した変更をリモートのメインブランチへ反映させるため操作を行う

$ git status #現在のgit状況を確認
$ git branch #現在のブランチの状況を確認
$ cd <プロジェクトのルートディレクトリ>
$ git add . #gitの変更をステージングにあげる
$ git status #現在のgit状況を確認
$ git commit -m "terraform-backend-setup" #gitの変更をローカルブランチへ適用させる
$ git push origin feature/terraform-backend-setup #変更をリモートのブランチへ適用させる
# ここからGithubなどのリモート側の操作
# リモートでmainブランチとfeature/terraform-backend-setupブランチがあるので、変更をmainブランチへ反映させるためにプルリクエストを行う
# プルリクエストを受けた側(自学習なら自分)は変更差分を見て問題無いか確認して、問題なければマージしてfeature/terraform-backend-setupブランチを削除する
# Githubなどのリモート側の操作はここまで
$ git branch #現在のブランチの状況を確認
$ git checkout main #最新のブランチに変更。リモートではもうブランチが削除されてる
$ git branch #現在のブランチの状況を確認
$ git pull origin main # リモートで反映させた変更が、ローカルにも反映される
$ git branch -d feature/terraform-backend-setup #不要になったブランチを削除する
$ git branch #現在のブランチの状況を確認

2025/08/13〜 モジュールとAssumeRoleを使用してのbackend-setupの作成へ変更

こんな感じ

┌─────────────────────────────────┐
│      管理/ツールアカウント (Hub)    │
│                                 │
│     作業者はまずここにログイン 🔐   │
│                                 │
│     Account: xxxxxxxxxxxx       │
└────────────┬────────────────────┘
             │
             │ AssumeRole
             │
    ┌────────┼────────┐
    │        │        │
    ▼        ▼        ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│   Dev   │ │   Stg   │ │   Prd   │
│ Account │ │ Account │ │ Account │
│ (Spoke) │ │ (Spoke) │ │ (Spoke) │
│         │ │         │ │         │
│ 111111  │ │ 222222  │ │ 333333  │
└─────────┘ └─────────┘ └─────────┘

フェーズ 1:“STS:AssumeRole” の許可

概要

IAM Identity Centerを作成しているので、IAM Identity Centerの管理アカウントから許可セットを作成し、AssumeRoleの踏み台にするアカウントに許可セットを割り当てる。

許可セットを作成

IAM Identity Centerから許可セットを作成
今回はprd,stg,devアカウントにAssumeRoleしたいので3つ記載
※追記: DNSアカウントを途中で追加したのでそのアカウントも追加が必要です

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": [
        "arn:aws:iam::<dev-account-id>:role/TerraformExecutionRole",
        "arn:aws:iam::<stg-account-id>:role/TerraformExecutionRole",
        "arn:aws:iam::<prd-account-id>:role/TerraformExecutionRole",
        "arn:aws:iam::<DNS-account-id>:role/TerraformExecutionRole",
      ]
    }
  ]
}

許可セットの割り当て

IAM Identity CenterからAWSアカウント→踏み台にするアカウントを選択し
割り当てたいユーザーやグループを選択し許可セットの割り当てを行う

フェーズ 2:IAMロールを作成

IAMロールの作成

prdアカウントからIAMサービスに移動し、ロールの作成を行う
信頼されたエンティティはカスタム信頼ポリシーを選択し以下を記載

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "<許可セットの割り当てから作成された、踏み台アカウントのロールarn>"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

次に、必要な権限を選択し作成

フェーズ 3:Terraformコード作成

git操作(ファイル編集始まり)

これからファイルの編集を行うので、何か間違えた時にある地点に戻せるようにgitのブランチを切ってファイルの編集を行う

$ git status #現在のgit状況を確認
$ git branch #現在のブランチの状況を確認
$ git checkout -b refactor/backend-setup-modules-assume-role #ブランチの作成

ディレクトリ構造

tano1:infra tano$ tree
.
├── backend-setup
│   ├── environments
│   │   └── prd
│   │       ├── backend.tf
│   │       ├── main.tf
│   │       ├── outputs.tf
│   │       ├── provider.tf
│   │       ├── terraform.tf
│   │       ├── terraform.tfstate
│   │       ├── terraform.tfvars
│   │       └── variables.tf
│   └── modules
│       └── backend
│           ├── main.tf
│           ├── outputs.tf
│           └── variables.tf

terraform.tf

terraformのバージョンとプロバイダのバージョンを指定

provider.tf

assume_roleを記載
AWS CLIのプロファイルが管理アカウントでも記載することによって、こちらのアカウントにリソースは作成される

provider "aws" {
  region = "ap-northeast-1"

  assume_role {
    # prdアカウント(0025...)で作成したロールのARN
    role_arn     = "<prdアカウント(0025...)で作成したロールのARN>"
    session_name = "tf-session-0603game-prd-backend"
  }
}

variables.tf

変数の定義を行うことによって、色んなところで変数を使用出来る

variable "project_name" {
  description = "プロジェクト名"
  type        = string
}

使用例

# S3バケット名で使用
resource "aws_s3_bucket" "state" {
  bucket = "${var.project_name}-tfstate-${var.environment}-${var.account_id}"
  # 結果: "0603game-tfstate-prd-333333333333"
}

outputs.tf

output "backend_s3_bucket_name" {
  description = "Terraformステート用のS3バケット名"
  value       = module.backend.s3_bucket_name
}

output "backend_dynamodb_table_name" {
  description = "Terraformステートロック用のDynamoDBテーブル名"
  value       = module.backend.dynamodb_table_name
}
output "backend_s3_bucket_name" {    # ① 出力名の定義
  description = "..."                 # ② 説明文
  value = module.backend.s3_bucket_name  # ③ 出力する値
           ↓      ↓           ↓
      モジュール参照  モジュール名  モジュールの出力名
}

main.tf

module "backend" {
  source = "../../modules/backend"

  project_name = var.project_name
  environment  = var.environment
  account_id   = var.account_id
  common_tags  = var.common_tags
}
module "backend" {
  source = "../../modules/backend"

作成するリソースのモジュールを指定し、そのファイルのパスも指定する
sourceは必須属性

変数の受け渡し

variables.tfで変数定義した入れ物に、terraform.tfvarsで値を挿入して、その値をmain.tfで参照される。
moduleに変数を引き渡す場合は、project_nameがmoduleのvariables.tfに対応していて、そこにvariables.tfの値を挿入している

  project_name = var.project_name
  environment  = var.environment
  account_id   = var.account_id
  common_tags  = var.common_tags
terraform.tfvars          variables.tf           main.tf              module
     の値          →    で変数定義    →    で変数参照    →    に渡される
                                             var.xxx              引数として

分かりやすいので。(生成AI)

Step 1: 値の定義
┌──────────────────────────────────────┐
│ environments/prd/terraform.tfvars    │
│                                      │
│ project_name = "0603game"            │
│ environment  = "prd"                 │
│ account_id   = "333333333333"        │
│ common_tags = {                      │
│   Project = "0603game"               │
│   Environment = "production"         │
│ }                                    │
└────────────────┬─────────────────────┘
                 │ 値を読み込み
                 ▼
Step 2: 変数の宣言
┌──────────────────────────────────────┐
│ environments/prd/variables.tf        │
│                                      │
│ variable "project_name" {            │
│   type = string                      │
│ }                                    │
│ variable "environment" {             │
│   type = string                      │
│ }                                    │
│ variable "account_id" {              │
│   type = string                      │
│ }                                    │
│ variable "common_tags" {             │
│   type = map(string)                 │
│ }                                    │
└────────────────┬─────────────────────┘
                 │ var.xxx で参照可能に
                 ▼
Step 3: モジュールへの受け渡し
┌──────────────────────────────────────┐
│ environments/prd/main.tf             │
│                                      │
│ module "backend" {                   │
│   source = "../../modules/backend"   │
│                                      │
│   project_name = var.project_name    │ ← "0603game"environment  = var.environment     │ ← "prd"account_id   = var.account_id      │ ← "333333333333"common_tags  = var.common_tags     │ ← {Project="0603game"...}}                                    │
└────────────────┬─────────────────────┘
                 │ モジュールの引数として渡す
                 ▼
Step 4: モジュール内での受け取り
┌──────────────────────────────────────┐
│ modules/backend/variables.tf         │
│                                      │
│ variable "project_name" {            │
│   description = "プロジェクト名"      │
│   type        = string               │
│ }                                    │
│ # ↑ モジュール内でvar.project_name  │#   として使用可能に                 │
└────────────────┬─────────────────────┘
                 │
                 ▼
Step 5: モジュール内での使用
┌──────────────────────────────────────┐
│ modules/backend/main.tf              │
│                                      │
│ resource "aws_s3_bucket" "state" {   │
│   bucket = "${var.project_name}-..." │ ← "0603game-..."tags = var.common_tags             │
│ }                                    │
└──────────────────────────────────────┘

backend.tf

ブーストトラップでのbackend.tfなので、後ほど書き換えが必要

# 初回はローカルにtfstateファイルを作成します
terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
}

terraform.tfvars

variables.tfの変数定義に値を挿入する

project_name = "0603game"
environment  = "prd"

# prdアカウントのID
account_id   = "002540791269"

common_tags = {
  ManagedBy   = "Terraform"
  Project     = "0603game"
  Environment = "prd"
}

modules/backend/variables.tf

変数を定義することにより、prd/main.tfから受け渡した変数の値をmodules/backendで使える

variable "project_name" {
  description = "プロジェクト名"
  type        = string
}

variable "environment" {
  description = "環境名 (例: prd, stg)"
  type        = string
}

variable "account_id" {
  description = "対象環境のAWSアカウントID"
  type        = string
}

variable "common_tags" {
  description = "すべてのリソースに適用する共通のタグ"
  type        = map(string)
  default     = {}
}

modules/backend/main.tf

# S3 Bucket for Terraform State
resource "aws_s3_bucket" "tfstate" {
  bucket = "${var.project_name}-${var.environment}-tfstate-${var.account_id}"

  lifecycle {
    prevent_destroy = true
  }

  tags = merge(
    var.common_tags,
    {
      Name = "${var.project_name}-${var.environment}-tfstate-bucket"
    }
  )
}

# S3 Bucket Versioning
resource "aws_s3_bucket_versioning" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  versioning_configuration {
    status = "Enabled"
  }
}

# S3 Bucket Public Access Block
resource "aws_s3_bucket_public_access_block" "tfstate" {
  bucket                  = aws_s3_bucket.tfstate.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# DynamoDB Table for Terraform State Lock
resource "aws_dynamodb_table" "tflock" {
  name         = "${var.project_name}-${var.environment}-tfstate-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }

  tags = merge(
    var.common_tags,
    {
      Name = "${var.project_name}-${var.environment}-tfstate-lock-table"
    }
  )
}

variables.tfでの変数定義が効いて来る

resource "aws_s3_bucket" "tfstate" {
  bucket = "${var.project_name}-${var.environment}-tfstate-${var.account_id}"

バケット名はproject-env-tfstate-account_id形式でグローバルな一意を確保

bucket = "${var.project_name}-${var.environment}-tfstate-${var.account_id}"
              ↓                    ↓                           ↓
         呼び出し元から      呼び出し元から           呼び出し元から
           受け取る値          受け取る値              受け取る値

terraform destroyでの削除保護

lifecycle {
    prevent_destroy = true

merge複数のマップを1つに結合
今回の場合は、var.common_tagsName = "${var.project_name}-${var.environment}-tfstate-bucket"を結合
Name = "${var.project_name}-${var.environment}-tfstate-bucket"はvariables.tfでの変数定義が効いて来る

  tags = merge(
    var.common_tags,
    {
      Name = "${var.project_name}-${var.environment}-tfstate-bucket"
    }
  )

S3 のバージョニング
S3 のパブリックアクセス遮断
DynamoDB(ロックテーブル)
上記は割愛

modules/backend/outputs.tf

何かで使う。
まだ不明

output "s3_bucket_name" {
  description = "Terraformステート用のS3バケット名"
  value       = aws_s3_bucket.tfstate.bucket
}

output "dynamodb_table_name" {
  description = "Terraformステートロック用のDynamoDBテーブル名"
  value       = aws_dynamodb_table.tflock.name
}

フェーズ 4:管理アカウントのSSOプロファイル作成

CLIでSSOプロファイル登録

CLIからコマンド入力すると、下記のように案内されるので従って登録する

tano1:0603game tano$ aws configure sso --profile <登録するプロファイル名>
SSO session name (Recommended): <セッション名>
There are 7 AWS accounts available to you.
Using the account ID <登録したいアカウント名>
There are 3 roles available to you.
Using the role name <登録したいロールを選択>
CLI default client Region [None]: ap-northeast-1 # リージョン指定
CLI default output format [None]: json # 出力形式

To use this profile, specify the profile name using --profile, as shown:

aws s3 ls --profile <プロファイル名が出てればOK>
tano1:0603game tano$

.envrcを使用してる場合はプロファイル名を環境変数として記載する

export AWS_PROFILE=<プロファイル名>

フェーズ 5:リソース作成

terraform init

作成するリソースのディレクトリで

ざっくり実施されてること
・バックエンドを確定
・Providersを取得
・Modulesを取得
・必要なファイル作成

つまりterraformでリソースを作るにあたり必要な、設定が確認される的な。。。?(たぶん)

terraform plan

ざっくり実施されてること
・これらの設定ファイルを読み込む
・現在のtfstate状態を読み込み
・実際のAWSリソースを確認
・実際のリソースとtfstate+実行計画との差分を計算
・実行計画生成、表示

差分表示とよく言われる

ヘッダーと記号

作成のみ表示されている(変更や削除は無し)

Resource actions are indicated with:
  + create

意味

# module.backend.aws_dynamodb_table.tflock will be created
  ↓          ↓                ↓            ↓        ↓
モジュール名  リソースタイプ    リソース名   アクション

確定している値は事前に記載されている

(known after apply) の意味

arn           = (known after apply)  # 作成後にAWSが割り当てる
id            = (known after apply)  # テーブル名と同じになる
read_capacity = (known after apply)  # PAY_PER_REQUESTなのでN/A

arn: arnのアカウントIDやリージョンはAWS側で決定
id: 作成完了後に確定
read_capacity: オンデマンドなので該当無し

terraform apply

ざっくり実施されてること
・terraform planの再実行
・リソース作成開始
・tfstate更新
・完了メッセージ

様々な問題でterraform planでOKだったものが、terraform applyで無理だった例は多々ある

フェーズ 6:backend.tf書き換え

terraform {
  backend "s3" {
    # applyで作成されたS3バケット名
    bucket = "<applyで作成されたS3バケット名>"

    # このバックエンドリソース自体のStateファイルのパス
    key = "backend/terraform.tfstate"

    region = "ap-northeast-1"

    # applyで作成されたDynamoDBテーブル名
    dynamodb_table = "<applyで作成されたDynamoDBテーブル名>"

    encrypt = true

    # バックエンド操作もassume_roleで行う
    role_arn = "<バックエンド操作もassume_roleで行うロール名>"
  }
}

そもそもパスのことをkeyって言うのはS3が階層構造ではなくて、フラットな構造になっているからで、コンソールで見ると階層構造のように見せている

    # このバックエンドリソース自体のStateファイルのパス
    key = "backend/terraform.tfstate"

terraform init -migrate-state

-migrate-state:バックエンド設定の変更を検出し、設定を変える

terraform init -migrate-state

git操作(ファイル編集終わり)

一区切りのファイル編集が終わったので、その編集した変更をリモートのメインブランチへ反映させるため操作を行う

$ git status #現在のgit状況を確認
$ git branch #現在のブランチの状況を確認
$ cd <プロジェクトのルートディレクトリ>
$ git add . #gitの変更をステージングにあげる
$ git status #現在のgit状況を確認
$ git commit -m "backend-setup-modules-assume-role" #gitの変更をローカルブランチへ適用させる
$ git push origin refactor/backend-setup-modules-assume-role #変更をリモートのブランチへ適用させる
# ここからGithubなどのリモート側の操作
# リモートでmainブランチとrefactor/backend-setup-modules-assume-roleブランチがあるので、変更をmainブランチへ反映させるためにプルリクエストを行う
# プルリクエストを受けた側(自学習なら自分)は変更差分を見て問題無いか確認して、問題なければマージしてrefactor/backend-setup-modules-assume-roleブランチを削除する
# Githubなどのリモート側の操作はここまで
$ git branch #現在のブランチの状況を確認
$ git checkout main #最新のブランチに変更。リモートではもうブランチが削除されてる
$ git branch #現在のブランチの状況を確認
$ git pull origin main # リモートで反映させた変更が、ローカルにも反映される
$ git branch -d refactor/backend-setup-modules-assume-role #不要になったブランチを削除する
$ git branch #現在のブランチの状況を確認

2025/08/14〜 静的Webサイトホスティングを本番環境に構築

構成内容

前回の記事で作成したものを別のアカウント(prd環境)で再現し、Terraformで管理する
(1/2)静的webサイトをS3にデプロイ!claude codeとGithub Actionsを用いて自動デプロイ!

DNS関連のお勉強

Route53の設定などをしたアカウントから、prd環境で再現するのでこれらの設定などの再現はどうするべきか検討する

ベストプラクティス

専用のアカウントでDNSを一元管理するのがベストプラクティス??
https://aws.amazon.com/jp/blogs/networking-and-content-delivery/dns-best-practices-for-amazon-route-53/

Route 53

AWSのマネージドDNSサービス。
ドメイン登録、DNSレコード管理、ヘルスチェックやトラフィック制御までを提供

Route 53はAWSサービス名
様々なDNS関連のサービスを提供している

Route 53 公開ホストゾーン

インターネット全体から参照されるDNSゾーン
あるドメインの権威情報を保持し、世界へ応答する。
NSレコードに書かれたネームサーバーをレジストラ側のNSに設定してはじめて権威になる。

・Route 53でゾーンを作ると、4つのネームサーバーが(NS)が発行される
・でも世界中のDNSはレジストラ(お名前.comなど)に登録されているNSだけ見に行く
・だからレジストラ側の設定画面でその4つのNSに変更すると、『このドメインの答えはRoute 53が出します』と全インターネットに委任される

Aレコード,Alias A

Aレコード:IPv4アドレスを紐づける基本レコード
Alias A:IPではなくAWSリソースに向けれる

CNAME

www.example.comd3xxx.cloudfront.netですよと伝えるだけ

NS / SOA レコード

NSレコード
厳密には分からないけど、そのレコードが登録されてるところが強い

SOAレコード
Route 53では自動管理され、手で触ることはほぼ無いらしい

ACM証明書と検証CNAME

証明書やDNS関連は難しい。。。

ACMが発行したCNAMEをRoute 53に作成
ACMがCNAMEでの検証をし、本人か確認する的な流れ

CloudFrontで独自ドメインをHTTPS配信をしたくてACMに証明書を申請
ACMが『このドメインの所有者の確認が必要』とのことで検証用CNAMEを発行
そして、権威DNSにその検証用CNAMEでCNEMEレコードを作成するとACMがそれをDNSに確認してDNS検証が完了となる

モジュールの粒度

アンチパターン

細かすぎるマイクロ分割

# ❌ マイクロ分割
modules/s3-bucket/
modules/s3-versioning/  # 細かすぎる

モジュール間の横串参照

# ❌ モジュール間の横串参照
data "terraform_remote_state" "other" { }  # 他モジュールの状態を直接参照

モジュール内でプロバイダー定義

# ❌ モジュール内でプロバイダー定義
provider "aws" {  # モジュール内で定義しない
  region = "us-east-1"
}

巨大な万能モジュール

# ❌ 巨大な万能モジュール
variable "create_s3" { }
variable "create_cloudfront" { }
variable "create_route53" { }  # フラグ地獄

ベストプラクティス

役割ごとのモジュールがベストプラクティス???
https://developer.hashicorp.com/terraform/tutorials/modules/pattern-module-creation
でも、よく分からん笑
使い倒すうちにメリットデメリットを実感してくる系??

フェーズ 1: 準備 — IAMロールとコードの骨格作り

git操作(ファイル編集始まり)

これからファイルの編集を行うので、何か間違えた時にある地点に戻せるようにgitのブランチを切ってファイルの編集を行う

$ git status #現在のgit状況を確認
$ git branch #現在のブランチの状況を確認
$ git checkout -b feature/prod-static-website-hosting #ブランチの作成

ディレクトリ構造

tano1:infra tano$ tree
.
├── backend-setup
│   ├── environments
│   │   └── prd
│   │       ├── backend.tf
│   │       ├── main.tf
│   │       ├── outputs.tf
│   │       ├── provider.tf
│   │       ├── terraform.tf
│   │       ├── terraform.tfstate
│   │       ├── terraform.tfvars
│   │       └── variables.tf
│   └── modules
│       └── backend
│           ├── main.tf
│           ├── outputs.tf
│           └── variables.tf
├── modules
│   ├── acm-certificate
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── cloudfront-oac
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── route53-records
│   │   ├── main.tf
│   │   └── variables.tf
│   └── s3-private-bucket
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
└── static-website
    └── environments
        └── prd
            ├── backend.tf
            ├── main.tf
            ├── outputs.tf
            ├── providers.tf
            ├── terraform.tf
            ├── terraform.tfvars
            └── variables.tf

14 directories, 30 files
tano1:infra tano$ 

IAMロールの作成

TerraformからDNS管理アカウントを操作するので、DNS管理アカウントにポリシーを作成し、そのポリシーでprdのTerraform操作を引き受けるロールを作成
※追記: 最初はroute53の権限絞ってたけど、必要な権限が想定より多くて*にしました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "route53:*",
            "Resource": "*"
        }
    ]
}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::<prdアカウントID>:role/TerraformExecutionRole"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

寄り道(突然のgit基本操作お勉強)

・空ディレクトリを作成しただけでは変更にならない。ファイルの中身の変更だけ追跡している。

$ git pull #最新の変更を反映させる
$ git checkout -b <ブランチ名-で繋げる> #ブランチの作成
$ git branch #現在のブランチの状況を確認
$ git status #現在のgit状況を確認
$ git add <.> #gitの変更をステージングにあげる
$ git commit -m "<コミットコメント>" #gitの変更をローカルブランチへ適用させる
$ git push <リモートリポジトリ名> <ブランチ名> #変更をリモートのブランチへ適用させる

Github上でプルリクエストを作成
受けた側は確認してマージしてブランチ削除

$ git checkout <ブランチ名> #最新のブランチに変更。リモートではもうブランチが削除されてるため
$ git pull origin <ブランチ名> #最新の変更を最新を維持するブランチに変更を反映させる
$ git branch -d <ブランチ名> #不要になったブランチを削除する

寄り道終わり(突然のgit基本操作お勉強)

ディレクトリの作成

再利用したいので、infra直下にmodulesディレクトリを作成。
static-websiteが静的webサイトホスティングになる。
backend-setupの下のmodulesは専用モジュールなので、そのまま。

tano1:infra tano$ tree
.
├── backend-setup
│   ├── environments
│   │   └── prd
│   │       ├── backend.tf
│   │       ├── main.tf
│   │       ├── outputs.tf
│   │       ├── provider.tf
│   │       ├── terraform.tf
│   │       ├── terraform.tfstate
│   │       ├── terraform.tfvars
│   │       └── variables.tf
│   └── modules
│       └── backend
│           ├── main.tf
│           ├── outputs.tf
│           └── variables.tf
├── modules
│   ├── acm-certificate
│   ├── cloudfront-oac
│   ├── route53-records
│   └── s3-private-bucket
└── static-website
    └── environments
        └── prd

14 directories, 11 files
tano1:infra tano$ 

フェーズ 2: 証明書の発行とS3バケットの作成

modules/acm-certificate/ の作成

機能

DNSゾーンを取得する
ACMの証明書をリクエスト
DNS検証用のCNAMEレコードを作成
DNS検証が完了するまで待機

modules/acm-certificate/main.tf

data: 既存リソースの情報を読み取る
aws_route53_zone: Terraformのデータソースの種類。AWS Route 53のホストゾーンを取得するためのデータソース。
main: ローカル識別名。data.aws_route53_zone.main.name=ドメインnameとなる。
provider = aws.dns_master: dns_masterと言うprovidersを使用する
name = var.domain_name: ドメインnameを取得

resource: dataと違って新しいリソースを作成する。既存のものを参照するのではなくて、新規作成、更新、削除の管理対象となる。
aws_acm_certificate: リソースタイプ。証明書を作成する
provider = aws.us-east-1: CloudFrontで使うためリージョン固定
domain_name = var.domain_name: メインドメイン
subject_alternative_names = var.subject_alternative_names: 追加ドメイン
validation_method = "DNS": 検証方法を選ぶ
lifecycle { create_before_destroy = true: 新しい証明書を作成してから古いのを削除する

# DNSゾーンの情報を取得
data "aws_route53_zone" "main" {
  provider = aws.dns_master
  name     = var.domain_name
}

# ACM証明書をリクエスト (DNS検証方式)
resource "aws_acm_certificate" "main" {
  domain_name       = var.domain_name
  subject_alternative_names = var.subject_alternative_names
  validation_method = "DNS"
  tags              = var.tags

  lifecycle {
    create_before_destroy = true
  }
}

寄り道(for_eachのお勉強)

処理の流れ

Route 53にレコードを追加する処理を、専用アカウントで実施します。
その実施内容はfor_eachで与えたキー分実施します。
その要素の作成が必要で、ACM証明書からドメイン名、CNAME名、レコードタイプ、CNAME値が格納されてる配列を受け取ってるので、それをマップ形式に変換する際にキーに設定したCNAME名が重複してマップ内に存在するならfor_eachに渡します。
そしてその渡されたマップ要素分for_eachで繰り返しレコードを作成します。

# DNS検証用のCNAMEレコードを作成
resource "aws_route53_record" "validation" { #レコード作成
  provider = aws.dns_master #provider指定
  for_each = { #要素分繰り返しレコード作成します
    for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {
# ACM証明書からの配列をdvoへ変数として格納して反復処理する:生成するのがマップ形式で、キー
#この時のマップに格納するときにキーの重複を参照して、重複があれば上書き
      name   = dvo.resource_record_name #マップ形式の値
      record = dvo.resource_record_value #マップ形式の値
      type   = dvo.resource_record_type #マップ形式の値
    }
  }

以下がACMから渡される値
1ブロックで1要素

aws_acm_certificate.main.domain_validation_options = [
  {#ここから
    domain_name            = "tanoyuusuke.com"
    resource_record_name   = "_xxxx.tanoyuusuke.com."
    resource_record_type   = "CNAME"
    resource_record_value  = "_token.acm-validations.aws."
  },#ここまでで1要素
  {
    domain_name            = "*.tanoyuusuke.com"
    resource_record_name   = "_xxxx.tanoyuusuke.com."
    resource_record_type   = "CNAME"
    resource_record_value  = "_token.acm-validations.aws."
  }
]

これを変数としてdvoに格納し、マップを作成する
それを要素ごとに繰り返しなので、下記の状態からもう一度繰り返すが、キーであるdvo.resource_record_nameと同じキーが存在するので上書きして1ブロックのマップが完成する。
そのマップのキー分for_eachが繰り返して、レコード作成を実行する(今回は1回)

{
  "_xxxx.tanoyuusuke.com." = {
    name   = "_xxxx.tanoyuusuke.com."
    record = "_token.acm-validations.aws."
    type   = "CNAME"
  }

寄り道終わり(for_eachのお勉強)

aws_route53_record: Route 53でレコードを操作する
provider = aws.dns_master: DNS専用アカウントを指定
for_each: 同じリソースを複数作成するためのTerraformのメタ引数
メタ引数: 通常の引数は、ami=ami-123456のようなAWS CLIの形で指定。メタ引数はTerraform制御のためのもの。下記のようにTerraform内の独自制御のもの。
主要なメタ引数一覧(生成AI)

メタ引数 用途 リソース モジュール
count 指定数のリソース作成
for_each マップ/セットから複数作成
provider 使用するプロバイダー指定
depends_on 明示的な依存関係
lifecycle ライフサイクル制御
provisioner 作成後の処理(非推奨)

for: 繰り返しの文を書きますとの宣言みたいなもの。for構文として書き方が決まっている
dvo: ループの変数名。各要素を一時的に入れる箱の名前
in: 次のリストやマップの中から1つずつ取り出す
aws_acm_certificate.main.domain_validation_options:
aws_acm_certificate:リソースタイプ
main:自分が付けた識別名、そのモジュール内のみに影響する
domain_validation_options:acm証明書の検証CNAMEなど。下記のような形

aws_acm_certificate.main.domain_validation_options = [
  {
    domain_name            = "tanoyuusuke.com"
    resource_record_name   = "_xxxx.tanoyuusuke.com."
    resource_record_type   = "CNAME"
    resource_record_value  = "_token.acm-validations.aws."
  },
  {
    domain_name            = "*.tanoyuusuke.com"
    resource_record_name   = "_xxxx.tanoyuusuke.com."
    resource_record_type   = "CNAME"
    resource_record_value  = "_token.acm-validations.aws."
  }
]

dvo.domain_name: dvoに格納された上記のdomain_nameのこと
マップ包括表記: 下記のような形でマップを作成しますとのこと、キーと{値}で構成されて重複のキーがあればマップ構成の際に上書きされる。

dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }

機能: ACMから検証CNAMEなどの値を取得して、重複無しでそのレコードを作成する

# DNS検証用のCNAMEレコードを作成
resource "aws_route53_record" "validation" {
  provider = aws.dns_master
  for_each = {
    for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

属性: 下記の記載のようなものを一まとめに属性と呼ぶ。aws_route53_recordで設定出来るもの。今回の場合はレコード登録際の話。
allow_overwrite = true: 同名、同タイプのレコードがすでに存在していても上書きをすると言う意味
name = each.key: 作成するレコード名。for_eachの時のキーを指定。(今回はドメイン名)
records = [each.value.record]: for_eachの時の値オブジェクト全般の中のrecordの値をレコード作成の値に使う
**ttl = 60:**TTL(キャッシュ寿命)。DNSキャッシュがこのレコードを何秒間保持するかの秒数
type = each.value.type: for_eachの時の値オブジェクト全般の中のtypeの値をレコード作成のレコードタイプに使う
zone_id = data.aws_route53_zone.main.zone_id: 登録先のホストゾーンを指定する

  allow_overwrite = true
  name            = each.key
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.main.zone_id
}

aws_acm_certificate_validation: DNS検証の完了を待つためのTerraformリソース
certificate_arn = aws_acm_certificate.main.arn: どの証明書を検証するかを指定。上記で作成しているものを参照する。
aws_route53_record.validation: name、record、typeを指定したのにプラスしてTerraformが自動で作成するのもがリソースIDと完全修飾ドメイン名で、それらのプロパティも含んでる。
forの構文:

claude
[for <変数> in <コレクション> : <出力式>]
#                               ↑
#                          コロンの後が「何を取り出すか」

: record.fqdn: for文の出力部分。recordは変数名、fqdnはプロティ名。recordの中のfqdnを取得すると言う意味
for record in aws_route53_record.validation : record.fqdn: aws_route53_record.validationrecordと言う変数に入れて、その中の各要素の数だけ繰り返しfqdnvalidation_record_fqdnsへ代入する
[]: リストって意味、{}はマップ
リストとマップの違い: 分からない。一応生成AIの説明をメモ
2️⃣ 特徴の比較(Claude)

特徴 リスト マップ
順序 ✅ 順序を保持 ❌ 順序は保証されない
重複 ✅ 同じ値OK ❌ キーの重複不可
アクセス 数字(インデックス) 文字列(キー)
検索速度 遅い(全要素チェック) 速い(キー直接アクセス)
用途 同じ種類のデータ集合 名前付きデータの管理
# DNS検証が完了するまで待機
resource "aws_acm_certificate_validation" "main" {
  certificate_arn = aws_acm_certificate.main.arn
  validation_record_fqdns = [
    for record in aws_route53_record.validation : record.fqdn
  ]
}

寄り道(権威DNSはRoute 53、検証用CNAMEはレジストラ側にあるままなのを発見)

前回作成したときにブログを記録として作成しててよかった
DNS検証を先に行なって、そのあとにネームサーバーをRoute 53に切り替えたのでCNAMEがレジストラ側で作成されたままになっている
https://zenn.dev/tanoyusuke/articles/02bd90438ad7d0#webサイト公開
次回の証明書更新の際にDNS検証が出来なくなる

ACMで発行されているCNAMEを確認

ACMで証明書を確認すると、ドメインの欄があって本来はそこのRoute 53でレコードを作成を押すと一発で作成出来ると思われるがグレーアウトになっててドメインを選択出来ない。

なのでCNAME名``CNAME値``レコードタイプを確認する
サブドメインもある場合でも値などが同じならレコード作成は1つでOK。

Route 53でCNAMEレコード作成

レコード作成から下記を入力しレコード作成
レコード名=CNAME名<.ドメイン名を抜いたもの>
レコードタイプ=レコードタイプ
=CNAME値

検証が成功するかはどうするの?

検証が成功するかどうかは実際に更新の時にならないと分からないが、CNAMEレコードとしてきちんと登録出来たかどうかをコマンドで確認出来る

tano1: tano$ dig +short _dca204859df78ca393858c4c8707c43c.tanoyuusuke.com CNAME
_68748e600a040003d680c5e0b4411482.xlfgrmvvlj.acm-validations.aws.
tano1: tano$ dig +short tanoyuusuke.com

寄り道終わり(権威DNSはRoute 53、検証用CNAMEはレジストラ側にあるままなのを発見)

modules/acm-certificate/variables.tf

variables.tf: 変数の定義。modulesのvariables.tfはprdのtfvarsの値が入るイメージ
subject_alternative_names: 証明書でカバーしたい追加のサブドメインを定義
variable "tags": 呼び出したルート側からタグが降ってくる

tano1:acm-certificate tano$ cat variables.tf 
variable "domain_name" {
  description = "証明書を発行するドメイン名"
  type        = string
}

variable "subject_alternative_names" {
  description = "証明書に含める追加のドメイン名 (SANs)"
  type        = list(string)
  default     = []
}

variable "tags" {
  description = "リソースに適用するタグ"
  type        = map(string)
  default     = {}
}
tano1:acm-certificate tano$ 

modules/acm-certificate/versions.tf

versions.tf: モジュール側の宣言情報。
configuration_aliases = [aws.dns_master]: aws.dns_masterはaliasだと伝える

tano1:acm-certificate tano$ cat versions.tf
# versions.tf(宣言あり)
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = ">= 5.0"
      configuration_aliases = [aws.dns_master]
    }
  }
}
tano1:acm-certificate tano$ 

modules/acm-certificate/outputs.tf

outputs.tf: modules呼び出し側から参照する情報を出力する。もしCloudFrontなどで証明書のARNが必要であれば出力しとく必要がある。
certificate_arn: 識別名。以下使用例。

resource "aws_cloudfront_distribution" "cdn" {
  viewer_certificate {
    acm_certificate_arn = module.acm_cert.certificate_arn #←これ
    ssl_support_method  = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
}
tano1:acm-certificate tano$ cat outputs.tf
output "certificate_arn" {
  description = "発行されたACM証明書のARN"
  value       = aws_acm_certificate.main.arn
}
tano1:acm-certificate tano$ 

modules/s3-private-bucket/ の作成

パブリックアクセスブロックなS3バケットの作成

modules/s3-private-bucket/main.tf

aws_s3_bucket: バケットを作成
bucket = aws_s3_bucket.main.id: どのバケットに適用するか設定
versioning_configuration: バージョニングの詳細設定
? "Enabled" : "Suspended": 条件式 ? 真の場合の値 : 偽の場合の値
id = "expire-old-versions": ルールの識別名
status = "Enabled": ルールを有効にする
filter {}: ライフサイクルルールにフィルターを指定しないと警告となる
noncurrent_version_expiration: 古いバージョンを削除する。何日で削除するか設定出来る。
abort_incomplete_multipart_upload: 不完全なマルチアップロードを削除
aws_s3_bucket_public_access_block: パブリックアクセスブロック設定
aws_s3_bucket_server_side_encryption_configuration: サーバーサイド暗号化設定
AES256: SSE-S3暗号化

tano1:s3-private-bucket tano$ cat main.tf
resource "aws_s3_bucket" "main" {
  bucket = var.bucket_name
  tags   = var.tags
}

# バージョニング設定
resource "aws_s3_bucket_versioning" "main" {
  bucket = aws_s3_bucket.main.id
  versioning_configuration {
    status = var.enable_versioning ? "Enabled" : "Suspended"
  }
}

# ライフサイクルポリシー(常に作成)
resource "aws_s3_bucket_lifecycle_configuration" "main" {
  bucket = aws_s3_bucket.main.id 

  rule {
    id     = "expire-old-versions"
    status = "Enabled"

    # 追加:バケット全体を対象にする
    filter {}

    # 古いバージョンの削除(バージョニング停止後も有効)
    noncurrent_version_expiration {
      noncurrent_days = var.noncurrent_version_expiration_days
    }
    
    # 不完全なマルチパートアップロードも削除
    abort_incomplete_multipart_upload {
      days_after_initiation = 7
    }
  }
}

resource "aws_s3_bucket_public_access_block" "main" {
  bucket                  = aws_s3_bucket.main.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
  bucket = aws_s3_bucket.main.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}
tano1:s3-private-bucket tano$ 

modules/s3-private-bucket/variables.tf

variables.tf: main.tfで使う変数を定義する

tano1:s3-private-bucket tano$ cat variables.tf
variable "bucket_name" {
  description = "作成するS3バケットの名前"
  type        = string
}

variable "tags" {
  description = "リソースに適用するタグ"
  type        = map(string)
  default     = {}
}

variable "enable_versioning" {
  description = "バージョニングを有効にする場合はtrue"
  type        = bool
  default     = true # デフォルトで有効にする
}

variable "noncurrent_version_expiration_days" {
  description = "古いバージョンを自動削除するまでの日数"
  type        = number
  default     = 90 # デフォルトは90日
}
tano1:s3-private-bucket tano$ 

modules/s3-private-bucket/outputs.tf

outputs.tf: モジュール呼び出し側から参照する情報を出力する。

tano1:s3-private-bucket tano$ cat outputs.tf
output "bucket_id" {
  description = "S3バケットのID (バケット名)"
  value       = aws_s3_bucket.main.id
}

output "bucket_arn" {
  description = "S3バケットのARN"
  value       = aws_s3_bucket.main.arn
}

output "bucket_regional_domain_name" {
  description = "S3バケットのリージョナルドメイン名"
  value       = aws_s3_bucket.main.bucket_regional_domain_name
}tano1:s3-private-bucket tano$ 

infra/static-website/environments/prd/の設定

パブリックアクセスブロックのS3バケットと、ACM証明書周りのドメイン設定をまずは設定

static-website/environments/prd/providers.tf

alias = "us-east-1": 呼び出すリソースによってリージョンやアカウント変えるための設定

tano1:prd tano$ cat providers.tf
# デフォルトプロバイダ (prdアカウントのap-northeast-1)
provider "aws" {
  region = "ap-northeast-1"
  assume_role {
    role_arn     = "arn:aws:iam::002540791269:role/TerraformExecutionRole"
    session_name = "tf-session-static-website-prd"
  }
}

# ACM証明書用のプロバイダ (prdアカウントのus-east-1)
provider "aws" {
  alias  = "us-east-1"
  region = "us-east-1"
  assume_role {
    role_arn     = "Terraformからの操作用のロールARN"
    session_name = "tf-session-static-website-prd-acm"
  }
}

# DNS操作用のプロバイダ (DNS管理アカウントのRoute53)
provider "aws" {
  alias  = "dns_master"
  region = "ap-northeast-1" # Route53はグローバルなのでリージョンはどこでもOK
  assume_role {
    role_arn     = "Terraformからの操作用のロールARN"
    session_name = "tf-session-dns-cross-account"
  }
}
tano1:prd tano$ 

static-website/environments/prd/terraform.tf

TerraformのバージョンとTerraformAWSプロバイダーを指定:

tano1:prd tano$ cat terraform.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
tano1:prd tano$ 

static-website/environments/prd/backend.tf

backend.tf: backend-setupで使用したリソースを指定

tano1:prd tano$ cat backend.tf
terraform {
  backend "s3" {
    # backend-setupで作成したS3バケット
    bucket = "<バケット名>"

    # このスタック専用のStateファイルのパス
    key = "static-website/terraform.tfstate"

    region         = "ap-northeast-1"
    dynamodb_table = "0603game-prd-tfstate-lock"
    encrypt        = true
    role_arn       = "<Terraform操作用のARN>"
  }
}
tano1:prd tano$ 

static-website/environments/prd/variables.tf

variables.tf: 変数を定義する

# 追記 2025/08/20
tano1:prd tano$ cat variables.tf
variable "project" {
  description = "プロジェクト名"
  type        = string
}

variable "environment" {
  description = "環境名(dev/stg/prd)"
  type        = string
}

# ここまで 2025/08/20

variable "domain_name" {
  description = "ウェブサイトのドメイン名"
  type        = string
}

variable "aws_account_id" {
  description = "この環境のAWSアカウントID"
  type        = string
}

variable "common_tags" {
  description = "リソースに適用する共通のタグ"
  type        = map(string)
  default     = {}
}
tano1:prd tano$

static-website/environments/prd/terraform.tfvars

terraform.tfvars: 定義した変数に代入する値

tano1:prd tano$ cat terraform.tfvars
# 追記 2025/08/20
project        = "0603game"
environment    = "prd"
# ここまで 2025/08/20
domain_name    = "tanoyuusuke.com"
aws_account_id = "002540791269"

common_tags = {
  Project     = "0603game"
  Environment = "prd"
  ManagedBy   = "Terraform"
  Owner       = "tano" # 追記 2025/08/20
}
tano1:prd tano$ 

static-website/environments/prd/main.tf

module "acm": モジュールの宣言acmは識別名
source = "../../../modules/acm-certificate": モジュールの場所を指定
aws = aws.us-east-1: aliasをus-east-1にしているので、us-east-1のプロバイダーを使用
bucket_name = "tano-0603game-bucket-${var.aws_account_id}": 変数を用いてバケット名を決めている。

tano1:prd tano$ cat main.tf
# ACM証明書モジュールを呼び出し
module "acm" {
  source = "../../../modules/acm-certificate"
  # providerをエイリアスで指定
  providers = {
    aws           = aws.us-east-1 # このモジュール内のデフォルトはus-east-1
    aws.dns_master = aws.dns_master # dns_masterプロバイダを渡す
  }

  domain_name               = var.domain_name
  subject_alternative_names = ["www.${var.domain_name}"]
  tags                      = var.common_tags
}

# S3バケットモジュールを呼び出し
module "s3_website" {
  source = "../../../modules/s3-private-bucket"

  bucket_name = "tano-0603game-bucket-${var.aws_account_id}" # 重複しないようにアカウントIDを付与
  tags        = var.common_tags
}
tano1:prd tano$ 

static-website/environments/prd/outputs.tf

outputs.tf: 外部や他スタックなどで参照するために出力する。

tano1:prd tano$ cat outputs.tf
output "acm_certificate_arn" {
  description = "発行されたACM証明書のARN"
  value       = module.acm.certificate_arn
}

output "s3_website_bucket_id" {
  description = "作成されたS3バケットのID"
  value       = module.s3_website.bucket_id
}
tano1:prd tano$ 

Terraformの実行

AWS CLIの認証確認

このコマンドで現在のAWS CLIの認証情報が出力される

$ aws sts get-caller-identity

terraform init

新しいバックエンド設定、モジュール設定などを読み込む

terraform plan

実行計画生成。以下今回出たエラー
aliasで記載していたのにaws.us-east-1を直接指定していた

│ Error: Provider configuration not present
│ 
│ To work with module.acm.aws_acm_certificate.main its original provider configuration at
│ module.acm.provider["registry.terraform.io/hashicorp/aws"].us-east-1 is required, but it has been
│ removed. This occurs when a provider configuration is removed while objects created by that provider
│ still exist in the state. Re-add the provider configuration to destroy
│ module.acm.aws_acm_certificate.main, after which you can remove the provider configuration again.
╵
╷
│ Error: Provider configuration not present
│ 
│ To work with module.acm.aws_acm_certificate_validation.main its original provider configuration at
│ module.acm.provider["registry.terraform.io/hashicorp/aws"].us-east-1 is required, but it has been
│ removed. This occurs when a provider configuration is removed while objects created by that provider
│ still exist in the state. Re-add the provider configuration to destroy
│ module.acm.aws_acm_certificate_validation.main, after which you can remove the provider configuration
│ again.
╵

ルールにfilterprefixを指定してとの警告: 記載して解消
AssumeRole出来ないとのエラー: DNS管理アカウントのロールを後から追加したので、踏み台ロールの許可ポリシーにそのロールが記載漏れ。
またprdアカウントからDNS操作すると勘違いし、そのロールをDNS管理ロールで信頼されたエンティティで許可していた。踏み台ロールからに修正。

╷
│ Warning: Invalid Attribute Combination
│ 
│   with module.s3_website.aws_s3_bucket_lifecycle_configuration.main,
│   on ../../../modules/s3-private-bucket/main.tf line 15, in resource "aws_s3_bucket_lifecycle_configuration" "main":
│   15: resource "aws_s3_bucket_lifecycle_configuration" "main" {
│ 
│ No attribute specified when one (and only one) of [rule[0].filter,rule[0].prefix] is required
│ 
│ This will be an error in a future version of the provider
│ 
│ (and one more similar warning elsewhere)
╵
╷
│ Error: Cannot assume IAM Role
│ 
│   with provider["registry.terraform.io/hashicorp/aws"].dns_master,
│   on providers.tf line 21, in provider "aws":
│   21: provider "aws" {
│ 
│ IAM Role (arn:aws:iam::<DNS管理アカウントID>:role/Route53CrossAccountManagerRole) cannot be assumed.
│ 
│ There are a number of possible causes of this - the most common are:
│   * The credentials used in order to assume the role are invalid
│   * The credentials do not have appropriate permission to assume the role
│   * The role ARN is not valid
│ 
│ Error: operation error STS: AssumeRole, https response error StatusCode: 403, RequestID:
│ 16bf10ef-bbf3-440c-b74a-069d806b5e4b, api error AccessDenied: User:
│ arn:aws:sts::<踏み台アカウントID>:assumed-role/AWSReservedSSO_0603game-TerraformOperator_c6f75dbe8448194d/tano-sso-user
│ is not authorized to perform: sts:AssumeRole on resource:
│ arn:aws:iam::<DNS管理アカウントID>:role/Route53CrossAccountManagerRole
│ 
╵

Route 53を操作する権限が足りなくてエラー: 想定の権限では、足りなかった。権限を増やすことにより解消。

╷
│ Error: reading Route53 Hosted Zone (<ホストゾーンID>): operation error Route 53: GetHostedZone, https response error StatusCode: 403, RequestID: 055a21ec-360d-406d-8e26-f2aa7b2dbaa9, api error AccessDenied: User: arn:aws:sts::<DNS管理アカウントID>:assumed-role/Route53CrossAccountManagerRole/tf-session-dns-cross-account is not authorized to perform: route53:GetHostedZone on resource: arn:aws:route53:::hostedzone/<ホストゾーンID> because no identity-based policy allows the route53:GetHostedZone action
│ 
│   with module.acm.data.aws_route53_zone.main,
│   on ../../../modules/acm-certificate/main.tf line 2, in data "aws_route53_zone" "main":
│    2: data "aws_route53_zone" "main" {
│ 
╵

CNAME名がキーだとリソースが作成されないと確定出来ずエラー: キーをドメイン名に変更で解消

╷
│ Error: Invalid for_each argument
│ 
│   on ../../../modules/acm-certificate/main.tf line 22, in resource "aws_route53_record" "validation":
│   22:   for_each = {23:     for dvo in aws_acm_certificate.main.domain_validation_options : dvo.resource_record_name => {24:       name   = dvo.resource_record_name
│   25:       record = dvo.resource_record_value
│   26:       type   = dvo.resource_record_type
│   27:     }28:   }
│     ├────────────────
│     │ aws_acm_certificate.main.domain_validation_options is set of object with 2 elements
│ 
│ The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the
│ full set of keys that will identify the instances of this resource.
│ 
│ When working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results
│ only in the map values.
│ 
│ Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply
│ a second time to fully converge.
╵

terraform apply

リソースを作成する

Gitを忘れずに

terraform applyしたらstateファイルが変更されると思ったら、それはローカルバックエンド設定をしてたからだったので、リモートバックエンドならGit操作しなくても大丈夫。

フェーズ 3: CloudFrontディストリビューションのデプロイ

modules/cloudfront-oacの作成

modules/cloudfront-oac/variables.tf

呼び出し元から変数の値を受け取る変数を定義する

tano1:cloudfront-oac tano$ cat variables.tf
# infra/modules/cloudfront-oac/variables.tf

variable "s3_origin_domain_name" {
  description = "オリジンとなるS3バケットのリージョナルドメイン名"
  type        = string
}

variable "s3_bucket_id" {
  description = "S3バケットID(バケット名)"
  type        = string
}

variable "acm_certificate_arn" {
  description = "使用するACM証明書のARN (us-east-1で作成されたもの)"
  type        = string
}

variable "domain_aliases" {
  description = "ディストリビューションに設定するドメイン名のリスト (CNAME)"
  type        = list(string)
  default     = []
}

variable "tags" {
  description = "リソースに適用するタグ"
  type        = map(string)
  default     = {}
}
tano1:cloudfront-oac tano$ 

modules/cloudfront-oac/main.tf

aws_cloudfront_origin_access_control
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_control
aws_cloudfront_distribution
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution
aws_cloudfront_origin_access_control: オリジンアクセスコントロールを作成
signing_behavior = "always": 署名リクエスト
signing_protocol = "sigv4": 署名方式
default_root_object = "index.html": ルートURLアクセス時に表示するファイル
aliases = var.domain_aliases: ドメイン設定
origin_access_control_id = aws_cloudfront_origin_access_control.main.id: ディストリビューションとOACを結ぶ設定
allowed_methods = ["GET", "HEAD", "OPTIONS"]: 許可されたHTTPメソッド設定
cached_methods = ["GET", "HEAD"]: キャッシュするメソッド設定
target_origin_id = "S3-${var.s3_origin_domain_name}": ターゲットのオリジン
viewer_protocol_policy = "redirect-to-https": HTTPはHTTPSへリダイレクトの設定
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6: AWSが推奨する一般的なキャッシュポリシー。キャッシュ最適化。

tano1:cloudfront-oac tano$ cat main.tf
# S3オリジン用のOrigin Access Control (OAC) を作成
# これがCloudFrontからS3へ安全にアクセスするための「鍵」の役割を果たします
resource "aws_cloudfront_origin_access_control" "main" {
  name                              = "oac-${var.s3_bucket_id}"
  description                       = "OAC for S3 bucket"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

# CloudFrontディストリビューションを作成
resource "aws_cloudfront_distribution" "main" {
  enabled             = true
  default_root_object = "index.html" # ルートURLへのアクセス時に表示するファイル
  aliases             = var.domain_aliases   # "tanoyuusuke.com" などを設定
  is_ipv6_enabled = true # 追記2025/08/20

  # オリジン(配信元)の設定
  origin {
    domain_name              = var.s3_origin_domain_name
    origin_id                = "S3-${var.s3_origin_domain_name}" # オリジンのユニークなID
    origin_access_control_id = aws_cloudfront_origin_access_control.main.id
  }

  # デフォルトのキャッシュ動作設定
  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD", "OPTIONS"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "S3-${var.s3_origin_domain_name}"
    viewer_protocol_policy = "redirect-to-https" # HTTPアクセスはHTTPSにリダイレクト
    compress               = true                # コンテンツを自動で圧縮して配信を高速化

    # AWSが推奨する一般的なキャッシュ最適化ポリシーを利用
    cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" # CachingOptimized
  }

ssl_support_method = "sni-only": レガシーなブラウザは非対応
minimum_protocol_version = "TLSv1.2_2021": 一番推奨なHTTPS接続に使用するTLSプロトコル
restrictions: 制限設定の開始
geo_restriction: 地理的制限
restriction_type = "none": 地理的制限無し
price_class = "PriceClass_All": 料金によって配信出来る場所が変わる

  # SSL/TLS証明書の設定
  viewer_certificate {
    acm_certificate_arn      = var.acm_certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
  # ← ここに追加(トップレベル)
  restrictions {
    geo_restriction {
      restriction_type = "none"   # 地理制限なし。whitelist/blacklist も選べる
      locations        = []        # none の場合は空配列
    }
  }

  # 価格クラス (日本を含むアジア、北米、欧州に限定してコストを最適化)
  price_class = "PriceClass_All"

  tags = var.tags
}
tano1:cloudfront-oac tano$ 

modules/cloudfront-oac/outputs.tf

呼び出し元が参照する情報を出力する

tano1:cloudfront-oac tano$ cat outputs.tf
output "distribution_id" {
  description = "CloudFrontディストリビューションのID"
  value       = aws_cloudfront_distribution.main.id
}

output "distribution_arn" {
  description = "CloudFrontディストリビューションのARN"
  value       = aws_cloudfront_distribution.main.arn
}

output "domain_name" {
  description = "CloudFrontディストリビューションのドメイン名"
  value       = aws_cloudfront_distribution.main.domain_name
}

output "hosted_zone_id" {
  description = "CloudFrontディストリビューションのRoute53ホストゾーンID"
  value       = aws_cloudfront_distribution.main.hosted_zone_id
}
tano1:cloudfront-oac tano$ 

司令塔のprd/main.tfの作成

CloudFrontディストリビューションを作成

前回:パブリックアクセスブロックのS3バケットと、ACM証明書周りのドメイン設定をまずは設定

acm_certificate_arn = module.acm.certificate_arn: ACMのモジュールの出力を渡す。SSL/TLS証明書の設定で使用
s3_origin_domain_name = module.s3_website.bucket_regional_domain_name: OCAのNAMEに使用。オリジンの名前とIDで使用。キャッシュ動作の設定でターゲットオリジンに使用。
domain_aliases: 代替ドメイン名で使用。

prd/main.tf

# --- 以下を追記 ---
module "cloudfront_cdn" {
  source = "../../../modules/cloudfront-oac"

  # ACMモジュールの出力を入力として渡す
  acm_certificate_arn = module.acm.certificate_arn

  # S3モジュールの出力を入力として渡す
  s3_origin_domain_name = module.s3_website.bucket_regional_domain_name

  # ⭐ bucket_id を使用(既存の出力をそのまま使える!)
  s3_bucket_id = module.s3_website.bucket_id

  domain_aliases = ["tanoyuusuke.com", "www.tanoyuusuke.com"]
  tags           = var.common_tags
}tano1:prd tano$ 

prd/outputs.tf

# --- 以下を追記 ---
output "cloudfront_distribution_domain_name" {
  description = "CloudFrontディストリビューションのドメイン名"
  value       = module.cloudfront_cdn.domain_name
}
tano1:prd tano$ 

自分が書き換える箇所は、tfvarsのみの方が良いなって思って固定名が書かれてる箇所を訂正

何が正解か分からないけど、訂正しました。

# infra/static-website/environments/prd/variables.tf
+ variable "project" {
+   description = "プロジェクト名"
+   type        = string
+ }
+ 
+ variable "environment" {
+   description = "環境名(dev/stg/prd)"
+   type        = string
+ }
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
# infra/static-website/environments/prd/terraform.tfvars
+ project        = "0603game"
+ environment    = "prd"
  domain_name    = "tanoyuusuke.com"
  aws_account_id = "002540791269"

  common_tags = {
    Project     = "0603game"
    Environment = "prd"
    ManagedBy   = "Terraform"
+   Owner       = "tano"
  }
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
# infra/static-website/environments/prd/main.tf
# S3バケットモジュールを呼び出し
module "s3_website" {
  source = "../../../modules/s3-private-bucket"

+ bucket_name = "bucket-${var.project}-${var.environment}-${var.aws_account_id}"
- bucket_name = "tano-0603game-bucket-${var.aws_account_id}"
  tags        = var.common_tags
}
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
# infra/static-website/environments/prd/main.tf
  # S3モジュールの出力を入力として渡す
  s3_origin_domain_name = module.s3_website.bucket_regional_domain_name

+ domain_aliases = ["${var.domain_name}", "www.${var.domain_name}"]
- domain_aliases = ["tanoyuusuke.com", "www.tanoyuusuke.com"]
  tags           = var.common_tags
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

Terraform実行

terraform init

モジュールを新しく作成したら読み込まれてないので、読み込むためにterraform initを実行

terraform plan

モジュールを取得出来ていなくてエラー。terraform initが必要。

tano1:prd tano$ terraform plan
╷
│ Error: Module not installed
│ 
│   on main.tf line 28:
│   28: module "cloudfront_cdn" {
│ 
│ This module is not yet installed. Run "terraform init" to install all modules required by this configuration.

terraform apply

OACの名前が長いのでエラー。名前をバケット名にすることで解消。
したと思ったがバケットidの方がoutputsしていて変更が少ないのでそちらにすることに。
モジュールのvariablesでbucket.idを定義し、mainでnameをidに。prd/mainでbucket.idをモジュールで渡す設定に修正。

╷
│ Error: expected length of name to be in the range (1 - 64), got oac-for-bucket-0603game-prd-002540791269.s3.ap-northeast-1.amazonaws.com
│ 
│   with module.cloudfront_cdn.aws_cloudfront_origin_access_control.main,
│   on ../../../modules/cloudfront-oac/main.tf line 4, in resource "aws_cloudfront_origin_access_control" "main":
│    4:   name                              = "oac-for-${var.s3_origin_domain_name}"
│ 
╵
tano1:prd tano$ 

指定したaliasesがすでに別のリソースに関連付いてるためエラー。
前回ブログで書いたときのドメインをそのまま使ってたので、そちらを削除。

╷
│ Error: creating CloudFront Distribution: operation error CloudFront: CreateDistributionWithTags, https response error StatusCode: 409, RequestID: d249b6cd-fbc9-49df-bda5-d9cb700fcdf2, CNAMEAlreadyExists: One or more of the CNAMEs you provided are already associated with a different resource.
│ 
│   with module.cloudfront_cdn.aws_cloudfront_distribution.main,
│   on ../../../modules/cloudfront-oac/main.tf line 12, in resource "aws_cloudfront_distribution" "main":
│   12: resource "aws_cloudfront_distribution" "main" {
│ 
╵
tano1:prd tano$ 

フェーズ 4: 最終接続と本番リリース

route53-recordsモジュールのコード作成

modules/route53-records/variables.tf

variable "records": DNSレコードを受け取るための入力変数の定義。配列として複数のレコードを受け取り、各要素はオブジェクトで1レコード分を表す。
optional: 値がモジュール呼び出し側で指定されなくてもエラーを返さず、デフォルトで入力される。これはaliasの時は入力無しになるため。モジュール側では、aliasの時は条件分岐で無視するようになっている。

}tano1:route53-records tano$ cat variables.tf 
variable "zone_name" {
  description = "対象のRoute 53ホストゾーン名"
  type        = string
}

variable "records" {
  description = "作成するDNSレコードのリスト"
  type = list(object({
    name    = string
    type    = string
    ttl     = optional(number, 300)
    content = optional(list(string), [])
    alias = optional(object({
      name    = string
      zone_id = string
    }), null)
  }))
  default = []
}
tano1:route53-records tano$ 

modules/route53-records/main.tf

for_each = { for record in var.records : "{record.name}-{record.type}" => record: レコード名-レコードタイプをキーとしたレコード情報を繰り返しマップ作成し、その情報でキーの数だけレコードを作成。
dynamic: ブロックを作る、作らない、複数作るを制御する機能。
for_each = each.value.alias != null ? [each.value.alias] : []:
each.value.alias != null: aliasがnullでは無い。がtrue。そうでは無い(aliasがnull)がfalse
? [each.value.alias] : []: ?は真偽式で、trueなら'[each.value.alias]'を返す。falseなら[]を返す。
trueなら'[each.value.alias]'を返す。: contentを記述してるので、要素の数だけcontentが実行される。その返されたものをresource "aws_route53_recordのalias"として設定される。
ttl = each.value.alias == null ? each.value.ttl : null: 設定されてるaliasnullならtrue、そうじゃないならfalse。つまりaliasが設定されてるならnullとして値を設定しない。
ホストゾーンを参照して、レコードを複数作成する。
そのために、レコード名とレコードタイプをキーとして被りが起きないように繰り返し設定を入れる。
その繰り返し設定の中にaliasをするかどうかの分岐も入れていてそれをdynamicとfor_eachで実現している。

tano1:route53-records tano$ cat main.tf
# ホストゾーン参照。プロバイダーは専用のを用意。
data "aws_route53_zone" "main" {
  provider = aws.dns_master
  name     = var.zone_name
}
# レコード名-レコードタイプをキーとしたレコード情報を繰り返しマップ作成し、その情報でキーの数だけレコードを作成。
resource "aws_route53_record" "main" {
  provider = aws.dns_master
  # キーを name と type の組み合わせにして、必ずユニークになるように修正
  for_each = { for record in var.records : "${record.name}-${record.type}" => record }

  zone_id = data.aws_route53_zone.main.zone_id
  name    = each.value.name
  type    = each.value.type

  dynamic "alias" {
    for_each = each.value.alias != null ? [each.value.alias] : []
    content {
      name                   = alias.value.name
      zone_id                = alias.value.zone_id
      evaluate_target_health = false
    }
  }

  ttl     = each.value.alias == null ? each.value.ttl : null
  records = each.value.alias == null ? each.value.content : null
}tano1:route53-records tano$ 

# versions.tf

プロバイダーaliasをモジュール側で定義する

tano1:route53-records tano$ cat versions.tf
# versions.tf(宣言あり)
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = ">= 5.0"
      configuration_aliases = [aws.dns_master]
    }
  }
}

tano1:route53-records tano$ 

prd/main.tf) の更新

Version = "2012-10-17": AWSのポリシー言語バージョン、他に2008-10-17が存在してて旧版となっている。
Effect = "Allow": このルールは許可
Principal: 誰に。CloudFrontに
Action = "s3:GetObject": 何を。s3のオブジェクト読み取り権限
Resource: どこの?S3BucketARN。(S3BucketARNのs3:GetObjectって意味)
Condition: 条件
StringEquals: AWS:SourceArnが指定した文字列が完全に一致するかどうか
CloudFrontに対して。AWS:SourceArnが指定した文字列が完全に一致したディストリビューションのみ特定のS3BucketARNのs3:GetObjectを許可します。

# 4. S3バケットポリシーの設定
# CloudFrontが作成された後に、その情報を使ってポリシーを適用
resource "aws_s3_bucket_policy" "website_bucket_policy" {
  bucket = module.s3_website.bucket_id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect    = "Allow",
        Principal = { Service = "cloudfront.amazonaws.com" },
        Action    = "s3:GetObject",
        Resource  = "${module.s3_website.bucket_arn}/*",
        Condition = {
          StringEquals = {
            # CloudFrontモジュールのARNを参照してアクセスを制限
            "AWS:SourceArn" = module.cloudfront_cdn.distribution_arn
          }
        }
      }
    ]
  })
}

records: モジュール側のvariablesで定義されている変数。

# 5. Route 53 レコードの作成 (本番リリース)
module "dns" {
  source = "../../../modules/route53-records"
  providers = {
    aws.dns_master = aws.dns_master
  }

  zone_name = var.domain_name
  records = [
    # --- Aレコード (IPv4) ---
    {
      name = var.domain_name
      type = "A"
      alias = {
        name    = module.cloudfront_cdn.domain_name
        zone_id = module.cloudfront_cdn.hosted_zone_id
      }
    },
    {
      name = "www.${var.domain_name}"
      type = "A"
      alias = {
        name    = module.cloudfront_cdn.domain_name
        zone_id = module.cloudfront_cdn.hosted_zone_id
      }
    },
    # --- AAAAレコード (IPv6) ---
    {
      name = var.domain_name
      type = "AAAA"
      alias = {
        name    = module.cloudfront_cdn.domain_name
        zone_id = module.cloudfront_cdn.hosted_zone_id
      }
    },
    {
      name = "www.${var.domain_name}"
      type = "AAAA"
      alias = {
        name    = module.cloudfront_cdn.domain_name
        zone_id = module.cloudfront_cdn.hosted_zone_id
      }
    }
  ]
tano1:prd tano$ 

Terraform 実行

terraform init

aliasのモジュール側に定義がなかったため警告。versions.tfを追加し解消。

│ Warning: Reference to undefined provider
│ 
│   on main.tf line 76, in module "dns":
│   76:     aws.dns_master = aws.dns_master
│ 
│ There is no explicit declaration for local provider name "aws.dns_master" in module.dns, so Terraform is assuming
│ you mean to pass a configuration for "hashicorp/aws".
│ 
│ If you also control the child module, add a required_providers entry named "aws.dns_master" with the source
│ address "hashicorp/aws".
│ 
│ (and one more similar warning elsewhere)

terraform plan

問題無し

terraform apply

Route53にすでにレコード名と一致するものが存在するエラー。前回ブログで作成してたものが残ってたので削除して解消。

╷
│ Error: creating Route53 Record: operation error Route 53: ChangeResourceRecordSets, https response error StatusCode: 400, RequestID: b7032817-50c1-4aec-b00a-70a2999b355d, InvalidChangeBatch: [Tried to create resource record set [name='www.tanoyuusuke.com.', type='A'] but it already exists]
│ 
│   with module.dns.aws_route53_record.main["www.tanoyuusuke.com-A"],
│   on ../../../modules/route53-records/main.tf line 6, in resource "aws_route53_record" "main":
│    6: resource "aws_route53_record" "main" {
│ 
╵
╷
│ Error: creating Route53 Record: operation error Route 53: ChangeResourceRecordSets, https response error StatusCode: 400, RequestID: 9adc302d-75d3-45d5-9b60-237fac79744a, InvalidChangeBatch: [Tried to create resource record set [name='tanoyuusuke.com.', type='A'] but it already exists]
│ 
│   with module.dns.aws_route53_record.main["tanoyuusuke.com-A"],
│   on ../../../modules/route53-records/main.tf line 6, in resource "aws_route53_record" "main":
│    6: resource "aws_route53_record" "main" {
│ 
╵
tano1:prd tano$ 

git操作(ファイル編集終わり)

一区切りのファイル編集が終わったので、その編集した変更をリモートのメインブランチへ反映させるため操作を行う

$ git status #現在のgit状況を確認
$ git branch #現在のブランチの状況を確認
$ cd <プロジェクトのルートディレクトリ>
$ git add . #gitの変更をステージングにあげる
$ git status #現在のgit状況を確認
$ git commit -m "prod-static-website-hosting" #gitの変更をローカルブランチへ適用させる
$ git push origin feature/prod-static-website-hosting #変更をリモートのブランチへ適用させる
# ここからGithubなどのリモート側の操作
# リモートでmainブランチとfeature/prod-static-website-hostingブランチがあるので、変更をmainブランチへ反映させるためにプルリクエストを行う
# プルリクエストを受けた側(自学習なら自分)は変更差分を見て問題無いか確認して、問題なければマージしてfeature/prod-static-website-hostingブランチを削除する
# Githubなどのリモート側の操作はここまで
$ git branch #現在のブランチの状況を確認
$ git checkout main #最新のブランチに変更。リモートではもうブランチが削除されてる
$ git branch #現在のブランチの状況を確認
$ git pull origin main # リモートで反映させた変更が、ローカルにも反映される
$ git branch -d feature/prod-static-website-hosting #不要になったブランチを削除する
$ git branch #現在のブランチの状況を確認

構成図起こし

awslabs/diagram-as-code

github
https://github.com/awslabs/diagram-as-code

インフラ構成図をコードで書くけどもなかなか簡単には難しい。。。


AWS構成図作成の悩みを一掃!Diagram as Codeで始める“コードで描く”インフラ設計
https://qiita.com/k_adachi_01/items/ccfbf6952d7ee252bfb0
[ブースレポート]AWS構成図とAWS CloudFormationテンプレートを自動生成!Diagram-as-Codeのデモをブースで触ってみた #AWSSummit
https://dev.classmethod.jp/articles/aws-summit-2025-diagram-as-code/

Discussion