🏗️
フレームワーク #Terraform #Kubernetes #ECS #Helm #Kustomize #GitOps #IaC 📚 TCPIPネットワーク

K8s・ECSのIaC管理(Terraform・Helm・GitOps)

マニフェスト直接管理・Helm・Terraform・Kustomize・GitOpsの全体像と使い分け。TerraformによるECS/K8sリソース管理の実例付き。

IaC全体像:何を使って何を管理するか

┌─────────────────────────────────────────────────────────────────┐
│  インフラ層(VPC / EKS Cluster / RDS / IAM)                    │
│  → Terraform が担当                                             │
├─────────────────────────────────────────────────────────────────┤
│  K8sリソース層(Deployment / Service / Ingress)                │
│  → Helm / Kustomize / kubectl apply が担当                      │
├─────────────────────────────────────────────────────────────────┤
│  継続的デリバリー層(誰がいつ apply するか)                     │
│  → GitOps (ArgoCD / Flux) が担当                               │
└─────────────────────────────────────────────────────────────────┘

※ Terraform の kubernetes/helm provider を使えば K8s リソースも
  Terraform で管理できるが、後述のトレードオフがある

Terraform で ECS を管理する

基本構成

# クラスター
resource "aws_ecs_cluster" "main" {
  name = "production"

  setting {
    name  = "containerInsights"
    value = "enabled"        # CloudWatch Container Insights
  }
}

# タスク定義(コンテナの設計図)
resource "aws_ecs_task_definition" "app" {
  family                   = "my-app"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "512"     # 0.5 vCPU
  memory                   = "1024"    # 1 GB

  # タスクが AWS API を呼ぶための IAM ロール
  task_role_arn      = aws_iam_role.task_role.arn
  # ECS エージェントがイメージをプルするための IAM ロール
  execution_role_arn = aws_iam_role.execution_role.arn

  container_definitions = jsonencode([
    {
      name      = "app"
      image     = "${aws_ecr_repository.app.repository_url}:${var.image_tag}"
      essential = true

      portMappings = [
        { containerPort = 8080, protocol = "tcp" }
      ]

      environment = [
        { name = "ENV", value = "production" }
      ]

      # Secrets Manager からシークレットを注入
      secrets = [
        {
          name      = "DB_PASSWORD"
          valueFrom = "arn:aws:secretsmanager:ap-northeast-1:123456789:secret:db-password"
        }
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = "/ecs/my-app"
          "awslogs-region"        = "ap-northeast-1"
          "awslogs-stream-prefix" = "ecs"
        }
      }

      healthCheck = {
        command     = ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
        interval    = 30
        timeout     = 5
        retries     = 3
        startPeriod = 60
      }
    }
  ])
}

# サービス(タスクを何台動かすか・どうアップデートするか)
resource "aws_ecs_service" "app" {
  name            = "my-app"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = 3
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.app.id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.app.arn
    container_name   = "app"
    container_port   = 8080
  }

  deployment_circuit_breaker {
    enable   = true    # デプロイ失敗時に自動ロールバック
    rollback = true
  }

  lifecycle {
    ignore_changes = [desired_count]  # オートスケールで変更される値は無視
  }
}

Terraform で K8s リソースを管理する

kubernetes provider

terraform {
  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.0"
    }
  }
}

provider "kubernetes" {
  host                   = module.eks.cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks.cluster_ca_certificate)
  token                  = data.aws_eks_cluster_auth.main.token
}

# Namespace
resource "kubernetes_namespace" "production" {
  metadata {
    name = "production"
    labels = {
      environment = "production"
    }
  }
}

# Deployment
resource "kubernetes_deployment" "app" {
  metadata {
    name      = "my-app"
    namespace = kubernetes_namespace.production.metadata[0].name
  }

  spec {
    replicas = 3

    selector {
      match_labels = { app = "my-app" }
    }

    template {
      metadata {
        labels = { app = "my-app" }
      }

      spec {
        container {
          name  = "app"
          image = "my-app:1.2.0"

          port { container_port = 8080 }

          resources {
            requests = { cpu = "250m", memory = "256Mi" }
            limits   = { cpu = "500m", memory = "512Mi" }
          }
        }
      }
    }
  }
}

kubernetes provider のトレードオフ

利点:
  ✓ インフラ(EKSクラスター)と K8sリソースを同じ tfstate で管理
  ✓ Terraform の変数・モジュール機能を使える
  ✓ CI/CD パイプラインを1本化できる

欠点:
  ✗ K8s のローリングアップデートが遅い(Terraform は変更を待つ)
  ✗ kubectl apply の柔軟さがない(helm rollback 相当がない)
  ✗ 複雑なK8sリソース(CRD等)の表現が難しい
  ✗ Terraform の state と K8s の実態がずれやすい

推奨:
  VPC / EKSクラスター / IAM / RDS → Terraform
  Deployment / Service / Ingress → Helm または Kustomize + GitOps

Helm:K8sのパッケージマネージャー

Chart・Values・Release の構造

my-app/                    ← Chart ディレクトリ
├── Chart.yaml             ← Chartのメタ情報(name, version, description)
├── values.yaml            ← デフォルト値
├── values-prod.yaml       ← 本番環境の上書き値(任意)
└── templates/             ← テンプレートディレクトリ
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── configmap.yaml
    └── _helpers.tpl       ← 共通のテンプレート関数定義

values.yaml と templates の関係

# values.yaml
replicaCount: 3
image:
  repository: my-app
  tag: "1.2.0"
resources:
  requests:
    cpu: 250m
    memory: 256Mi
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  replicas: {{ .Values.replicaCount }}      # values から参照
  template:
    spec:
      containers:
        - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          resources:
            requests:
              cpu: {{ .Values.resources.requests.cpu }}

Helm コマンド

# 生成されるマニフェストを確認(apply前の確認に必須)
helm template my-release ./my-app -f values-prod.yaml

# インストール
helm install my-release ./my-app \
  -n production \
  -f values-prod.yaml \
  --set image.tag=1.3.0        # コマンドラインで値を上書き

# アップグレード(リリースが存在しなければインストール)
helm upgrade --install my-release ./my-app \
  -n production \
  -f values-prod.yaml \
  --set image.tag=1.3.0

# ロールバック
helm rollback my-release 2     # リビジョン2に戻す
helm history my-release        # リビジョン履歴を確認

# リリース一覧
helm list -n production

# Artifact Hub からパブリックチャートをインストール
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx

Terraform の helm provider

provider "helm" {
  kubernetes {
    host  = module.eks.cluster_endpoint
    token = data.aws_eks_cluster_auth.main.token
    cluster_ca_certificate = base64decode(module.eks.cluster_ca_certificate)
  }
}

# Helm Release を Terraform で管理
resource "helm_release" "ingress_nginx" {
  name       = "ingress-nginx"
  repository = "https://kubernetes.github.io/ingress-nginx"
  chart      = "ingress-nginx"
  version    = "4.10.0"
  namespace  = "ingress-nginx"
  create_namespace = true

  set {
    name  = "controller.replicaCount"
    value = "2"
  }
}

Kustomize:YAMLの差分管理

Helm のようなテンプレートエンジンを使わず、パッチ(差分) で環境差異を管理する。

k8s/
├── base/                     ← 共通のマニフェスト
│   ├── kustomization.yaml
│   ├── deployment.yaml
│   └── service.yaml
└── overlays/
    ├── staging/              ← ステージング環境の差分
    │   ├── kustomization.yaml
    │   └── replica-patch.yaml
    └── production/           ← 本番環境の差分
        ├── kustomization.yaml
        └── resource-patch.yaml
# base/kustomization.yaml
resources:
  - deployment.yaml
  - service.yaml
# overlays/production/kustomization.yaml
bases:
  - ../../base

patchesStrategicMerge:
  - resource-patch.yaml

images:
  - name: my-app
    newTag: "1.3.0"           # イメージタグだけ上書き
# overlays/production/resource-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 5                 # 本番は5台に上書き
  template:
    spec:
      containers:
        - name: app
          resources:
            requests:
              cpu: "500m"     # 本番はリソースを多く
# 生成されるマニフェストを確認
kubectl kustomize overlays/production/

# 適用
kubectl apply -k overlays/production/

GitOps:ArgoCD / Flux

「Gitリポジトリの状態 = クラスターの状態」を維持する仕組み。

【従来の Push 型CI/CD】
開発者 → push → CI/CD Pipeline → kubectl apply → K8s Cluster
                 ↑ CI/CDがクラスターへの書き込み権限を持つ(セキュリティリスク)

【GitOps(Pull 型)】
開発者 → push → Git Repository
                      ↑ ArgoCD/Flux がリポジトリを監視
                K8s Cluster ← ArgoCD/Flux が変更を検知して apply
                ↑ クラスター側が引っ張る。CI/CDはクラスターに触らない

ArgoCD の概念

Application(ArgoCDのリソース):
  source: Gitリポジトリのパス(マニフェスト or Helm Chart)
  destination: 適用先のクラスター・Namespace
  syncPolicy: 自動同期するかどうか

状態:
  Synced   = GitとClusterが一致している
  OutOfSync = GitとClusterが乖離している(手動変更された等)
  Healthy  = 全リソースが正常動作中
  Degraded = 一部リソースが異常
# Application マニフェスト
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/my-org/k8s-manifests
    targetRevision: main
    path: overlays/production

  destination:
    server: https://kubernetes.default.svc
    namespace: production

  syncPolicy:
    automated:
      prune: true        # Git から消えたリソースを自動削除
      selfHeal: true     # 手動変更を自動で元に戻す
    syncOptions:
      - CreateNamespace=true

選択基準:何をいつ使うか

┌──────────────────────────────────────────────────────────────────┐
│  インフラ(VPC/クラスター/DB/IAM)                               │
│    → Terraform(状態管理・依存解決が必要なリソース)              │
├──────────────────────────────────────────────────────────────────┤
│  サードパーティの K8s アドオン(ingress-nginx, cert-manager等)  │
│    → Helm(公式Chartが配布されているため)                       │
│    → Terraform helm_release でバージョン固定管理も可             │
├──────────────────────────────────────────────────────────────────┤
│  自社アプリの K8s マニフェスト                                   │
│    環境差分が少ない → kubectl apply / Kustomize                  │
│    テンプレートが必要 → Helm(独自Chart)                        │
│    継続的デリバリー → GitOps(ArgoCD/Flux)と組み合わせる        │
└──────────────────────────────────────────────────────────────────┘

組み合わせの王道パターン:
  Terraform      クラスター・RDS・IAM を管理
  Helm           ingress-nginx, cert-manager, prometheus-stack を管理
  Kustomize      自社アプリのマニフェストを環境別に管理
  ArgoCD         Git push → 自動デプロイのパイプラインを担う

アンチパターン

✗ Terraform で全 K8sリソースを管理する
  → terraform apply のたびに Deployment のローリングアップデートを待つ
  → K8sリソースは kubectl / helm / argocd に任せる

✗ kubectl apply を手動でしか行わない
  → 環境間の差分が属人化・ドリフトが発生
  → Git管理 + GitOps で宣言的に管理する

✗ Helm values.yaml に直接シークレットを書く
  → Gitにシークレットが残る
  → SOPS + Helm Secrets / External Secrets Operator を使う