LESSON 30分

「サーバーにSSHでログインしてパッチを当てる時代は終わった。インフラは作り直すものだ。変更したければ新しいイメージを焼いて丸ごと入れ替える。それがイミュータブルインフラだよ」と佐藤CTOが断言した。

推定読了時間: 30分


Mutable vs Immutable Infrastructure

Mutable(ミュータブル)の問題

graph LR
    V1["Server v1<br/>初期構築"]
    V11["Server v1.1<br/>パッチ適用"]
    V12["Server v1.2<br/>状態不明"]:::snowflake

    V1 -->|"SSH & patch"| V11
    V11 -->|"さらにパッチ"| V12

    V12 -.- Label["Snowflake Server<br/>(二度と再現できない)"]:::warning

    classDef default fill:#1e3a5f,stroke:#4a90d9,color:#e2e2e2
    classDef snowflake fill:#8b0000,stroke:#ff4444,color:#ffffff,stroke-width:3px
    classDef warning fill:#ff6347,stroke:#ff4444,color:#ffffff,stroke-dasharray:5 5
問題説明
Configuration Drift各サーバーの状態が微妙に異なる
Snowflake Server再現不可能な一品物のサーバー
デバッグ困難「あのサーバーだけ動かない」問題
ロールバック不可変更履歴が追えない

Immutable(イミュータブル)の原則

graph LR
    IMG1["Image v1"]:::image
    IMG2["Image v2"]:::image

    subgraph Phase1["現行環境"]
        SA["Server A"]:::server
        SB["Server B"]:::server
    end

    subgraph Phase2["新環境"]
        SC["Server C"]:::newserver
        SD["Server D"]:::newserver
    end

    subgraph Destroy["破棄"]
        SA2["Server A"]:::destroyed
        SB2["Server B"]:::destroyed
    end

    IMG1 -->|"デプロイ"| Phase1
    Phase1 -->|"変更が必要"| IMG2
    IMG2 -->|"デプロイ"| Phase2
    Phase1 -->|"古いサーバーは破棄"| Destroy

    classDef image fill:#0f3460,stroke:#533483,color:#ffffff,stroke-width:2px
    classDef server fill:#1e3a5f,stroke:#4a90d9,color:#e2e2e2
    classDef newserver fill:#006400,stroke:#00ff00,color:#ffffff,stroke-width:2px
    classDef destroyed fill:#555555,stroke:#888888,color:#aaaaaa,stroke-dasharray:5 5

AMI / マシンイメージのベイキング

Packer によるイメージビルド

# packer/app-server.pkr.hcl
packer {
  required_plugins {
    amazon = {
      version = ">= 1.3.0"
      source  = "github.com/hashicorp/amazon"
    }
  }
}

source "amazon-ebs" "app" {
  ami_name      = "app-server-{{timestamp}}"
  instance_type = "t3.medium"
  region        = "ap-northeast-1"

  source_ami_filter {
    filters = {
      name                = "al2023-ami-*-x86_64"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    owners      = ["amazon"]
    most_recent = true
  }

  ssh_username = "ec2-user"

  tags = {
    Name        = "app-server"
    BuildTime   = "{{timestamp}}"
    SourceAMI   = "{{ .SourceAMI }}"
    Environment = "production"
  }
}

build {
  sources = ["source.amazon-ebs.app"]

  # システムパッケージの更新
  provisioner "shell" {
    inline = [
      "sudo dnf update -y",
      "sudo dnf install -y docker",
      "sudo systemctl enable docker",
    ]
  }

  # アプリケーション設定のコピー
  provisioner "file" {
    source      = "configs/"
    destination = "/tmp/configs"
  }

  provisioner "shell" {
    inline = [
      "sudo mv /tmp/configs/* /etc/app/",
      "sudo chown -R root:root /etc/app/",
    ]
  }

  # アプリケーションのデプロイ
  provisioner "shell" {
    script = "scripts/install-app.sh"
  }

  # CIS ベンチマークに基づくハードニング
  provisioner "shell" {
    script = "scripts/harden.sh"
  }

  # イメージのテスト
  post-processor "manifest" {
    output     = "manifest.json"
    strip_path = true
  }
}

イメージテスト(Goss)

# goss.yaml - イメージの検証
port:
  tcp:3000:
    listening: true
    ip:
      - 0.0.0.0

service:
  app:
    enabled: true
    running: true

user:
  appuser:
    exists: true
    shell: /sbin/nologin

file:
  /etc/app/config.yaml:
    exists: true
    mode: "0644"
    owner: root

command:
  check-app-version:
    exec: "/usr/local/bin/app --version"
    exit-status: 0
    stdout:
      - "/v[0-9]+\\.[0-9]+\\.[0-9]+/"

Blue-Green デプロイメント with Immutable Infrastructure

# terraform/blue-green.tf

variable "active_color" {
  description = "Currently active deployment color"
  type        = string
  default     = "blue"
}

variable "blue_ami" {
  type = string
}

variable "green_ami" {
  type = string
}

# Blue 環境
resource "aws_launch_template" "blue" {
  name_prefix   = "app-blue-"
  image_id      = var.blue_ami
  instance_type = "t3.medium"

  tag_specifications {
    resource_type = "instance"
    tags = {
      Color = "blue"
    }
  }
}

resource "aws_autoscaling_group" "blue" {
  name                = "app-blue"
  desired_capacity    = var.active_color == "blue" ? 4 : 0
  min_size            = 0
  max_size            = 8
  vpc_zone_identifier = var.private_subnets

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

  target_group_arns = var.active_color == "blue" ? [aws_lb_target_group.active.arn] : []
}

# Green 環境
resource "aws_launch_template" "green" {
  name_prefix   = "app-green-"
  image_id      = var.green_ami
  instance_type = "t3.medium"

  tag_specifications {
    resource_type = "instance"
    tags = {
      Color = "green"
    }
  }
}

resource "aws_autoscaling_group" "green" {
  name                = "app-green"
  desired_capacity    = var.active_color == "green" ? 4 : 0
  min_size            = 0
  max_size            = 8
  vpc_zone_identifier = var.private_subnets

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

  target_group_arns = var.active_color == "green" ? [aws_lb_target_group.active.arn] : []
}

切り替えフロー

1. 新AMIをビルド(Packer)
2. Green環境にデプロイ(desired_capacity = 4)
3. ヘルスチェック通過を確認
4. ALBのターゲットグループをGreenに切り替え
5. 一定期間経過後、Blue環境を縮退(desired_capacity = 0)
6. 問題発生時はBlueに即座にロールバック

コンテナ環境でのイミュータブル戦略

Kubernetes でのイミュータブルデプロイ

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
        - name: api
          # タグは常にイミュータブル(latest は使わない)
          image: my-registry/api-server:v1.2.3@sha256:abc123...
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          # ReadOnlyRootFilesystem でファイルシステムも不変に
          securityContext:
            readOnlyRootFilesystem: true
            runAsNonRoot: true
            runAsUser: 1000
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}

readOnlyRootFilesystem: true を設定すれば、コンテナ内のファイルシステムへの書き込みを禁止できる。真のイミュータブルだ」


Golden Image パイプライン

graph LR
    BASE["Base OS Image<br/>月次更新"]:::base
    HARD["Hardened Image<br/>セキュリティ<br/>ベースライン"]:::hardened
    APP["App Image<br/>アプリ+設定<br/>を焼き込み"]:::app
    DEPLOY["Deploy<br/>Blue-Green<br/>切り替え"]:::deploy

    BASE -->|"ハードニング"| HARD
    HARD -->|"アプリ組込"| APP
    APP -->|"リリース"| DEPLOY

    classDef base fill:#1a1a2e,stroke:#16213e,color:#e2e2e2,stroke-width:2px
    classDef hardened fill:#16213e,stroke:#0f3460,color:#e2e2e2,stroke-width:2px
    classDef app fill:#0f3460,stroke:#533483,color:#ffffff,stroke-width:2px
    classDef deploy fill:#533483,stroke:#e94560,color:#ffffff,stroke-width:2px
# CI/CD パイプライン
name: Golden Image Pipeline
on:
  schedule:
    - cron: "0 2 * * 0"  # 毎週日曜 AM2:00
  workflow_dispatch:

jobs:
  build-base:
    runs-on: ubuntu-latest
    steps:
      - name: Build Base Image
        run: |
          packer build \
            -var "source_ami=${{ env.LATEST_AL2023 }}" \
            base-image.pkr.hcl

  test-image:
    needs: build-base
    runs-on: ubuntu-latest
    steps:
      - name: Launch test instance
        run: |
          INSTANCE_ID=$(aws ec2 run-instances \
            --image-id ${{ needs.build-base.outputs.ami_id }} \
            --instance-type t3.micro \
            --query 'Instances[0].InstanceId' \
            --output text)
          echo "instance_id=$INSTANCE_ID" >> $GITHUB_OUTPUT

      - name: Run Goss tests
        run: |
          goss -g goss.yaml validate --retry-timeout 120s

      - name: Run CIS benchmark
        run: |
          aws inspector2 create-findings-report \
            --filter-criteria '{"resourceId": [{"comparison": "EQUALS", "value": "${{ steps.launch.outputs.instance_id }}"}]}'

まとめ

概念説明
イミュータブルインフラ変更せず作り直す
AMI ベイキングPacker でマシンイメージを構築
Blue-Green2環境の切り替えによるゼロダウンタイムデプロイ
Golden Imageベースイメージの標準化
ReadOnly FSコンテナのファイルシステムを不変に
イメージテストGoss / InSpec でイメージを検証

チェックリスト

  • 本番サーバーへのSSHログインを禁止している
  • イメージはCIパイプラインで自動ビルドされる
  • デプロイはイメージの入れ替えで行われる
  • ロールバックは前バージョンのイメージに戻すだけ
  • イメージのテスト(Goss等)がパイプラインに組み込まれている
  • コンテナは ReadOnlyRootFilesystem で実行されている

次のステップへ

次のレッスンでは、イミュータブルインフラの運用を自動化する GitOps の原則と実践を学びます。