0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【S3 + CloudFront + GitHub Actions】構成をTerraformでコード化する

0
Last updated at Posted at 2026-05-23

はじめに

今回は前回AWS CDKを使ってコード化した内容を、そのままTerraformに置き換えてみました。

Terraformの基礎に関しては、こちらの記事をぜひご参照ください。

完成したTerraformフォルダ構成

プロジェクトルートにインフラのデプロイ用フォルダterraform/を作成します。

terraform/
├── .gitignore
├── .terraform-version ①               # tfenv 用(Terraform 1.11.4)
├── modules/
│   ├── github-oidc-provider/          # アカウントベース singleton(CDK BaseStack 相当)
│   │   ├── main.tf ②
│   │   ├── variables.tf ③
│   │   ├── outputs.tf ④
│   │   └── versions.tf ⑤
│   ├── static-site/                   # S3 + CloudFront(OAC)(CDK FrontendStack 相当)
│   │   ├── main.tf ⑥
│   │   ├── variables.tf ⑦
│   │   ├── outputs.tf ⑧
│   │   └── versions.tf ⑨
│   └── github-oidc-role/              # GitHub Actions Deploy Role(CDK GithubOidcStack 相当)
│       ├── main.tf ⑩
│       ├── variables.tf ⑪
│       ├── outputs.tf ⑫
│       └── versions.tf ⑬
└── envs/
    ├── dev/
    │   ├── backend.tf ⑭                # S3 backend 設定(dev アカウント側)
    │   ├── providers.tf ⑮              # provider "aws" + アカウントガード data source
    │   ├── versions.tf ⑯               # terraform { required_version / required_providers }
    │   ├── main.tf ⑰                   # module 呼び出しのみ
    │   ├── variables.tf ⑱
    │   ├── outputs.tf ⑲
    │   └── terraform.tfvars ⑳          # dev 固有値
    ├── stg/
    │   └── ... (同上)
    └── prod/
        └── ... (同上、force_destroy=false を validation で強制)

各ファイル内容

※前回の記事で作成した内容を、AIを使用してコード化しています(微修正/動作確認済み)
※ベストプラクティスというわけではないため、あくまで参考にしていただけると幸いです
※各ファイルの詳細な説明は省略いたしますが、気になった方はAIに投げてみてください
※機密情報を扱う際は、別途.envSecretManagerなどと連携する必要があります

① terraform/.terraform-version

「このディレクトリ」で使う Terraformコマンドのバージョンを固定する宣言ファイルです。
セットアップ(後述)で使用するtfenv(Terraform 用バージョンマネージャ、Node.js でいう nvm相当)が読み込む規約ファイルです。

terraform/.terraform-version
1.11.4

② terraform/modules/github-oidc-provider/main.tf

GitHub ActionsのOIDCトークンをAWSで受け入れるための「OIDC Provider」をAWS IAMに登録するTerraformモジュールです。CDKのcdk/lib/stacks/base-stack.tsと1:1で対応しています。

terraform/modules/github-oidc-provider/main.tf
# =============================================================================
# GitHub Actions 用 OIDC Provider
#
# 同一 URL(token.actions.githubusercontent.com)の OIDC Provider は
# AWS アカウントあたり 1 つしか作れない singleton リソース。
# 本プロジェクトは「マルチアカウント前提(dev/stg/prod が別アカウント)」なので、
# 各アカウントごとに 1 度だけ作成すれば良い。
#
# AWS Provider 5.x 以降は thumbprint_list を省略しても自動で取得される。
# https://github.com/hashicorp/terraform-provider-aws/issues/32976
# =============================================================================

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

  tags = {
    Name = "${var.prefix}-${var.env_name}-github-oidc-provider"
  }
}

③ terraform/modules/github-oidc-provider/variables.tf

このモジュールが「外から受け取る入力値」を宣言するファイルです。

terraform/modules/github-oidc-provider/variables.tf
variable "prefix" {
  description = "リソース名のプレフィックス(タグ補助用)"
  type        = string
}

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

④ terraform/modules/github-oidc-provider/outputs.tf

このモジュールが「外に返す値」を宣言するファイルです。
上記のvariables.tfがモジュールの入力(引数)なら、outputs.tfは戻り値であり、関数で言うreturnに相当します。

terraform/modules/github-oidc-provider/outputs.tf
output "provider_arn" {
  description = "GitHub Actions OIDC Provider の ARN(同一アカウント内の Deploy Role が trust する)"
  value       = aws_iam_openid_connect_provider.github.arn
}

⑤ terraform/modules/github-oidc-provider/versions.tf

このモジュールを動かすために必要な「Terraform本体のバージョン」と「Providerのバージョン」の制約を宣言するファイルです。

terraform/modules/github-oidc-provider/versions.tf
terraform {
  required_version = ">= 1.11.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.80.0, < 7.0.0"
    }
  }
}

⑥ terraform/modules/static-site/main.tf

terraform/modules/static-site/main.tf
# =============================================================================
# S3(プライベート)+ CloudFront(OAC)の静的サイト構成。SPA ホスティングに最適。
#
# - Bucket: BlockPublicAccess、SSE-S3、BucketOwnerEnforced、TLS 強制
# - Distribution: Managed CachingOptimized、redirect-to-https、HTTP/2、IPv6
# - SPA エラーマッピング: 403/404 → /index.html(200 で応答)
# =============================================================================

data "aws_caller_identity" "current" {}

locals {
  bucket_name = "${var.prefix}-${var.env_name}-static-${data.aws_caller_identity.current.account_id}"
  oac_name    = "${var.prefix}-${var.env_name}-oac"
}

# -----------------------------------------------------------------------------
# S3 バケット本体
# -----------------------------------------------------------------------------

resource "aws_s3_bucket" "site" {
  bucket        = local.bucket_name
  force_destroy = var.force_destroy

  # prod では terraform destroy を構造的に拒否したい。
  # ただし lifecycle.prevent_destroy は static にしか書けないため、
  # ここでは設定せず、運用ガード(README / force_destroy=false)に委ねる。
}

# Public Access を完全にブロック。
resource "aws_s3_bucket_public_access_block" "site" {
  bucket = aws_s3_bucket.site.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Object Ownership: BucketOwnerEnforced(ACL 無効化)。
resource "aws_s3_bucket_ownership_controls" "site" {
  bucket = aws_s3_bucket.site.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

# SSE-S3(AES256)でサーバ側暗号化。
resource "aws_s3_bucket_server_side_encryption_configuration" "site" {
  bucket = aws_s3_bucket.site.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
    # bucket_key_enabled は SSE-KMS 向けの KMS 呼び出し削減機能であり、
    # SSE-S3 (AES256) では意味を持たないためここでは指定しない。
  }
}

# `aws s3 sync` 失敗時に残る未完了 multipart upload を自動回収する。
# 設定しないと孤児パートが残り続けてストレージ課金が止まらない。
resource "aws_s3_bucket_lifecycle_configuration" "site" {
  bucket = aws_s3_bucket.site.id

  rule {
    id     = "AbortIncompleteMultipartUploads"
    status = "Enabled"

    filter {}

    abort_incomplete_multipart_upload {
      days_after_initiation = var.abort_incomplete_multipart_upload_days
    }
  }
}

# -----------------------------------------------------------------------------
# CloudFront Origin Access Control(OAC)
# OAI ではなく OAC を使う(OAI は非推奨)。SigV4 always で署名を強制。
# -----------------------------------------------------------------------------

resource "aws_cloudfront_origin_access_control" "site" {
  name                              = local.oac_name
  description                       = "${var.prefix}-${var.env_name} S3 origin access control"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

# -----------------------------------------------------------------------------
# CloudFront Distribution
# -----------------------------------------------------------------------------

resource "aws_cloudfront_distribution" "site" {
  enabled             = true
  is_ipv6_enabled     = true
  http_version        = "http2"
  comment             = "${var.prefix}-${var.env_name} static site"
  default_root_object = "index.html"
  price_class         = "PriceClass_All"

  origin {
    domain_name              = aws_s3_bucket.site.bucket_regional_domain_name
    origin_id                = "s3-${aws_s3_bucket.site.id}"
    origin_access_control_id = aws_cloudfront_origin_access_control.site.id
  }

  default_cache_behavior {
    target_origin_id       = "s3-${aws_s3_bucket.site.id}"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    compress               = true

    # AWS Managed-CachingOptimized
    # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html
    cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"
  }

  # SPA エラーマッピング: S3 が 403/404 を返したら / index.html を 200 で返す。
  # ttl=0 にしておかないと、デプロイ直後にエラーがキャッシュされたままになる。
  custom_error_response {
    error_code            = 403
    response_code         = 200
    response_page_path    = "/index.html"
    error_caching_min_ttl = 0
  }

  custom_error_response {
    error_code            = 404
    response_code         = 200
    response_page_path    = "/index.html"
    error_caching_min_ttl = 0
  }

  viewer_certificate {
    # CloudFront のデフォルト証明書 (*.cloudfront.net) を使う構成。
    # この場合 minimum_protocol_version は AWS 側で固定(TLSv1)となり、
    # `TLSv1.2_2021` 等を明示すると `terraform apply` 時に ValidationError で拒否される。
    # カスタムドメイン + ACM 証明書 (us-east-1) を導入する際に、acm_certificate_arn / aliases
    # と合わせて minimum_protocol_version = "TLSv1.2_2021" を再指定すること。
    cloudfront_default_certificate = true
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
}

# -----------------------------------------------------------------------------
# S3 Bucket Policy
#   - CloudFront(OAC) からの GetObject 許可
#   - 非 TLS リクエストを全 Deny(enforceSSL 相当)
# -----------------------------------------------------------------------------

data "aws_iam_policy_document" "site" {
  # OAC からの読み取りのみ許可。AWS:SourceArn で当該 Distribution に限定。
  statement {
    sid     = "AllowCloudFrontServicePrincipalReadOnly"
    effect  = "Allow"
    actions = ["s3:GetObject"]
    resources = [
      "${aws_s3_bucket.site.arn}/*",
    ]

    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values   = [aws_cloudfront_distribution.site.arn]
    }
  }

  # 非 TLS を Deny(CDK の enforceSSL=true 相当)。
  statement {
    sid     = "DenyInsecureTransport"
    effect  = "Deny"
    actions = ["s3:*"]
    resources = [
      aws_s3_bucket.site.arn,
      "${aws_s3_bucket.site.arn}/*",
    ]

    principals {
      type        = "AWS"
      identifiers = ["*"]
    }

    condition {
      test     = "Bool"
      variable = "aws:SecureTransport"
      values   = ["false"]
    }
  }
}

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

  depends_on = [
    aws_s3_bucket_public_access_block.site,
  ]
}

⑦ terraform/modules/static-site/variables.tf

terraform/modules/static-site/variables.tf
# リソース名のプレフィックス(例: "<project-name>-liff")。
variable "prefix" {
  description = "リソース名のプレフィックス(例: <project-name>-liff)"
  type        = string

  validation {
    condition     = can(regex("^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$", var.prefix))
    error_message = "prefix は 3〜32 文字の小文字英数字とハイフンで、英数字で始まり終わる必要があります。"
  }
}

# 環境名(dev / stg / prod)。バケット名・OAC 名・コメント等の suffix として使う。
variable "env_name" {
  description = "環境名(dev / stg / prod)"
  type        = string

  validation {
    condition     = contains(["dev", "stg", "prod"], var.env_name)
    error_message = "env_name は dev / stg / prod のいずれかである必要があります。"
  }
}

# `aws s3 sync` 失敗時に残る未完了 multipart upload を回収するまでの日数。
variable "abort_incomplete_multipart_upload_days" {
  description = "未完了 multipart upload を自動 abort するまでの日数"
  type        = number
  default     = 7
}

# 本番のバケット誤削除を阻止する。dev/stg では true、prod では false に設定する想定。
# false の場合、バケットに中身が残っていると Terraform destroy はエラーになる。
variable "force_destroy" {
  description = "バケット削除時に中身も一緒に削除するか(dev/stg のみ true 推奨)"
  type        = bool
  default     = false
}

⑧ terraform/modules/static-site/outputs.tf

terraform/modules/static-site/outputs.tf
output "bucket_name" {
  description = "S3 バケット名"
  value       = aws_s3_bucket.site.id
}

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

output "distribution_id" {
  description = "CloudFront Distribution ID"
  value       = aws_cloudfront_distribution.site.id
}

output "distribution_arn" {
  description = "CloudFront Distribution ARN"
  value       = aws_cloudfront_distribution.site.arn
}

output "distribution_domain_name" {
  description = "CloudFront のデフォルトドメイン(例: dxxxx.cloudfront.net)"
  value       = aws_cloudfront_distribution.site.domain_name
}

⑨ terraform/modules/static-site/versions.tf

terraform/modules/static-site/versions.tf
terraform {
  required_version = ">= 1.11.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.80.0, < 7.0.0"
    }
  }
}

⑩ terraform/modules/github-oidc-role/main.tf

terraform/modules/github-oidc-role/main.tf
# =============================================================================
# 環境別の GitHub Actions Deploy Role
#
# Trust:
#   federated = token.actions.githubusercontent.com の OIDC Provider
#   aud       = sts.amazonaws.com
#   sub       = github_subjects 各要素について repo:<owner>/<repo>:<subject>
#
# 権限:
#   - 環境の S3 バケットへの sync(List/Get/Put/Delete object + multipart 系)
#   - 環境の CloudFront Distribution の invalidation
#
# `sub` には StringLike ではなく StringEquals を使う。これにより
# wildcard が解釈されない。github_subjects は完全一致の subject claim 一覧
# (例: 'environment:prod')を想定しており、将来 '*' を含むエントリが
# 紛れ込んでも StringLike のように暗黙的に権限が広がる事故を防げる。
# =============================================================================

locals {
  role_name = "${var.prefix}-${var.env_name}-github-deploy"

  # 各 subject に repo:<owner>/<repo>: のプレフィックスを付ける。
  subject_claims = [
    for s in var.github_subjects :
    "repo:${var.github_owner}/${var.github_repo}:${s}"
  ]
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    sid     = "GitHubActionsAssumeRole"
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [var.oidc_provider_arn]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:sub"
      values   = local.subject_claims
    }
  }
}

resource "aws_iam_role" "deploy" {
  name                 = local.role_name
  description          = "GitHub Actions deploy role for ${var.prefix} ${var.env_name}"
  max_session_duration = var.max_session_duration_seconds
  assume_role_policy   = data.aws_iam_policy_document.assume_role.json
}

# -----------------------------------------------------------------------------
# Deploy Policy
# -----------------------------------------------------------------------------

data "aws_iam_policy_document" "deploy" {
  # S3 バケットレベル操作。
  # `aws s3 sync` が実行中 multipart upload を列挙して abort/resume を判断するために
  # ListBucketMultipartUploads が必要。
  statement {
    sid    = "S3BucketLevel"
    effect = "Allow"
    actions = [
      "s3:ListBucket",
      "s3:GetBucketLocation",
      "s3:ListBucketMultipartUploads",
    ]
    resources = [var.bucket_arn]
  }

  # S3 オブジェクトレベル操作。
  # `aws s3 sync` は約 8 MB を超えるファイルで multipart upload に切り替わる。
  # AbortMultipartUpload / ListMultipartUploadParts が無いと、アップロード失敗時に
  # 孤児パートが残ってストレージ課金が発生し続けるうえ、デプロイ Role 自身では掃除できない。
  statement {
    sid    = "S3ObjectLevel"
    effect = "Allow"
    actions = [
      "s3:GetObject",
      "s3:PutObject",
      "s3:DeleteObject",
      "s3:GetObjectVersion",
      "s3:AbortMultipartUpload",
      "s3:ListMultipartUploadParts",
    ]
    resources = ["${var.bucket_arn}/*"]
  }

  # CloudFront invalidation。
  statement {
    sid    = "CloudFrontInvalidate"
    effect = "Allow"
    actions = [
      "cloudfront:CreateInvalidation",
      "cloudfront:GetInvalidation",
      "cloudfront:GetDistribution",
    ]
    resources = [var.distribution_arn]
  }
}

resource "aws_iam_role_policy" "deploy" {
  name   = "${local.role_name}-policy"
  role   = aws_iam_role.deploy.id
  policy = data.aws_iam_policy_document.deploy.json
}

⑪ terraform/modules/github-oidc-role/variables.tf

terraform/modules/github-oidc-role/variables.tf
variable "prefix" {
  description = "リソース名のプレフィックス(例: <project-name>-liff)"
  type        = string
}

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

variable "oidc_provider_arn" {
  description = "GitHub Actions OIDC Provider の ARN(github-oidc-provider モジュールから渡す)"
  type        = string
}

variable "github_owner" {
  description = "GitHub Organization / User 名"
  type        = string
}

variable "github_repo" {
  description = "リポジトリ名"
  type        = string
}

# OIDC sub claim のリスト。例:
#   - "environment:dev"         : GitHub Environment 'dev' 発行のトークン
#   - "ref:refs/heads/main"     : main ブランチへの push で発行されたトークン
#   - "pull_request"            : pull_request ワークフロー発行のトークン
# 複数指定した場合は OR 結合される(StringEquals なので完全一致のみ)。
variable "github_subjects" {
  description = "AssumeRole を許可する GitHub OIDC sub claim 一覧"
  type        = list(string)

  validation {
    condition     = length(var.github_subjects) > 0
    error_message = "github_subjects には最低 1 件の subject claim を指定してください。"
  }
}

variable "bucket_arn" {
  description = "この Role が sync 対象とする S3 バケット ARN"
  type        = string
}

variable "distribution_arn" {
  description = "この Role が invalidation を許可される CloudFront Distribution ARN"
  type        = string
}

variable "max_session_duration_seconds" {
  description = "AssumeRole の最大セッション時間(秒)"
  type        = number
  default     = 3600
}

⑫ terraform/modules/github-oidc-role/outputs.tf

terraform/modules/github-oidc-role/outputs.tf
output "role_arn" {
  description = "GitHub Actions に AWS_DEPLOY_ROLE_ARN として渡す値"
  value       = aws_iam_role.deploy.arn
}

output "role_name" {
  description = "Deploy Role 名"
  value       = aws_iam_role.deploy.name
}

⑬ terraform/modules/github-oidc-role/versions.tf

terraform/modules/github-oidc-role/versions.tf
terraform {
  required_version = ">= 1.11.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.80.0, < 7.0.0"
    }
  }
}

⑭ terraform/envs/dev/backend.tf

「Terraformのstateファイルをどこに保存するか」を宣言するファイルです。
※後述のデプロイ手順で最初に作成するs3バケット名を設定します。

terraform/envs/dev/backend.tf
# =============================================================================
# Remote backend (S3 + S3 native locking)
#
# - Terraform 1.11+ の use_lockfile = true により、DynamoDB を使わず S3 単独で
#   排他制御できる(state file と並んで <key>.tflock が作られる)。
# - 各環境は別アカウント・別 state なので、bucket / key を環境ごとに完全分離する。
# - bucket は事前に作成しておく必要がある(README の "Bootstrap" 参照)。
# =============================================================================

terraform {
  backend "s3" {
    bucket       = "<project-name>-liff-dev-tfstate-<your-aws-account-id>"
    key          = "<project-name>-liff/dev/terraform.tfstate"
    region       = "ap-northeast-1"
    encrypt      = true
    use_lockfile = true
  }
}

⑮ terraform/envs/dev/providers.tf

terraform/envs/dev/providers.tf
# =============================================================================
# Provider 設定(HashiCorp Style Guide に従い main.tf から分離)
#
# - allowed_account_ids: 誤アカウントへの terraform apply を構造的に拒否するガード。
#   CDK の bin/app.ts における CDK_DEFAULT_ACCOUNT との突合と同じ役割。
#   認証情報のアカウントが var.account_id と一致しない場合、provider 初期化に失敗する。
# - default_tags:        App 配下のすべてのリソースに統一タグを付与。
# =============================================================================

provider "aws" {
  region              = var.region
  allowed_account_ids = [var.account_id]

  default_tags {
    tags = var.tags
  }
}

# 認証情報のアカウントが var.account_id と一致することを再確認する二重防御。
# allowed_account_ids でも止まるが、postcondition のほうがメッセージが読みやすい。
data "aws_caller_identity" "current" {
  lifecycle {
    postcondition {
      condition     = self.account_id == var.account_id
      error_message = "認証情報のアカウント (${self.account_id}) は var.account_id (${var.account_id}) と一致しません。--profile を確認してください。"
    }
  }
}

⑯ terraform/envs/dev/versions.tf

terraform/envs/dev/versions.tf
terraform {
  required_version = ">= 1.11.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.80.0, < 7.0.0"
    }
  }
}

⑰ terraform/envs/dev/main.tf

terraform/envs/dev/main.tf
# =============================================================================
# dev 環境のルート構成(modules の呼び出しのみを記述)
#
# 構築リソース(同一 state):
#   1. github-oidc-provider : アカウントベース singleton(IAM OIDC Provider)
#   2. static-site          : S3 + CloudFront(OAC)
#   3. github-oidc-role     : GitHub Actions Deploy Role
#
# 注:
#   - provider 設定とアカウントガード data source は providers.tf に分離。
#   - terraform / required_providers ブロックは versions.tf に分離。
#   - backend 設定は backend.tf に分離。
# =============================================================================

# -----------------------------------------------------------------------------
# 1. アカウントベース(GitHub OIDC Provider)
# -----------------------------------------------------------------------------
module "github_oidc_provider" {
  source = "../../modules/github-oidc-provider"

  prefix   = var.prefix
  env_name = var.env_name
}

# -----------------------------------------------------------------------------
# 2. 静的サイト(S3 + CloudFront OAC)
# -----------------------------------------------------------------------------
module "static_site" {
  source = "../../modules/static-site"

  prefix        = var.prefix
  env_name      = var.env_name
  force_destroy = var.force_destroy
}

# -----------------------------------------------------------------------------
# 3. GitHub Actions Deploy Role
# -----------------------------------------------------------------------------
module "github_oidc_role" {
  source = "../../modules/github-oidc-role"

  prefix            = var.prefix
  env_name          = var.env_name
  oidc_provider_arn = module.github_oidc_provider.provider_arn
  github_owner      = var.github_owner
  github_repo       = var.github_repo
  github_subjects   = var.github_subjects
  bucket_arn        = module.static_site.bucket_arn
  distribution_arn  = module.static_site.distribution_arn
}

⑱ terraform/envs/dev/variables.tf

terraform/envs/dev/variables.tf
# 各環境ディレクトリで上書きする変数群。値は terraform.tfvars に書く。

variable "account_id" {
  description = "この環境を構築する AWS アカウント ID。provider の allowed_account_ids にも指定する。"
  type        = string

  validation {
    condition     = can(regex("^[0-9]{12}$", var.account_id))
    error_message = "account_id は 12 桁の数字である必要があります。"
  }
}

variable "region" {
  description = "AWS リージョン"
  type        = string
}

variable "prefix" {
  description = "リソース名のプレフィックス(例: <project-name>-liff)"
  type        = string
}

variable "env_name" {
  description = "環境名(dev / stg / prod)"
  type        = string

  validation {
    condition     = contains(["dev", "stg", "prod"], var.env_name)
    error_message = "env_name は dev / stg / prod のいずれかである必要があります。"
  }
}

variable "github_owner" {
  description = "GitHub Organization / User 名"
  type        = string
}

variable "github_repo" {
  description = "GitHub リポジトリ名"
  type        = string
}

variable "github_subjects" {
  description = "AssumeRole を許可する GitHub OIDC sub claim(例: [\"environment:dev\"])"
  type        = list(string)
}

variable "tags" {
  description = "App 配下のすべてのリソースに付与するタグ"
  type        = map(string)
}

variable "force_destroy" {
  description = "S3 バケットを Terraform destroy 時に空にしてから削除するか(dev/stg のみ true 推奨)"
  type        = bool
}


# terraform/envs/prod/variables.tfであれば以下のようにする
variable "force_destroy" {
  description = "S3 バケットを Terraform destroy 時に空にしてから削除するか。prod は必ず false。"
  type        = bool

  validation {
    condition     = var.force_destroy == false
    error_message = "prod 環境では force_destroy は必ず false である必要があります(誤削除防止)。"
  }
}

⑲ terraform/envs/dev/outputs.tf

terraform/envs/dev/outputs.tf
# CDK の CfnOutput に対応する出力。
# GitHub Actions の Environment Variables にそのまま登録する。

output "github_oidc_provider_arn" {
  description = "GitHub Actions OIDC Provider ARN(アカウントベース、同アカウント内で共有)"
  value       = module.github_oidc_provider.provider_arn
}

output "bucket_name" {
  description = "S3_BUCKET_NAME に登録"
  value       = module.static_site.bucket_name
}

output "distribution_id" {
  description = "CLOUDFRONT_DISTRIBUTION_ID に登録"
  value       = module.static_site.distribution_id
}

output "distribution_domain_name" {
  description = "CloudFront のデフォルトドメイン(例: dxxxx.cloudfront.net)"
  value       = module.static_site.distribution_domain_name
}

output "deploy_role_arn" {
  description = "AWS_DEPLOY_ROLE_ARN に登録"
  value       = module.github_oidc_role.role_arn
}

⑳ terraform/envs/dev/terraform.tfvars

terraform/envs/dev/terraform.tfvars
account_id = "<your-aws-account-id>"
region     = "ap-northeast-1"
prefix     = "<project-name>-liff"
env_name   = "dev"

github_owner    = "<your-organization-name>"
github_repo     = "<your-repo-name>"
github_subjects = ["environment:dev"]

tags = {
  Project     = "<project-name>-liff"
  Environment = "dev"
  ManagedBy   = "terraform"
}

force_destroy = true


# 本番では必ず false(誤削除防止。variables.tf の validation で強制)。
# terraform/envs/prod/terraform.tfvarsであれば以下のようにする
force_destroy = false

実行方法

①terraformコマンドのセットアップ

以下の記事を参考に、セットアップを行います。

②設定ファイルの編集

次に、terraform/envs/dev/stg/prodterraform.tfvarsファイルを編集し、必要な値を入力します(AWSアカウントID等)

③SSOログイン

デプロイ対象のAWSアカウントでSSOログインします。

zsh
aws sso login --profile <your-profile-name>

以下のコマンドを実行し、出力結果のAWSアカウントIDが、terraform.tfvarsのファイルに設定したIDと一致していることを確認します。

zsh
aws sts get-caller-identity --profile <your-profile-name>

④Bootstrap

tfstate保存用バケットのBootstrapを行います(各アカウント × 各リージョンで初回 1 回のみ)
以下のコマンドを順に実行します。
※スクリプトやIaC化すると楽かもしれません
※暗号化やパブリックアクセス全ブロックなど、デフォルトの設定で問題ない場合もあります

zsh
# S3バケットを作成(ここでバケット名を指定)
aws s3api create-bucket \
  --profile <project-name>-liff-dev \
  --bucket <project-name>-liff-dev-tfstate-<aws-account-id> \
  --region ap-northeast-1 \
  --create-bucket-configuration LocationConstraint=ap-northeast-1

# バージョニングの有効化
aws s3api put-bucket-versioning \
  --profile <project-name>-liff-dev \
  --bucket <project-name>-liff-dev-tfstate-<aws-account-id> \
  --versioning-configuration Status=Enabled

# AWS管理鍵による暗号化
aws s3api put-bucket-encryption \
  --profile <project-name>-liff-dev \
  --bucket <project-name>-liff-dev-tfstate-<aws-account-id> \
  --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'

# パブリックアクセス全ブロック
aws s3api put-public-access-block \
  --profile <project-name>-liff-dev \
  --bucket <project-name>-liff-dev-tfstate-<aws-account-id> \
  --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

⑤AWSリソースのデプロイ

以下のコマンドを実行し、実際にAWSリソースをデプロイします。

zsh
cd terraform/envs/dev
terraform/envs/dev
AWS_PROFILE=<your-profile-name> terraform init

AWS_PROFILE=<your-profile-name> terraform plan

AWS_PROFILE=<your-profile-name> terraform apply

⑥GitHub Actions連携

ターミナル上に「Outputs:」として出力された値を、デプロイ対象のGitHubリポジトリのEnvironmentsに登録していきます。

SCR-20260520-ufho.png

変数名 取得元 (Terraform Output)
AWS_DEPLOY_ROLE_ARN <project-name>-liff-<env>-github-oidc.DeployRoleArn arn:aws:iam::<env-account>:role/<project-name>-liff-dev-github-deploy
AWS_REGION 固定 ap-northeast-1
S3_BUCKET_NAME <project-name>-liff-<env>-frontend.BucketName <project-name>-liff-dev-static-<env-account>
CLOUDFRONT_DISTRIBUTION_ID <project-name>-liff-<env>-frontend.DistributionId EXXXXXXXXXXXX
LIFF_ID LINE Developers から 2001234567-asdfghjk

その後、GitHub Actionsのワークフローを実行してフロントエンドのコンテンツをアップロードすれば完了です!
このあたりは手動での設定が必要となるため、前回の記事をご参照ください(スクリプトで自動化するのもありです)

⑦リソースの削除

以下のコマンドを実行します。

terraform/envs/dev
AWS_PROFILE=<your-profile-name> terraform destroy

dev/stg環境の場合、tfstate保存用バケットを除く、「今回のterraformによって作成されたすべてのAWSリソース」が削除されます。

今回は以上になります!

0
3
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?