Skip to content

✍️ 필사 모드: Terraform & IaC 完全ガイド — インフラをコードで管理するすべて

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

1. Infrastructure as Codeとは

なぜインフラをコードで管理するのか

従来のインフラ管理方式は、管理者がコンソールにアクセスして手動でサーバーをプロビジョニングし、ネットワークを設定するものでした。この方式にはいくつかの問題があります。

  • 再現不可能: 同一の環境を再度構築することが困難です。
  • 変更追跡不可: 誰が、いつ、何を変更したか把握できません。
  • スケーリングの限界: サーバー10台までは手動で管理できますが、100台以上は事実上不可能です。
  • 環境の不一致: 開発、ステージング、本番環境が微妙に異なるConfiguration Driftが発生します。

Infrastructure as Code(IaC)は、インフラの望ましい状態をコードファイルで定義し、ツールが自動的にその状態を実現する方法論です。コードであるためGitでバージョン管理し、コードレビューを行い、CI/CDパイプラインでデプロイできます。

IaCツール比較

ツール言語アプローチ状態管理クラウド対応
TerraformHCL宣言的自己管理Stateファイルマルチクラウド
CloudFormationJSON/YAML宣言的AWS管理AWS専用
PulumiTypeScript/Python/Go命令的 + 宣言的Pulumi Cloudマルチクラウド
AWS CDKTypeScript/Python/Go命令的CloudFormationスタックAWS専用
CrossplaneYAML宣言的 (K8s CRD)K8s etcdマルチクラウド

TerraformはHCLという専用の宣言的言語を使用し、最も広いプロバイダエコシステムを持っています。AWS、GCP、Azureはもちろん、Datadog、PagerDuty、GitHubのようなSaaSも管理できます。

CloudFormationはAWSネイティブツールで、AWSサービスとの統合が最も早いです。新しいAWSサービスのリリースと同時にサポートされます。

Pulumiは汎用プログラミング言語を使用するため、IDE自動補完、型チェック、ユニットテストをそのまま活用できます。

AWS CDKはCloudFormationの上に抽象化レイヤーを追加したツールで、L2/L3 Constructで複雑なパターンを簡潔に表現します。

宣言的 vs 命令的アプローチ

IaCツールは大きく2つのアプローチに分かれます。

宣言的(Declarative): 望ましい最終状態を定義すると、ツールが現在の状態との差分を計算して変更を実行します。TerraformとCloudFormationがこの方式です。

命令的(Imperative): 実行する手順を順番に記述します。シェルスクリプト、Ansible(一部)、Pulumiがこの方式に近いです。

Terraformは宣言的アプローチを採用しており、インフラの「何を(What)」を定義すれば「どのように(How)」はTerraformが処理します。


2. Terraform基礎

Provider

ProviderはTerraformが特定のインフラプラットフォームと通信するためのプラグインです。AWS、GCP、Azureのようなクラウドプロバイダだけでなく、Kubernetes、Helm、Datadog、GitHubなど数千のプロバイダが存在します。

terraform {
  required_version = ">= 1.5.0"

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

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

  default_tags {
    tags = {
      Environment = "production"
      ManagedBy   = "terraform"
    }
  }
}

required_providersブロックでプロバイダのソースとバージョン制約を明示します。~> 5.0は5.x範囲の最新バージョンを使用するが6.0以上は許可しないという意味です。

Resource

Resourceは Terraformで管理するインフラオブジェクトを宣言します。リソースタイプとローカル名の組み合わせで一意に識別されます。

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

  tags = {
    Name = "main-vpc"
  }
}

resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  map_public_ip_on_launch = true

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

リソース間の参照はリソースタイプ.ローカル名.属性の形式です。上記の例でaws_vpc.main.idはVPCリソースのIDを参照しています。

Data Source

Data SourceはTerraform外部で既に存在するリソースの情報を読み取ります。読み取り専用のため、インフラを変更しません。

data "aws_availability_zones" "available" {
  state = "available"
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

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

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

VariableとOutput

Variableはモジュールの入力パラメータで、Outputはモジュールの出力値です。

# variables.tf
variable "environment" {
  description = "デプロイ環境 (dev, staging, prod)"
  type        = string
  default     = "dev"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environmentはdev、staging、prodのいずれかでなければなりません。"
  }
}

variable "instance_type" {
  description = "EC2インスタンスタイプ"
  type        = string
  default     = "t3.medium"
}

variable "db_password" {
  description = "データベースパスワード"
  type        = string
  sensitive   = true
}
# outputs.tf
output "vpc_id" {
  description = "作成されたVPCのID"
  value       = aws_vpc.main.id
}

output "alb_dns_name" {
  description = "ALBのDNS名"
  value       = aws_lb.main.dns_name
}

3. HCL構文の深掘り

ブロック構造

HCL(HashiCorp Configuration Language)の基本構造はブロックです。ブロックはタイプ、ラベル、ボディで構成されます。

# ブロックタイプ "ラベル1" "ラベル2" {
#   属性 = 値
# }

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type

  root_block_device {
    volume_size = 20
    volume_type = "gp3"
  }
}

型システム

HCLは豊富な型システムをサポートしています。

# プリミティブ型
variable "name" {
  type = string
}

variable "port" {
  type = number
}

variable "enabled" {
  type = bool
}

# コレクション型
variable "availability_zones" {
  type = list(string)
}

variable "instance_tags" {
  type = map(string)
}

variable "allowed_ports" {
  type = set(number)
}

# 構造体型
variable "database_config" {
  type = object({
    engine         = string
    engine_version = string
    instance_class = string
    allocated_storage = number
    multi_az       = bool
  })
}

条件式

条件式は三項演算子の形式で記述します。

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"

  monitoring = var.environment == "prod" ? true : false
}

ループ - countとfor_each

countは数値ベースの反復に適しています。

resource "aws_subnet" "private" {
  count = 3

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 10)
  availability_zone = data.aws_availability_zones.available.names[count.index]

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

for_eachはマップやセットベースの反復に適しており、途中の項目を削除してもインデックスがずれません。

variable "subnets" {
  type = map(object({
    cidr_block        = string
    availability_zone = string
    public            = bool
  }))
}

resource "aws_subnet" "this" {
  for_each = var.subnets

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

  map_public_ip_on_launch = each.value.public

  tags = {
    Name = each.key
  }
}

for式

locals {
  # リスト変換
  subnet_ids = [for s in aws_subnet.this : s.id]

  # 条件付きフィルタリング
  public_subnet_ids = [for k, s in aws_subnet.this : s.id if s.map_public_ip_on_launch]

  # マップ変換
  subnet_id_map = { for k, s in aws_subnet.this : k => s.id }
}

ローカル変数

ローカル変数は、モジュール内で繰り返し使用する値を一箇所で定義します。

locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "terraform"
    Team        = "platform"
  }

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

  is_production = var.environment == "prod"
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = local.is_production ? "t3.large" : "t3.micro"

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-web"
    Role = "webserver"
  })
}

4. 状態管理

terraform.tfstateとは

Terraformは管理中のインフラの現在の状態をStateファイルに記録します。planコマンドはStateファイルの状態とコードで宣言された状態を比較して変更点を計算します。

Stateファイルには機密情報(パスワード、キーなど)が含まれる可能性があるため、ローカルファイルシステムに保存するのは危険です。

リモートバックエンド (S3 + DynamoDB)

チーム環境ではリモートバックエンドを使用してStateを中央で管理する必要があります。AWSではS3 + DynamoDBの組み合わせが標準です。

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

S3バケットには必ずバージョニングを有効化し、サーバーサイド暗号化を適用する必要があります。

resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state-bucket"

  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_lock" {
  name         = "terraform-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

State Locking

DynamoDBテーブルはStateロックを提供します。2人が同時にterraform applyを実行すると、先に実行した人がロックを取得し、後から実行した人は待機します。

ロックが解放されない場合(プロセスの異常終了など)は、terraform force-unlock LOCK_IDコマンドで強制解除できます。ただし、他の人が実際に作業中でないことを必ず確認してください。

State管理コマンド

# Stateのリソース一覧確認
terraform state list

# 特定リソースの状態確認
terraform state show aws_vpc.main

# リソース名の変更(コードで名前を変えた場合)
terraform state mv aws_vpc.main aws_vpc.primary

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

# 既存インフラをStateにインポート
terraform import aws_vpc.existing vpc-0123456789abcdef0

5. モジュール

モジュールとは

モジュールは関連するリソースを1つのパッケージにまとめたものです。コードの再利用性を高め、関心事を分離し、チーム間で標準化されたインフラパターンを共有できるようにします。

モジュールのディレクトリ構造

modules/
  vpc/
    main.tf
    variables.tf
    outputs.tf
    README.md
  ec2/
    main.tf
    variables.tf
    outputs.tf
  rds/
    main.tf
    variables.tf
    outputs.tf

モジュールの作成

# 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_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

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

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}"
    Tier = "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}"
    Tier = "private"
  })
}
# modules/vpc/variables.tf
variable "name_prefix" {
  description = "リソース名のプレフィックス"
  type        = string
}

variable "cidr_block" {
  description = "VPC CIDRブロック"
  type        = string
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  description = "パブリックサブネットCIDRリスト"
  type        = list(string)
}

variable "private_subnet_cidrs" {
  description = "プライベートサブネットCIDRリスト"
  type        = list(string)
}

variable "availability_zones" {
  description = "アベイラビリティゾーンリスト"
  type        = list(string)
}

variable "tags" {
  description = "共通タグ"
  type        = map(string)
  default     = {}
}
# modules/vpc/outputs.tf
output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.this.id
}

output "public_subnet_ids" {
  description = "パブリックサブネットIDリスト"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "プライベートサブネットIDリスト"
  value       = aws_subnet.private[*].id
}

モジュールの呼び出し

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

  name_prefix          = "myapp-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-2c"]

  tags = local.common_tags
}

# モジュール出力の参照
resource "aws_instance" "web" {
  subnet_id = module.vpc.public_subnet_ids[0]
  # ...
}

Terraform Registryモジュール

Terraform RegistryにはコミュニティとHashiCorpが検証したモジュールが公開されています。

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-2c"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]

  enable_nat_gateway   = true
  single_nat_gateway   = false
  enable_dns_hostnames = true
}

6. Terragrunt

Terragruntとは

TerragruntはTerraformのラッパーツールで、DRY(Don't Repeat Yourself)原則を適用して重複設定を排除します。複数の環境(dev、staging、prod)を管理する際に特に有用です。

ディレクトリ構造

infrastructure/
  terragrunt.hcl              # ルート設定
  environments/
    dev/
      terragrunt.hcl          # dev環境共通設定
      vpc/
        terragrunt.hcl
      ec2/
        terragrunt.hcl
      rds/
        terragrunt.hcl
    staging/
      terragrunt.hcl
      vpc/
        terragrunt.hcl
      ec2/
        terragrunt.hcl
    prod/
      terragrunt.hcl
      vpc/
        terragrunt.hcl
      ec2/
        terragrunt.hcl
      rds/
        terragrunt.hcl
  modules/
    vpc/
    ec2/
    rds/

ルート設定

# infrastructure/terragrunt.hcl
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket         = "my-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "ap-northeast-2"
}
EOF
}

環境別設定

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

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

inputs = {
  name_prefix          = "myapp-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-2c"]
}

依存関係管理

Terragruntはモジュール間の依存関係を明示的に宣言できます。

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

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

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

inputs = {
  vpc_id    = dependency.vpc.outputs.vpc_id
  subnet_id = dependency.vpc.outputs.public_subnet_ids[0]
}

terragrunt run-all applyを実行すると、依存関係の順序に従ってVPCを先に作成し、EC2を後から作成します。


7. ワークフロー

コアコマンド

# プロバイダプラグインのダウンロードと初期化
terraform init

# 変更計画の確認(実際の変更なし)
terraform plan

# 変更の適用
terraform apply

# 特定リソースのみ適用
terraform apply -target=aws_vpc.main

# インフラ全体の削除
terraform destroy

# コードフォーマット
terraform fmt -recursive

# 設定の妥当性検証
terraform validate

# 依存関係グラフの出力
terraform graph | dot -Tpng > graph.png

Planファイルの保存

# plan結果をファイルに保存
terraform plan -out=tfplan

# 保存されたplanをそのまま適用(追加確認なし)
terraform apply tfplan

planファイルを使用すると、plan時点とapply時点の間にコードが変更されてもplan時点の変更のみが適用されます。

CI/CD連携

GitHub Actionsを使用したTerraform CI/CDパイプラインの例です。

name: Terraform CI/CD

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

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

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

      - name: Terraform Init
        working-directory: infrastructure
        run: terraform init

      - name: Terraform Format Check
        working-directory: infrastructure
        run: terraform fmt -check -recursive

      - name: Terraform Validate
        working-directory: infrastructure
        run: terraform validate

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

      - name: Comment Plan on PR
        uses: actions/github-script@v7
        with:
          script: |
            const output = `#### Terraform Plan
            \`\`\`
            Plan output here
            \`\`\`
            `;

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

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

      - name: Terraform Init
        working-directory: infrastructure
        run: terraform init

      - name: Terraform Apply
        working-directory: infrastructure
        run: terraform apply -auto-approve

8. AWS実践例 - VPC + EC2 + RDS + ALB

全体アーキテクチャ

この例では以下のインフラを構築します。

  • VPC(パブリックサブネット2つ + プライベートサブネット2つ)
  • Application Load Balancer (ALB)
  • EC2インスタンス (Auto Scaling Group)
  • RDS PostgreSQL (Multi-AZ)
  • Security Groupチェーン

VPCとネットワーク

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

  name = "${local.name_prefix}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-2a", "ap-northeast-2c"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]

  enable_nat_gateway   = true
  single_nat_gateway   = true
  enable_dns_hostnames = true

  public_subnet_tags = {
    Tier = "public"
  }

  private_subnet_tags = {
    Tier = "private"
  }

  tags = local.common_tags
}

Security Groups

# security_groups.tf
resource "aws_security_group" "alb" {
  name_prefix = "${local.name_prefix}-alb-"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

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

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-alb-sg"
  })

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_security_group" "app" {
  name_prefix = "${local.name_prefix}-app-"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

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

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-app-sg"
  })

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_security_group" "rds" {
  name_prefix = "${local.name_prefix}-rds-"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-rds-sg"
  })

  lifecycle {
    create_before_destroy = true
  }
}

ALB

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

  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                = "/health"
    healthy_threshold   = 2
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
  }

  tags = local.common_tags
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

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

Auto Scaling Group

# asg.tf
resource "aws_launch_template" "app" {
  name_prefix   = "${local.name_prefix}-app-"
  image_id      = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type

  vpc_security_group_ids = [aws_security_group.app.id]

  user_data = base64encode(<<-SCRIPT
    #!/bin/bash
    yum update -y
    yum install -y docker
    systemctl start docker
    systemctl enable docker
    docker run -d -p 8080:8080 myapp:latest
    SCRIPT
  )

  tag_specifications {
    resource_type = "instance"
    tags = merge(local.common_tags, {
      Name = "${local.name_prefix}-app"
    })
  }
}

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

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

  tag {
    key                 = "Name"
    value               = "${local.name_prefix}-app"
    propagate_at_launch = true
  }
}

RDS

# rds.tf
resource "aws_db_subnet_group" "main" {
  name       = "${local.name_prefix}-db-subnet"
  subnet_ids = module.vpc.private_subnets

  tags = local.common_tags
}

resource "aws_db_instance" "main" {
  identifier     = "${local.name_prefix}-postgres"
  engine         = "postgres"
  engine_version = "15.4"
  instance_class = "db.t3.medium"

  allocated_storage     = 20
  max_allocated_storage = 100
  storage_encrypted     = true

  db_name  = "myapp"
  username = "admin"
  password = var.db_password

  multi_az               = true
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  backup_retention_period = 7
  skip_final_snapshot     = false
  final_snapshot_identifier = "${local.name_prefix}-final-snapshot"

  tags = local.common_tags
}

9. セキュリティ

Sensitive変数

sensitiveとしてマークされた変数はplan/apply出力でマスキングされます。

variable "db_password" {
  type      = string
  sensitive = true
}

output "db_endpoint" {
  value = aws_db_instance.main.endpoint
}

output "db_password" {
  value     = var.db_password
  sensitive = true
}

機密値の管理方法

  1. 環境変数: TF_VAR_db_password環境変数を通じて注入します。
  2. terraform.tfvarsファイル: .gitignoreに追加してバージョン管理から除外します。
  3. Vault連携: HashiCorp Vaultから動的にシークレットを取得します。
# VaultからDBパスワードを取得
data "vault_generic_secret" "db" {
  path = "secret/data/production/database"
}

resource "aws_db_instance" "main" {
  password = data.vault_generic_secret.db.data["password"]
  # ...
}
  1. AWS Secrets Manager連携
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "production/database/password"
}

resource "aws_db_instance" "main" {
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
  # ...
}

ポリシー検査 - OPA (Open Policy Agent)

OPAを使用してTerraform planにポリシーを強制できます。

# policy/terraform.rego
package terraform

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_s3_bucket"
  not resource.change.after.server_side_encryption_configuration
  msg := "S3バケットには必ず暗号化を設定する必要があります。"
}

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_security_group_rule"
  resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
  resource.change.after.from_port == 22
  msg := "SSH(22)ポートをインターネット全体(0.0.0.0/0)に開放することはできません。"
}
# planをJSONで出力
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json

# OPAでポリシー検査
opa eval --data policy/ --input tfplan.json "data.terraform.deny"

tfsec / Trivy 静的解析

# tfsecでセキュリティスキャン
tfsec .

# TrivyでIaCスキャン
trivy config .

10. ベストプラクティス

ディレクトリ構造

project/
  environments/
    dev/
      main.tf
      variables.tf
      terraform.tfvars
      backend.tf
    staging/
      main.tf
      variables.tf
      terraform.tfvars
      backend.tf
    prod/
      main.tf
      variables.tf
      terraform.tfvars
      backend.tf
  modules/
    vpc/
    ec2/
    rds/
    alb/
  global/
    iam/
    route53/

命名規則

  • リソース名: snake_caseを使用 (aws_security_group.web_server)
  • 変数名: snake_caseを使用 (instance_type, db_password)
  • ファイル名: リソースタイプ別に分離 (vpc.tf, ec2.tf, rds.tf)
  • タグ値: 環境-サービス-ロールパターン (prod-myapp-web)

コードレビューチェックリスト

  1. セキュリティ: シークレットがハードコードされていないか?Security Groupが過度に開放されていないか?
  2. コスト: インスタンスタイプは適切か?未使用のリソースはないか?
  3. 可用性: Multi-AZが適用されているか?Auto Scalingが設定されているか?
  4. 状態管理: Stateキーは適切か?リモートバックエンドが設定されているか?
  5. モジュール化: 重複コードをモジュールに抽出できるか?
  6. タグ付け: すべてのリソースに必須タグがあるか?

よくある間違いと解決策

間違い解決策
StateファイルをGitにコミット.gitignoreに追加、リモートバックエンド使用
シークレットのハードコードsensitive変数、Vault、Secrets Manager使用
countで作成したリソースの途中項目削除for_eachを使用
プロバイダバージョン未固定required_providersにバージョン明示
plan無しで直接applyCI/CDでplanレビューを必須化
モジュール無しですべてのコードを1ファイルに記述モジュールに分離、関心事別にファイル分離

まとめ

TerraformとIaCは現代のクラウドインフラ管理の基盤です。コードでインフラを定義することで、再現可能で、変更追跡が可能で、コードレビューを通じて品質を高めることができます。核心原則をまとめると以下の通りです。

  1. 宣言的コードでインフラを定義し、Gitでバージョン管理します。
  2. リモートバックエンドでStateを安全に管理し、ロックで同時変更を防止します。
  3. モジュールでコードを再利用し、チーム標準を確立します。
  4. CI/CDパイプラインでplanレビューと自動デプロイを実現します。
  5. ポリシー検査でセキュリティとコンプライアンスを自動化します。

このガイドの例をベースに、自分のプロジェクトに合わせて修正して活用してください。

현재 단락 (1/791)

従来のインフラ管理方式は、管理者がコンソールにアクセスして手動でサーバーをプロビジョニングし、ネットワークを設定するものでした。この方式にはいくつかの問題があります。

작성 글자: 0원문 글자: 19,617작성 단락: 0/791