Skip to content
Published on

Terraform IaC完全ガイド2025:AWS/GCPインフラをコードで管理する全て

Authors

はじめに

インフラを手動で管理する時代は終わりました。クラウドリソースが数十、数百に増えると、コンソールクリックでは再現性、監査追跡、コラボレーションが不可能になります。TerraformはHashiCorpが作ったオープンソースIaC(Infrastructure as Code)ツールで、AWS、GCP、Azureを含む3,000以上のプロバイダーをサポートしています。

このガイドでは、Terraformの基礎から実践まで全てをカバーします。HCL構文、State管理、モジュール設計、AWS/GCP実践インフラ、Terragrunt、CI/CD自動化、セキュリティスキャン、コスト見積もりまで — 本番レベルのIaCを構築するために必要な全ての知識を体系的に整理します。


1. なぜIaCが重要なのか

1.1 手動管理 vs IaC

観点手動(コンソール)IaC(Terraform)
再現性不可能コードで100%再現
監査追跡限定的Git履歴
コラボレーション不可能PRレビュー
環境複製数時間数分
ロールバック手動復旧terraform apply
ドキュメント別途管理コードがドキュメント

1.2 Terraform vs 他のIaCツール

ツール言語クラウド状態管理学習コスト
TerraformHCLマルチクラウドStateファイル
CloudFormationJSON/YAMLAWS専用スタック
PulumiPython/TS/GoマルチクラウドStateファイル
AWS CDKPython/TS/GoAWS専用CloudFormation
OpenTofuHCLマルチクラウドStateファイル

Terraformを選ぶ理由:

  • マルチクラウドサポート(AWS + GCP + Azure)
  • 最大のコミュニティとモジュールレジストリ
  • 宣言的構文で意図が明確
  • Plan -> Applyワークフローで安全な変更

2. Terraform基礎

2.1 HCL基本構文

# プロバイダー設定
terraform {
  required_version = ">= 1.7.0"

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

  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "terraform"
      Project     = var.project_name
    }
  }
}

2.2 変数(Variables)

# variables.tf
variable "aws_region" {
  description = "AWS region to deploy resources"
  type        = string
  default     = "ap-northeast-2"
}

variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "instance_config" {
  description = "EC2 instance configuration"
  type = object({
    instance_type     = string
    volume_size       = number
    enable_monitoring = bool
  })
  default = {
    instance_type     = "t3.medium"
    volume_size       = 50
    enable_monitoring = true
  }
}

variable "allowed_cidrs" {
  description = "List of CIDR blocks allowed for ingress"
  type        = list(string)
  default     = ["10.0.0.0/8"]
}

variable "tags" {
  description = "Additional tags"
  type        = map(string)
  default     = {}
}

2.3 リソース(Resources)

# main.tf
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "main-vpc-${var.environment}"
  }
}

resource "aws_subnet" "public" {
  count = length(var.availability_zones)

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = var.availability_zones[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet-${count.index + 1}"
    Type = "public"
  }
}

2.4 データソース(Data Sources)

# 最新Amazon Linux 2023 AMI検索
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# 現在のAWSアカウント情報
data "aws_caller_identity" "current" {}

# 現在のリージョン
data "aws_region" "current" {}

2.5 出力値(Outputs)

# outputs.tf
output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "Public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "db_password" {
  description = "Database password"
  value       = random_password.db.result
  sensitive   = true
}

2.6 Locals

locals {
  name_prefix = "${var.project_name}-${var.environment}"

  common_tags = merge(var.tags, {
    Environment = var.environment
    ManagedBy   = "terraform"
    Project     = var.project_name
  })

  env_config = {
    dev = {
      instance_type = "t3.small"
      min_size      = 1
      max_size      = 2
    }
    staging = {
      instance_type = "t3.medium"
      min_size      = 2
      max_size      = 4
    }
    prod = {
      instance_type = "t3.large"
      min_size      = 3
      max_size      = 10
    }
  }

  config = local.env_config[var.environment]
}

3. State管理

3.1 Stateとは?

Terraform Stateは実際のインフラとコード間のマッピング情報を保存します。terraform.tfstateファイルに全リソースの現在の状態がJSONで記録されます。

Stateがないと:

  • Terraformがどのリソースを管理しているか分からない
  • Planで全リソースを新規作成しようとする
  • リソース間の依存関係を追跡できない

3.2 リモートState(S3 + DynamoDB)

# backend.tf
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "environments/prod/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

S3バケットとDynamoDBテーブルの作成(ブートストラップ):

# bootstrap/main.tf
resource "aws_s3_bucket" "terraform_state" {
  bucket = "mycompany-terraform-state"

  lifecycle {
    prevent_destroy = true
  }
}

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

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 = "aws:kms"
    }
  }
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

3.3 Stateコマンド

# State一覧確認
terraform state list

# 特定リソースの詳細情報
terraform state show aws_vpc.main

# リソース名変更(コードリファクタリング時)
terraform state mv aws_instance.old aws_instance.new

# Stateからリソース削除(実際のインフラは維持)
terraform state rm aws_instance.legacy

4. モジュール(Modules)

4.1 モジュール作成

# modules/vpc/main.tf
resource "aws_vpc" "this" {
  cidr_block           = var.cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-vpc"
  })
}

resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)

  vpc_id                  = aws_vpc.this.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-public-${count.index + 1}"
    Type = "public"
  })
}

resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)

  vpc_id            = aws_vpc.this.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-private-${count.index + 1}"
    Type = "private"
  })
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-igw"
  })
}

resource "aws_nat_gateway" "this" {
  count = var.enable_nat_gateway ? length(var.public_subnet_cidrs) : 0

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-nat-${count.index + 1}"
  })
}

resource "aws_eip" "nat" {
  count  = var.enable_nat_gateway ? length(var.public_subnet_cidrs) : 0
  domain = "vpc"
}

4.2 モジュール使用

# environments/prod/main.tf
module "vpc" {
  source = "../../modules/vpc"

  name_prefix = "prod"
  cidr_block  = "10.0.0.0/16"

  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
  availability_zones   = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]

  enable_nat_gateway = true
  tags               = local.common_tags
}

4.3 Terraform Registryモジュール使用

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = false

  tags = local.common_tags
}

5. AWS実践インフラ

5.1 VPC + EC2 + ALB + RDS

resource "aws_launch_template" "app" {
  name_prefix   = "${local.name_prefix}-app-"
  image_id      = data.aws_ami.amazon_linux.id
  instance_type = local.config.instance_type

  vpc_security_group_ids = [aws_security_group.app.id]

  user_data = base64encode(templatefile("userdata.sh", {
    environment = var.environment
    db_host     = aws_db_instance.main.endpoint
  }))

  block_device_mappings {
    device_name = "/dev/xvda"
    ebs {
      volume_size = var.instance_config.volume_size
      volume_type = "gp3"
      encrypted   = true
    }
  }
}

resource "aws_autoscaling_group" "app" {
  name                = "${local.name_prefix}-app-asg"
  desired_capacity    = local.config.min_size
  min_size            = local.config.min_size
  max_size            = local.config.max_size
  vpc_zone_identifier = module.vpc.private_subnet_ids
  target_group_arns   = [aws_lb_target_group.app.arn]

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  health_check_type         = "ELB"
  health_check_grace_period = 300
}

5.2 ALB設定

resource "aws_lb" "app" {
  name               = "${local.name_prefix}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = module.vpc.public_subnet_ids

  enable_deletion_protection = var.environment == "prod"
  tags                       = local.common_tags
}

resource "aws_lb_target_group" "app" {
  name     = "${local.name_prefix}-app-tg"
  port     = 8080
  protocol = "HTTP"
  vpc_id   = module.vpc.vpc_id

  health_check {
    path                = "/actuator/health"
    port                = "traffic-port"
    healthy_threshold   = 3
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    matcher             = "200"
  }

  deregistration_delay = 30
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.app.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.main.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

5.3 RDS(PostgreSQL)

resource "aws_db_instance" "main" {
  identifier = "${local.name_prefix}-db"

  engine         = "postgres"
  engine_version = "15.5"
  instance_class = "db.r6g.large"

  allocated_storage     = 100
  max_allocated_storage = 500
  storage_type          = "gp3"
  storage_encrypted     = true

  db_name  = "myapp"
  username = "admin"
  password = random_password.db.result

  multi_az               = var.environment == "prod"
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.db.id]

  backup_retention_period = 7
  skip_final_snapshot     = var.environment != "prod"

  performance_insights_enabled = true
  tags                         = local.common_tags
}

6. GCP実践インフラ

6.1 VPC + GKE

provider "google" {
  project = var.gcp_project_id
  region  = var.gcp_region
}

resource "google_compute_network" "main" {
  name                    = "${local.name_prefix}-vpc"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "main" {
  name          = "${local.name_prefix}-subnet"
  ip_cidr_range = "10.0.0.0/20"
  region        = var.gcp_region
  network       = google_compute_network.main.id

  secondary_ip_range {
    range_name    = "pods"
    ip_cidr_range = "10.1.0.0/16"
  }

  secondary_ip_range {
    range_name    = "services"
    ip_cidr_range = "10.2.0.0/20"
  }
}

resource "google_container_cluster" "primary" {
  name     = "${local.name_prefix}-gke"
  location = var.gcp_region

  remove_default_node_pool = true
  initial_node_count       = 1

  network    = google_compute_network.main.name
  subnetwork = google_compute_subnetwork.main.name

  ip_allocation_policy {
    cluster_secondary_range_name  = "pods"
    services_secondary_range_name = "services"
  }

  workload_identity_config {
    workload_pool = "${var.gcp_project_id}.svc.id.goog"
  }

  release_channel {
    channel = "REGULAR"
  }
}

resource "google_container_node_pool" "primary" {
  name       = "${local.name_prefix}-node-pool"
  location   = var.gcp_region
  cluster    = google_container_cluster.primary.name
  node_count = 3

  node_config {
    machine_type = "e2-standard-4"
    disk_size_gb = 100
    disk_type    = "pd-ssd"
  }

  autoscaling {
    min_node_count = 3
    max_node_count = 10
  }
}

6.2 Cloud SQL + Cloud Run

resource "google_sql_database_instance" "main" {
  name             = "${local.name_prefix}-db"
  database_version = "POSTGRES_15"
  region           = var.gcp_region

  settings {
    tier = "db-custom-4-16384"

    ip_configuration {
      ipv4_enabled    = false
      private_network = google_compute_network.main.id
    }

    backup_configuration {
      enabled                        = true
      point_in_time_recovery_enabled = true
      start_time                     = "03:00"
    }

    availability_type = var.environment == "prod" ? "REGIONAL" : "ZONAL"
  }

  deletion_protection = var.environment == "prod"
}

resource "google_cloud_run_v2_service" "app" {
  name     = "${local.name_prefix}-app"
  location = var.gcp_region

  template {
    containers {
      image = "gcr.io/${var.gcp_project_id}/myapp:latest"

      ports {
        container_port = 8080
      }

      resources {
        limits = {
          cpu    = "2"
          memory = "1Gi"
        }
      }

      env {
        name  = "SPRING_PROFILES_ACTIVE"
        value = var.environment
      }

      startup_probe {
        http_get {
          path = "/actuator/health"
        }
        initial_delay_seconds = 10
        period_seconds        = 3
      }
    }

    scaling {
      min_instance_count = var.environment == "prod" ? 2 : 0
      max_instance_count = 10
    }
  }
}

7. 高度なパターン

7.1 Workspaces

# ワークスペース作成
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

# ワークスペース切り替え
terraform workspace select prod
locals {
  env_config = {
    dev     = { instance_type = "t3.small", db_instance = "db.t3.medium" }
    staging = { instance_type = "t3.medium", db_instance = "db.r6g.large" }
    prod    = { instance_type = "t3.large", db_instance = "db.r6g.xlarge" }
  }

  config = local.env_config[terraform.workspace]
}

7.2 Dynamic Blocks

resource "aws_security_group" "app" {
  name        = "${local.name_prefix}-app-sg"
  description = "Application security group"
  vpc_id      = module.vpc.vpc_id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

7.3 for_each vs count

# count -- インデックス基盤(削除時の再作成問題)
resource "aws_subnet" "example" {
  count      = 3
  cidr_block = "10.0.${count.index}.0/24"
  vpc_id     = aws_vpc.main.id
}

# for_each -- キー基盤(安全な削除/追加)
resource "aws_subnet" "example" {
  for_each = {
    "public-a"  = { cidr = "10.0.1.0/24", az = "ap-northeast-2a" }
    "public-b"  = { cidr = "10.0.2.0/24", az = "ap-northeast-2b" }
    "private-a" = { cidr = "10.0.11.0/24", az = "ap-northeast-2a" }
  }

  cidr_block        = each.value.cidr
  availability_zone = each.value.az
  vpc_id            = aws_vpc.main.id

  tags = {
    Name = "${local.name_prefix}-${each.key}"
  }
}

7.4 Lifecycle ルール

resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = local.config.instance_type

  lifecycle {
    create_before_destroy = true
    prevent_destroy       = false
    ignore_changes        = [ami]
  }
}

moved {
  from = aws_instance.old_name
  to   = aws_instance.new_name
}

8. Import & マイグレーション

8.1 terraform importコマンド

terraform import aws_vpc.main vpc-0123456789abcdef0
terraform import aws_instance.web i-0123456789abcdef0

8.2 Import Block(Terraform 1.5+)

import {
  to = aws_vpc.main
  id = "vpc-0123456789abcdef0"
}

import {
  to = aws_instance.web
  id = "i-0123456789abcdef0"
}
# コード自動生成
terraform plan -generate-config-out=generated.tf

# レビュー後適用
terraform apply

8.3 既存インフラマイグレーション戦略

段階的アプローチ:

  1. インベントリ作成 — 現在のリソースをリスト化
  2. コード作成 — リソースごとにHCLコードを作成
  3. Import — 既存リソースをStateに取り込み
  4. Plan検証terraform planで変更がないことを確認
  5. 段階的管理 — 新しいリソースからTerraformで作成

9. Terragrunt

9.1 DRY環境管理

# infrastructure/terragrunt.hcl(ルート)
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  config = {
    bucket         = "mycompany-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite"
  contents  = <<EOF
provider "aws" {
  region = "ap-northeast-2"
}
EOF
}
# infrastructure/environments/prod/vpc/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules/vpc"
}

inputs = {
  name_prefix          = "prod"
  cidr_block           = "10.0.0.0/16"
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
  availability_zones   = ["ap-northeast-2a", "ap-northeast-2b"]
  enable_nat_gateway   = true
}

9.2 依存関係管理

# infrastructure/environments/prod/rds/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules/rds"
}

dependency "vpc" {
  config_path = "../vpc"
}

inputs = {
  name_prefix = "prod"
  vpc_id      = dependency.vpc.outputs.vpc_id
  subnet_ids  = dependency.vpc.outputs.private_subnet_ids
}

10. CI/CD自動化

10.1 GitHub Actions + Terraform

# .github/workflows/terraform.yml
name: Terraform CI/CD

on:
  pull_request:
    paths:
      - 'infrastructure/**'
  push:
    branches:
      - main
    paths:
      - 'infrastructure/**'

jobs:
  plan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/terraform-ci
          aws-region: ap-northeast-2

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.7.0

      - name: Terraform Init
        working-directory: infrastructure/environments/prod
        run: terraform init

      - name: Terraform Plan
        working-directory: infrastructure/environments/prod
        run: terraform plan -no-color -out=tfplan

  apply:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/terraform-ci
          aws-region: ap-northeast-2

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Init & Apply
        working-directory: infrastructure/environments/prod
        run: |
          terraform init
          terraform apply -auto-approve

11. コスト管理(Infracost)

# Infracost実行
infracost breakdown --path=infrastructure/environments/prod

# JSON出力
infracost breakdown \
  --path=infrastructure/environments/prod \
  --format=json \
  --out-file=infracost.json

タギング戦略

locals {
  required_tags = {
    Environment = var.environment
    Project     = var.project_name
    Team        = var.team_name
    CostCenter  = var.cost_center
    ManagedBy   = "terraform"
  }
}

12. セキュリティ(tfsec、Checkov)

12.1 tfsec静的分析

tfsec ./infrastructure/

12.2 Checkov

checkov -d ./infrastructure/ --framework terraform

12.3 シークレット管理(Vault)

data "vault_generic_secret" "db" {
  path = "secret/data/myapp/database"
}

resource "aws_db_instance" "main" {
  username = data.vault_generic_secret.db.data["username"]
  password = data.vault_generic_secret.db.data["password"]
}

13. Terraform資格ガイド

HashiCorp Certified: Terraform Associate (003)

試験概要:

  • 時間:60分
  • 問題数:57問(選択式+穴埋め)
  • 合格基準:70%
  • 有効期間:2年
  • 費用:約70 USD

コアドメイン:

  1. IaCコンセプト理解(15%)— IaCのメリット、Terraformワークフロー
  2. Terraform基礎(20%)— プロバイダー、リソース、変数、出力
  3. State管理(15%)— リモートState、ロック、コマンド
  4. モジュール(15%)— モジュール作成、使用、レジストリ
  5. Terraform CLI(15%)— init、plan、apply、destroy、import
  6. ワークフロー(10%)— チームワークフロー、Terraform Cloud
  7. 実装と保守(10%)— デバッグ、アップグレード

14. インタビュー質問15選

基本(1-5)

Q1. Terraformのコアワークフローを説明してください。

Write(コード作成)-> Plan(変更プレビュー)-> Apply(適用)の3段階です。terraform planは実際の変更なしに何が変わるかを表示し、terraform applyが実際にインフラを変更します。

Q2. Terraform Stateの役割と重要性は?

Stateは実際のインフラとコード間のマッピングを保存します。Stateがないと、Terraformはどのリソースを管理しているか分かりません。リモートバックエンド(S3+DynamoDB)でチーム共有とロックを実装する必要があります。

Q3. ProviderとResourceの違いは?

Providerはクラウド/サービスとのAPI接続を担当するプラグイン(AWS、GCP等)で、Resourceはプロバイダーを通じて管理する実際のインフラオブジェクト(VPC、EC2等)です。

Q4. terraform planとterraform applyの違いは?

planは変更をプレビューで表示し、実際のインフラを変更しません。applyは実際にインフラを変更します。applyは内部的にplanを先に実行します。

Q5. 変数の優先順位(Variable Precedence)を説明してください。

環境変数TF_VAR_xxx、terraform.tfvars、terraform.tfvars.json、auto.tfvars、-varまたは-var-fileコマンドラインオプションの順で優先順位が高くなります。

深掘り(6-10)

Q6. countとfor_eachの違いと、いつどちらを使うべきですか?

countはインデックス基盤で、中間要素を削除するとそれ以降の全リソースが再作成されます。for_eachはキー基盤で、特定の要素だけを安全に削除/追加できます。一般的にfor_eachを推奨します。

Q7. State Lockingが必要な理由は?

2人が同時にterraform applyを実行するとStateが競合し、インフラが損傷する可能性があります。DynamoDBロックテーブルで同時実行を防止します。

Q8. モジュール(Module)のメリットと設計原則は?

再利用性、DRY原則、カプセル化がメリットです。単一責任原則で設計し、変数で設定を外部化し、出力値で他のモジュールと接続します。

Q9. terraform importとImport Blockの違いは?

terraform importはコマンドラインで1つずつリソースをインポートし、コードを自動生成しません。Import Block(1.5+)は宣言的に複数リソースを一括インポートでき、-generate-config-outオプションでコード自動生成が可能です。

Q10. Terraformでシークレットを安全に管理する方法は?

Stateファイルに機密情報が保存されるため、リモートバックエンドの暗号化必須。sensitive = true変数を使用し、HashiCorp VaultやAWS Secrets Managerと連携し、.tfvarsファイルは.gitignoreに追加し、環境変数で注入します。

実践(11-15)

Q11. Drift Detectionとは何で、どう対処しますか?

Driftは実際のインフラがTerraformコードと異なる状態です。terraform planで検知し、terraform applyでコードの状態に戻すかコードを修正します。定期的なplan実行でドリフトを監視する必要があります。

Q12. Terragruntを使う理由は?

TerraformのDRY原則の不足を補完します。環境別設定を継承構造で管理し、リモートState設定を自動化し、モジュール間の依存関係を管理します。

Q13. Terraform CI/CDパイプラインのベストプラクティスは?

PRでplan実行後、結果をコメントとして投稿。mainブランチmerge時にapply実行。OIDCでAWS認証(シークレットキー不要)。terraform fmt + tfsecでコード品質検証。環境別承認プロセス(GitHub Environments)を適用します。

Q14. TerraformとCloudFormationを併用すべき場合は?

AWS専用でCloudFormation StackSets、Service Catalog等のAWSネイティブ機能が必要な場合はCFNを、マルチクラウドやSaaSリソース管理が必要な場合はTerraformを使用します。

Q15. Infracostでコスト管理する方法は?

PRでインフラ変更のコスト影響を自動的に見積もり、コメントとして表示します。月間コストの増減を可視化し、チームがコストを認識した状態でインフラ変更を承認します。


15. クイズ

Q1. Terraform Stateのリモートバックエンドとして S3を使用する際、State Lockingに必要なAWSサービスは?

DynamoDBです。S3はStateファイルの保存、DynamoDBはロック(Lock)テーブルとして同時実行を防止します。dynamodb_table属性で設定します。

Q2. for_eachがcountより推奨される理由は?

countはインデックス基盤のため、中間要素を削除するとそれ以降の全リソースが再作成されます。for_eachはキー基盤のため、特定の要素だけを安全に追加/削除できます。

Q3. terraform plan -generate-config-outオプションの用途は?

Import Blockで宣言したリソースのHCLコードを自動生成します。既存インフラをTerraformにマイグレーションする際、コードを最初から書く必要なく、自動生成されたコードをベースに整理すればよいです。

Q4. Terragruntのdependencyブロックの役割は?

モジュール間の実行順序とデータ受け渡しを管理します。例えばRDSモジュールがVPCモジュールのoutput(subnet_ids)に依存する場合、TerragruntがVPCを先に適用し、そのoutputをRDSモジュールに渡します。

Q5. tfsecとCheckovの違いは?

tfsecはTerraform専用のセキュリティスキャナーで、HCLコードのセキュリティ脆弱性を検査します。CheckovはTerraformだけでなくCloudFormation、Kubernetes、Docker等、様々なIaCツールをサポートする汎用ポリシー検査ツールです。


参考資料

  1. Terraform 公式ドキュメント
  2. Terraform Registry
  3. Terraform AWS Provider
  4. Terraform GCP Provider
  5. Terragrunt 公式ドキュメント
  6. Infracost ドキュメント
  7. tfsec ガイド
  8. Checkov ドキュメント
  9. HashiCorp Learn Terraform
  10. Terraform Up and Running (O'Reilly)
  11. AWS VPC Terraform Module
  12. Terraform Best Practices
  13. Atlantis ドキュメント
  14. OpenTofu
  15. HashiCorp Vault Terraform Provider
  16. Terraform Associate 試験ガイド
  17. AWS IAM Terraform ベストプラクティス
  18. GKE Terraform モジュール