Skip to content

✍️ 필사 모드: IaCパターン&ベストプラクティス2025:Terraformモジュール、Pulumi、Crossplane、状態管理

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

目次

1. IaCランドスケープ2025:ツール比較(ひかく)と選択基準(せんたくきじゅん)

Infrastructure as Code(IaC)は、インフラをコードで定義(ていぎ)しバージョン管理(かんり)するDevOpsの核心的(かくしんてき)実践法(じっせんほう)です。2025年現在、IaCエコシステムは多様(たよう)なツールが共存(きょうぞん)し、それぞれ固有(こゆう)の強みを持っています。

1.1 主要IaCツール比較

ツール言語状態管理クラウド対応特徴
Terraform/OpenTofuHCLリモート状態ファイルマルチクラウド最大のエコシステム、豊富なプロバイダー
PulumiTS/Python/Go/C#Pulumi Cloud/自前バックエンドマルチクラウド汎用プログラミング言語使用
CDKTFTS/Python/Go/C#TerraformバックエンドマルチクラウドCDK構文+Terraformプロバイダー
AWS CDKTS/Python/Go/C#CloudFormationAWS専用AWSネイティブ、L2/L3コンストラクト
CrossplaneYAML(K8s CRD)K8s etcdマルチクラウドK8sネイティブ、GitOps親和的
AnsibleYAMLステートレス(手続き型)マルチクラウド構成管理+プロビジョニング

1.2 宣言的(せんげんてき)vs 命令的(めいれいてき)IaC

宣言的(Declarative)IaC:
┌─────────────────────────────────────────────────┐
│  「EC2インスタンスが3台欲しい」                     │
│  → ツールが現在の状態と比較し、必要な変更を計算      │
│  → Terraform, Pulumi, CloudFormation              │
└─────────────────────────────────────────────────┘

命令的(Imperative)IaC:
┌─────────────────────────────────────────────────┐
│  「EC2インスタンスを作成せよ、SGをアタッチせよ」     │
│  → 順番にコマンドを実行                            │
│  → Ansible, Shell Scripts                         │
└─────────────────────────────────────────────────┘

1.3 OpenTofu vs Terraform

2023年のHashiCorpライセンス変更(へんこう)(BSL)後、OpenTofu がLinux Foundationプロジェクトとして誕生(たんじょう)しました。

# OpenTofu と Terraform は同一の HCL 構文を使用
# opentofu init / terraform init どちらも同様に動作

terraform {
  required_version = ">= 1.6.0"

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

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

2. Terraformモジュール設計(せっけい)パターン

Terraformモジュールは再利用可能(さいりようかのう)なインフラコードの核心単位(たんい)です。正しいモジュール設計は、組織全体(そしきぜんたい)のインフラ一貫性(いっかんせい)と生産性(せいさんせい)を決定します。

2.1 フラットモジュール vs ネストモジュール

フラットモジュール構造(推奨):
modules/
├── vpc/
│   ├── main.tf
│   ├── variables.tf
│   ├── outputs.tf
│   └── versions.tf
├── ecs-cluster/
│   ├── main.tf
│   ├── variables.tf
│   ├── outputs.tf
│   └── versions.tf
└── rds/
    ├── main.tf
    ├── variables.tf
    ├── outputs.tf
    └── versions.tf

ネストモジュール構造(複雑性増加):
modules/
└── platform/
    ├── main.tf          # vpc, ecs, rds モジュールを呼び出し
    ├── modules/
    │   ├── vpc/
    │   ├── ecs-cluster/
    │   └── rds/
    └── outputs.tf

2.2 コンポジションパターン

モジュールコンポジションは、小さなモジュールを組み合わせてより大きなインフラを構成(こうせい)するパターンです。

# environments/prod/main.tf
# コンポジションパターン:小さなモジュールを組み合わせ

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

  name       = "prod-vpc"
  cidr_block = "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  # 本番環境はAZごとにNAT
}

module "ecs_cluster" {
  source = "../../modules/ecs-cluster"

  name       = "prod-cluster"
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnet_ids

  capacity_providers = ["FARGATE", "FARGATE_SPOT"]
}

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

  name               = "prod-db"
  engine             = "aurora-postgresql"
  engine_version     = "15.4"
  instance_class     = "db.r6g.xlarge"
  vpc_id             = module.vpc.vpc_id
  subnet_ids         = module.vpc.database_subnet_ids
  security_group_ids = [module.ecs_cluster.security_group_id]
}

2.3 ファクトリーパターン

同じモジュールを複数回(ふくすうかい)インスタンス化するパターンです。

# サービスファクトリーパターン
variable "services" {
  type = map(object({
    cpu    = number
    memory = number
    port   = number
    count  = number
    health_check_path = string
  }))
  default = {
    "api-gateway" = {
      cpu    = 512
      memory = 1024
      port   = 8080
      count  = 3
      health_check_path = "/health"
    }
    "user-service" = {
      cpu    = 256
      memory = 512
      port   = 8081
      count  = 2
      health_check_path = "/actuator/health"
    }
    "order-service" = {
      cpu    = 512
      memory = 1024
      port   = 8082
      count  = 2
      health_check_path = "/health"
    }
  }
}

module "ecs_services" {
  source   = "../../modules/ecs-service"
  for_each = var.services

  name              = each.key
  cluster_id        = module.ecs_cluster.cluster_id
  cpu               = each.value.cpu
  memory            = each.value.memory
  container_port    = each.value.port
  desired_count     = each.value.count
  health_check_path = each.value.health_check_path
  subnet_ids        = module.vpc.private_subnet_ids
}

2.4 ラッパーモジュールパターン

コミュニティモジュールをラップして組織標準(ひょうじゅん)を強制(きょうせい)するパターンです。

# modules/org-s3-bucket/main.tf
# 組織標準を強制するラッパーモジュール

module "s3_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "~> 4.0"

  bucket = var.bucket_name

  # 組織標準:常に暗号化
  server_side_encryption_configuration = {
    rule = {
      apply_server_side_encryption_by_default = {
        sse_algorithm     = "aws:kms"
        kms_master_key_id = var.kms_key_id
      }
    }
  }

  # 組織標準:パブリックアクセスブロック
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true

  # 組織標準:バージョニング有効化
  versioning = {
    enabled = true
  }

  # 組織標準:タグ
  tags = merge(var.tags, {
    ManagedBy   = "terraform"
    Team        = var.team
    Environment = var.environment
    CostCenter  = var.cost_center
  })
}

3. Terraformベストプラクティス

3.1 リモート状態管理

# 状態管理インフラのブートストラップ
# bootstrap/main.tf

resource "aws_s3_bucket" "terraform_state" {
  bucket = "myorg-terraform-state-${data.aws_caller_identity.current.account_id}"

  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"
  }

  tags = {
    Name      = "Terraform State Lock Table"
    ManagedBy = "terraform"
  }
}

3.2 ワークスペース vs ディレクトリ戦略(せんりゃく)

ディレクトリ戦略(推奨):
infrastructure/
├── modules/           # 共有モジュール
│   ├── vpc/
│   ├── ecs/
│   └── rds/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf    # dev専用の状態ファイル
│   ├── staging/
│   └── prod/
└── global/            # 共有リソース(IAM, Route53)

ワークスペース戦略(注意が必要):
- 同じコード、異なる状態 → 環境間の差異が大きくなると管理困難
- terraform workspace select dev / prod
- 状態ファイルが同じバックエンドに保存 → 権限分離が困難

3.3 プロバイダーバージョン管理

# versions.tf
terraform {
  required_version = ">= 1.6.0, < 2.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.30"  # 5.30.x を許可、6.0 は不可
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = ">= 2.24, < 3.0"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.12"
    }
  }
}

# .terraform.lock.hcl は必ず Git にコミット
# terraform init -upgrade でプロバイダーを更新

3.4 変数(へんすう)バリデーションと型安全性(かたあんぜんせい)

variable "environment" {
  type        = string
  description = "デプロイ環境"

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

variable "instance_type" {
  type        = string
  description = "EC2インスタンスタイプ"

  validation {
    condition     = can(regex("^(t3|m6i|c6i|r6i)\\.", var.instance_type))
    error_message = "許可されたインスタンスファミリー:t3, m6i, c6i, r6i"
  }
}

variable "cidr_block" {
  type        = string
  description = "VPC CIDRブロック"

  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "有効なCIDRブロックを入力してください。"
  }
}

# 複合型変数
variable "scaling_config" {
  type = object({
    min_size     = number
    max_size     = number
    desired_size = number
  })

  validation {
    condition     = var.scaling_config.min_size <= var.scaling_config.desired_size
    error_message = "min_sizeはdesired_size以下である必要があります。"
  }

  validation {
    condition     = var.scaling_config.desired_size <= var.scaling_config.max_size
    error_message = "desired_sizeはmax_size以下である必要があります。"
  }
}

4. Pulumiディープダイブ:プログラミング言語によるIaC

Pulumiは、TypeScript、Python、Go、C#などの汎用(はんよう)プログラミング言語でインフラを定義します。条件分岐(じょうけんぶんき)、ループ、抽象化(ちゅうしょうか)など、言語のすべての機能(きのう)を活用(かつよう)できます。

4.1 Pulumi TypeScript 基本構造

// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const environment = config.require("environment");
const vpcCidr = config.get("vpcCidr") || "10.0.0.0/16";

// VPC 作成
const vpc = new aws.ec2.Vpc("main-vpc", {
  cidrBlock: vpcCidr,
  enableDnsHostnames: true,
  enableDnsSupport: true,
  tags: {
    Name: `${environment}-vpc`,
    Environment: environment,
    ManagedBy: "pulumi",
  },
});

// パブリックサブネット(プログラミング言語のループを活用)
const azs = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"];
const publicSubnets = azs.map((az, index) => {
  return new aws.ec2.Subnet(`public-subnet-${index}`, {
    vpcId: vpc.id,
    cidrBlock: `10.0.${index + 1}.0/24`,
    availabilityZone: az,
    mapPublicIpOnLaunch: true,
    tags: {
      Name: `${environment}-public-${az}`,
      Type: "public",
    },
  });
});

// 出力値
export const vpcId = vpc.id;
export const publicSubnetIds = publicSubnets.map(s => s.id);

4.2 Pulumiコンポーネントリソース

// components/ecs-service.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

interface EcsServiceArgs {
  clusterArn: pulumi.Input<string>;
  vpcId: pulumi.Input<string>;
  subnetIds: pulumi.Input<string>[];
  containerImage: string;
  cpu: number;
  memory: number;
  port: number;
  desiredCount: number;
  healthCheckPath: string;
}

export class EcsService extends pulumi.ComponentResource {
  public readonly serviceUrl: pulumi.Output<string>;
  public readonly taskDefinitionArn: pulumi.Output<string>;

  constructor(
    name: string,
    args: EcsServiceArgs,
    opts?: pulumi.ComponentResourceOptions
  ) {
    super("custom:app:EcsService", name, {}, opts);

    const logGroup = new aws.cloudwatch.LogGroup(`${name}-logs`, {
      retentionInDays: 30,
      tags: { Service: name },
    }, { parent: this });

    const taskRole = new aws.iam.Role(`${name}-task-role`, {
      assumeRolePolicy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
          Action: "sts:AssumeRole",
          Effect: "Allow",
          Principal: { Service: "ecs-tasks.amazonaws.com" },
        }],
      }),
    }, { parent: this });

    const taskDefinition = new aws.ecs.TaskDefinition(`${name}-task`, {
      family: name,
      cpu: args.cpu.toString(),
      memory: args.memory.toString(),
      networkMode: "awsvpc",
      requiresCompatibilities: ["FARGATE"],
      executionRoleArn: taskRole.arn,
      taskRoleArn: taskRole.arn,
      containerDefinitions: JSON.stringify([{
        name: name,
        image: args.containerImage,
        cpu: args.cpu,
        memory: args.memory,
        portMappings: [{ containerPort: args.port }],
        logConfiguration: {
          logDriver: "awslogs",
          options: {
            "awslogs-group": logGroup.name,
            "awslogs-region": "ap-northeast-2",
            "awslogs-stream-prefix": "ecs",
          },
        },
      }]),
    }, { parent: this });

    this.taskDefinitionArn = taskDefinition.arn;
    this.registerOutputs({ taskDefinitionArn: this.taskDefinitionArn });
  }
}

4.3 Pulumiスタックリファレンス

// スタック間のデータ共有
// infrastructure/index.ts で VPC 情報を export
export const vpcId = vpc.id;
export const privateSubnetIds = privateSubnets.map(s => s.id);

// application/index.ts で参照
const infraStack = new pulumi.StackReference("org/infrastructure/prod");
const vpcId = infraStack.getOutput("vpcId");
const subnetIds = infraStack.getOutput("privateSubnetIds");

4.4 Pulumi Policy as Code(CrossGuard)

// policy-pack/index.ts
import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";

new PolicyPack("aws-security", {
  policies: [
    {
      name: "s3-no-public-read",
      description: "S3バケットはパブリック読み取りを許可してはなりません",
      enforcementLevel: "mandatory",
      validateResource: validateResourceOfType(aws.s3.BucketAclV2, (acl, args, reportViolation) => {
        if (acl.acl === "public-read" || acl.acl === "public-read-write") {
          reportViolation("S3バケットにパブリックACLが設定されています。");
        }
      }),
    },
    {
      name: "ec2-require-tags",
      description: "EC2インスタンスは必須タグを持つ必要があります",
      enforcementLevel: "mandatory",
      validateResource: validateResourceOfType(aws.ec2.Instance, (instance, args, reportViolation) => {
        const requiredTags = ["Name", "Environment", "Team", "CostCenter"];
        const tags = instance.tags || {};
        for (const tag of requiredTags) {
          if (!(tag in tags)) {
            reportViolation(`必須タグ '${tag}' が不足しています。`);
          }
        }
      }),
    },
  ],
});

5. Crossplane:KubernetesネイティブIaC

Crossplaneは、Kubernetes CRD(Custom Resource Definition)を使用してクラウドリソースを管理(かんり)します。kubectlでAWS、GCP、Azureリソースを作成(さくせい)・管理できます。

5.1 Crossplaneアーキテクチャ

Crossplane アーキテクチャ:
┌─────────────────────────────────────────────────┐
│  K8s Cluster                                     │
│  ┌───────────────────────────────────────────┐   │
│  │  Crossplane Core                          │   │
│  │  ┌─────────┐  ┌──────────┐  ┌─────────┐  │   │
│  │  │Composite│  │Composition│  │  XRD    │  │   │
│  │  │Resource │  │          │  │         │  │   │
│  │  └────┬────┘  └────┬─────┘  └────┬────┘  │   │
│  │       └─────────────┼─────────────┘       │   │
│  └───────────────────┬─┤─────────────────────┘   │
│                      │ │                          │
│  ┌───────────────────┴─┴─────────────────────┐   │
│  │  Providers                                │   │
│  │  ┌──────────┐  ┌──────────┐  ┌─────────┐  │   │
│  │  │AWS Prov. │  │GCP Prov. │  │Azure P. │  │   │
│  │  └────┬─────┘  └────┬─────┘  └────┬────┘  │   │
│  └───────┼──────────────┼─────────────┼──────┘   │
└──────────┼──────────────┼─────────────┼──────────┘
           ▼              ▼             ▼
       AWS Cloud      GCP Cloud    Azure Cloud

5.2 XRD(Composite Resource Definition)定義

# xrd.yaml - プラットフォームチームが定義するAPIスキーマ
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xdatabases.platform.example.com
spec:
  group: platform.example.com
  names:
    kind: XDatabase
    plural: xdatabases
  claimNames:
    kind: Database
    plural: databases
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                engine:
                  type: string
                  enum: ["postgresql", "mysql"]
                  description: "データベースエンジン"
                size:
                  type: string
                  enum: ["small", "medium", "large"]
                  description: "インスタンスサイズ"
                region:
                  type: string
                  default: "ap-northeast-2"
              required:
                - engine
                - size

5.3 Composition定義

# composition.yaml - 実際のリソースマッピング
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xdatabases.aws.platform.example.com
  labels:
    provider: aws
spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1alpha1
    kind: XDatabase
  resources:
    - name: rds-instance
      base:
        apiVersion: rds.aws.upbound.io/v1beta1
        kind: Instance
        spec:
          forProvider:
            engine: postgresql
            engineVersion: "15"
            instanceClass: db.t3.medium
            allocatedStorage: 20
            publiclyAccessible: false
            skipFinalSnapshot: true
      patches:
        - type: FromCompositeFieldPath
          fromFieldPath: "spec.engine"
          toFieldPath: "spec.forProvider.engine"
        - type: FromCompositeFieldPath
          fromFieldPath: "spec.size"
          toFieldPath: "spec.forProvider.instanceClass"
          transforms:
            - type: map
              map:
                small: db.t3.medium
                medium: db.r6g.large
                large: db.r6g.xlarge

5.4 Claimベースプロビジョニング

# claim.yaml - 開発者がリクエストするリソース
apiVersion: platform.example.com/v1alpha1
kind: Database
metadata:
  name: orders-db
  namespace: orders-team
spec:
  engine: postgresql
  size: medium
  region: ap-northeast-2
# 開発者の使用体験
kubectl apply -f claim.yaml
kubectl get databases -n orders-team
# NAME        ENGINE       SIZE    READY   AGE
# orders-db   postgresql   medium  True    5m

6. 状態管理(じょうたいかんり)の深掘(ふかぼ)り

6.1 状態バックエンド比較

S3 + DynamoDB(AWS):
┌─────────────┐     ┌──────────────┐
│ S3 Bucket   │     │ DynamoDB     │
│(状態保存)  │     │(ロック管理)  │
│ バージョン有 │     │ LockIDハッシュ│
│ 暗号化有効   │     │ キーテーブル  │
└─────────────┘     └──────────────┘

GCS(GCP):
┌─────────────────────────────┐
│ GCS Bucket                  │
│(状態保存+組み込みロック)    │
│ バージョン有効、暗号化有効   │
└─────────────────────────────┘

Terraform Cloud / Spacelift:
┌─────────────────────────────┐
│ マネージド状態保存           │
│ 組み込みロック、状態履歴     │
│ アクセス制御、監査ログ       │
└─────────────────────────────┘

6.2 状態ロックと同時実行制御(どうじじっこうせいぎょ)

# 状態ロックの強制解除(注意:他の操作が実行中でないことを確認)
terraform force-unlock LOCK_ID

# 状態ファイルの照会
terraform state list
terraform state show aws_instance.web

# 状態からリソースを除去(削除なし)
terraform state rm aws_instance.legacy

# 既存リソースを状態に取り込み
terraform import aws_instance.web i-1234567890abcdef0

6.3 状態マイグレーション

# moved ブロックでリソース移動(Terraform 1.1+)
moved {
  from = aws_instance.web
  to   = module.compute.aws_instance.web
}

moved {
  from = aws_security_group.web_sg
  to   = module.networking.aws_security_group.web_sg
}

# import ブロックで既存リソースの取り込み(Terraform 1.5+)
import {
  to = aws_instance.legacy_server
  id = "i-0abc123def456789"
}

import {
  to = aws_s3_bucket.existing_bucket
  id = "my-existing-bucket-name"
}

7. IaCテスト戦略

7.1 Terratest

// test/vpc_test.go
package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestVpcModule(t *testing.T) {
    t.Parallel()

    terraformOptions := &terraform.Options{
        TerraformDir: "../modules/vpc",
        Vars: map[string]interface{}{
            "name":       "test-vpc",
            "cidr_block": "10.0.0.0/16",
            "azs":        []string{"us-east-1a", "us-east-1b"},
        },
        NoColor: true,
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcId)

    privateSubnetIds := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
    assert.Equal(t, 2, len(privateSubnetIds))
}

7.2 Checkov静的解析(せいてきかいせき)

# Checkov 実行
checkov -d . --framework terraform

# 特定チェックのスキップ
checkov -d . --skip-check CKV_AWS_18,CKV_AWS_21

# JSONレポート生成
checkov -d . -o json > checkov-report.json
# custom_policy.yaml
metadata:
  id: "CUSTOM_001"
  name: "Ensure S3 bucket has lifecycle policy"
  category: "general"
definition:
  cond_type: "attribute"
  resource_types:
    - "aws_s3_bucket"
  attribute: "lifecycle_rule"
  operator: "exists"

7.3 OPA Conftest

# policy/terraform.rego
package terraform

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_instance"
  not resource.change.after.tags.Environment
  msg := sprintf("EC2インスタンス '%s' にEnvironmentタグがありません", [resource.address])
}

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_s3_bucket"
  resource.change.after.acl == "public-read"
  msg := sprintf("S3バケット '%s' にパブリック読み取りACLが設定されています", [resource.address])
}

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に開放してはなりません"
}
# Conftest 実行
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json -p policy/

8. モノレポ vs ポリレポ戦略

8.1 モノレポ構造

infrastructure/                    # モノレポ
├── .github/
│   └── workflows/
│       ├── terraform-plan.yml     # PRでplan
│       └── terraform-apply.yml    # マージ後apply
├── modules/                       # 共有モジュール
│   ├── vpc/
│   ├── ecs/
│   ├── rds/
│   └── monitoring/
├── environments/
│   ├── shared/                    # 共有リソース(IAM, DNS)
│   ├── dev/
│   ├── staging/
│   └── prod/
├── terragrunt.hcl                 # ルート設定
└── Makefile

8.2 TerragruntでDRYを実現(じつげん)

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

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

inputs = {
  environment = "prod"
  cidr_block  = "10.0.0.0/16"
  azs         = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]

  enable_nat_gateway = true
  single_nat_gateway = false
}

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

9. GitOps for IaC

9.1 Atlantis

# atlantis.yaml
version: 3
automerge: false
parallel_plan: true
parallel_apply: true

projects:
  - name: prod-vpc
    dir: environments/prod
    workspace: default
    terraform_version: v1.7.0
    autoplan:
      when_modified:
        - "*.tf"
        - "../../modules/vpc/**"
      enabled: true
    apply_requirements:
      - approved
      - mergeable
      - undiverged

9.2 Spacelift設定

# .spacelift/config.yml
version: "1"

stacks:
  prod-infra:
    space: production
    project_root: environments/prod
    terraform_version: "1.7.0"
    autodeploy: false
    labels:
      - "env:prod"
      - "team:platform"
    drift_detection:
      enabled: true
      schedule:
        - "0 */6 * * *"  # 6時間ごとにドリフト検知
      reconcile: false   # 自動修正しない

10. ドリフト検知(けんち)と修復(しゅうふく)

10.1 ドリフト検知戦略

# Terraform組み込みドリフト検知
terraform plan -detailed-exitcode
# 終了コード:
# 0 - 変更なし
# 1 - エラー
# 2 - 変更あり(ドリフト検知)

# 自動ドリフト検知スクリプト
#!/bin/bash
set -e

ENVIRONMENTS=("dev" "staging" "prod")

for env in "${ENVIRONMENTS[@]}"; do
  echo "=== Checking drift for $env ==="
  cd "environments/$env"
  terraform init -no-color > /dev/null

  if ! terraform plan -detailed-exitcode -no-color > "/tmp/drift-${env}.txt" 2>&1; then
    EXIT_CODE=$?
    if [ $EXIT_CODE -eq 2 ]; then
      echo "DRIFT DETECTED in $env"
      curl -X POST "$SLACK_WEBHOOK" \
        -H 'Content-Type: application/json' \
        -d "{\"text\": \"Drift detected in ${env} environment\"}"
    fi
  fi
  cd ../..
done

11. IaCにおけるシークレット管理

11.1 HashiCorp Vault連携(れんけい)

# Vault プロバイダー
provider "vault" {
  address = "https://vault.example.com"
}

# Vault からシークレットを読み取り
data "vault_generic_secret" "db_credentials" {
  path = "secret/data/prod/database"
}

resource "aws_db_instance" "main" {
  engine         = "postgres"
  engine_version = "15"
  instance_class = "db.r6g.large"

  username = data.vault_generic_secret.db_credentials.data["username"]
  password = data.vault_generic_secret.db_credentials.data["password"]
}

11.2 SOPS(Secrets OPerationS)

# SOPS で暗号化
sops --encrypt --age age1xxxxx secrets.yaml > secrets.enc.yaml
# Terraform で SOPS を使用
data "sops_file" "secrets" {
  source_file = "secrets.enc.yaml"
}

resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id     = aws_secretsmanager_secret.db.id
  secret_string = data.sops_file.secrets.data["db_password"]
}

12. クイズ

この記事で取り上げたIaCパターンについて理解(りかい)を確認(かくにん)しましょう。

Q1. Terraformモジュール設計

質問:Terraformラッパーモジュールパターンの主な目的は何ですか?

正解:コミュニティモジュールをラップして、組織標準(暗号化、タグ付け、アクセス制御)を強制することです。

ラッパーモジュールは内部的にコミュニティモジュールを呼び出しながら、組織で必須とされる設定(S3暗号化、パブリックアクセスブロック、タグポリシー)をデフォルト値として適用します。開発者はラッパーモジュールを使用するだけで、自動的にセキュリティおよびガバナンスポリシーに準拠できます。

Q2. Pulumi vs Terraform

質問:PulumiがTerraformに対して持つ最大の利点は何ですか?

正解:TypeScript、Python、Goなどの汎用プログラミング言語を使用するため、条件分岐、ループ、抽象化など言語のすべての機能をインフラコードに活用できます。

HCLは宣言的DSLであり、複雑なロジックの実装に制約があります。Pulumiは既存のIDE、デバッガー、テストフレームワーク、パッケージマネージャーをそのまま使用でき、開発者の生産性が向上します。

Q3. Crossplaneアーキテクチャ

質問:CrossplaneにおけるXRD、Composition、Claimの役割をそれぞれ説明してください。

正解:

  • XRD(Composite Resource Definition):プラットフォームチームが定義するAPIスキーマ。開発者に公開するフィールドと型を定義します。
  • Composition:XRDに対する実際のリソースマッピング。1つのXRDに複数のComposition(AWS用、GCP用など)を紐付けできます。
  • Claim:開発者がNamespace単位でリクエストするリソース。XRDスキーマに沿ったシンプルなYAMLを書くと、Compositionが実際のクラウドリソースを作成します。

Q4. 状態管理

質問:Terraform状態ファイルにおけるmovedブロックとimportブロックの違いは何ですか?

正解:

  • movedブロック:すでに状態にあるリソースのアドレスを変更します。モジュールリファクタリング時にリソースを削除/再作成せずに移動できます。
  • importブロック:状態ファイルにないが、実際のクラウドに存在するリソースをTerraform管理下に取り込みます。Terraform 1.5からコード内で宣言的にimportできるようになりました。

Q5. GitOps for IaC

質問:AtlantisとSpaceliftのドリフト検知方式の違いを説明してください。

正解:

  • Atlantis:PRベースのワークフローに焦点。ドリフト検知機能は組み込まれておらず、別途cronスクリプトやCIパイプラインで terraform plan -detailed-exitcode を実行する必要があります。
  • Spacelift:組み込みドリフト検知機能を提供。スケジュール(例:6時間ごと)に従って自動的にplanを実行し、ドリフトが検出されたら通知を送信するか、自動修復(reconcile)オプションを提供します。ポリシーでドリフト対応をコード化できます。

13. 参考資料(さんこうしりょう)

  1. HashiCorp Terraform Documentation - https://developer.hashicorp.com/terraform/docs
  2. Pulumi Documentation - https://www.pulumi.com/docs/
  3. Crossplane Documentation - https://docs.crossplane.io/
  4. OpenTofu Documentation - https://opentofu.org/docs/
  5. Terragrunt Documentation - https://terragrunt.gruntwork.io/docs/
  6. Terratest - https://terratest.gruntwork.io/
  7. Checkov by Bridgecrew - https://www.checkov.io/
  8. Infracost Documentation - https://www.infracost.io/docs/
  9. Atlantis Documentation - https://www.runatlantis.io/docs/
  10. Spacelift Documentation - https://docs.spacelift.io/
  11. SOPS (Secrets OPerationS) - https://github.com/getsops/sops
  12. OPA Conftest - https://www.conftest.dev/
  13. tfsec by Aqua Security - https://aquasecurity.github.io/tfsec/

この記事では、IaCの主要ツール(Terraform、Pulumi、Crossplane)と設計パターン(コンポジション、ファクトリー、ラッパー)、状態管理、テスト、GitOps、ドリフト検知まで包括的(ほうかつてき)に取り上げました。組織の規模と要件に合ったツールとパターンを選択し、テストとセキュリティスキャンをCI/CDに統合(とうごう)することが、成功するIaC運用の鍵(かぎ)です。

현재 단락 (1/766)

Infrastructure as Code(IaC)は、インフラをコードで定義(ていぎ)しバージョン管理(かんり)するDevOpsの核心的(かくしんてき)実践法(じっせんほう)です。2025年現在、...

작성 글자: 0원문 글자: 21,192작성 단락: 0/766