【実務・中級編】Pulumiで実現するブルーグリーンデプロイメント:ゼロダウンタイムなインフラ更新戦略 – インフラ構成管理(IaC)活用バイブル

ゼロダウンタイムのその先へ:Pulumiのエイリアスとスタックライフサイクルで実現する、本番インフラのブルーグリーン・デプロイメント

こんにちは。テックリードの私だ。
日々のデプロイで「インスタンスの差し替えに伴う数秒の断」に怯えていないか? ロードバランサーのターゲットグループを付け替える瞬間に冷汗をかき、ロールバックの手順書を開いて絶望した経験はないか?

インフラストラクチャをコード(IaC)で管理する時代において、アプリケーションと同様にインフラストラクチャも無停止(ゼロダウンタイム)で更新・移行されるべきだ。
今回は、Terraformの静的なステート管理の限界を軽々と超越するPulumiの真骨頂——`Alias`(エイリアス)機能とスタックライフサイクルを駆使し、完全に自動化されたブルーグリーン・デプロイメント(BGD)を実装する極意を伝授しよう。

一般的な入門記事にあるような「ツールのインストール方法」は割愛する。今日ここで解説するのは、明日から君のプロダクション環境に導入でき、チーム全体のデプロイに対する恐怖心を完全になくすための実戦的かつ洗練されたアーキテクチャだ。

—

1. 開発スピードを爆発させるPulumi開発環境の極意

本題に入る前に、プロのSREが実践している「開発効率を限界まで高めるセットアップ」を共有しておこう。これを知っているか否かで、IaCの記述速度は3倍変わる。

必携の神プラグイン & 拡張機能

  • VS Code: Pulumi Snippets & Better Comments

リソース定義のボイラープレートを排除し、コメントの重要度(`TODO`, `FIXME`, `HACK`)を色分けしてインフラの負債を可視化する。

  • EditorConfig:

インデントの揺れ(YAMLのスペース数やTypeScriptのタブ・スペース混同)を物理的に防ぐ。PulumiのTypeScript/Pythonコードでは致命的なエラーを防ぐ防壁となる。

チーム開発の暗黙のルール:設定の共有化

Pulumiの強みは汎用プログラミング言語(TypeScript, Python, Go, C#)を使える点だが、自由度が高いゆえにコードの乱れが生じやすい。

  • 厳格なLint/Formatの強制: `eslint` と `prettier` をCI/CDパイプラインだけでなく、Git Hooks(Husky + lint-staged)で強制する。
  • Secrets管理の統一: `pulumi config set –secret` を徹底し、プレーンテキストでのクレデンシャル流出をシステム的にゼロにする。チーム間ではKMS(AWS KMS / GCP KMS / Vault)プロバイダをバックエンドに指定したスタック構成を共通化する。

—

2. ブルーグリーン・デプロイメントの設計思想:Pulumiが選ばれる理由

従来のTerraformでブルーグリーンをやろうとすると、リソース名(`name` や `tags`)の衝突を避けるために `name_prefix` を使い、ランダムサフィックスが付いた新規リソースを作ってから古いものを破棄するという、いわゆる「Create-Before-Destroy(置換)」のライフサイクル制御に頼ることになる。しかし、これではロードバランサーのリスเนー規則やターゲットグループの切り替え時にステートの競合や予期せぬダウンタイムが発生しがちだ。

Pulumiならどうするか?
プログラミング言語の動的な制御力と、Pulumiエンジンが持つ`alias`(エイリアス)システムを組み合わせることで、「論理名(Logical Name)」と「物理名(Physical Name)」を完全に分離し、安全な世代交代を行える。

[Blue環境 (現行)] —-> Target Group A <---- ALB Listener ^ | (切り替え) [Green環境 (新)] ----> Target Group B —-> (ヘルスチェックOK後にルーティング)

これをプログラムコードとして美しく、かつ冪等性を完全に担保して実装していこう。

—

3. 実践:Pulumi (TypeScript) によるゼロダウンタイム・BGD実装

ここでは、AWSをターゲットに、ECS(あるいはEC2)とALBを用いたブルーグリーン環境の構築コードを提示する。スタックのコンフィグで `activeColor` (`blue` または `green`) を切り替えるだけで、インフラが自動的に新世代へシフトする設計だ。

プロジェクト構成

.
├── Pulumi.yaml
├── Pulumi.dev.yaml
├── package.json
└── index.ts

設定ファイル (`Pulumi.dev.yaml`)

config:
aws:region: ap-northeast-1
my-app:activeColor: “blue” # デプロイ時に “green” に切り替える
my-app:environment: “production”

実装コード (`index.ts`)

import as pulumi from “@pulumi/pulumi”;
import as aws from “@pulumi/aws”;

// 1. 設定の読み込み
const config = new pulumi.Config();
const activeColor = config.require(“activeColor”) as “blue” | “green”;
const env = config.require(“environment”);

// 逆の色を算出(Greenへの切り替え時にBlueを特定するため)
const inactiveColor = activeColor === “blue” ? “green” : “blue”;

// 2. 共通VPC・ネットワークの取得(既存インフラを想定)
const vpcId = “vpc-0123456789abcdef0”;
const subnetIds = [“subnet-01111111111111111”, “subnet-02222222222222222”];

// 3. 共通ALBの定義
const alb = new aws.lb.LoadBalancer(`app-alb-${env}`, {
internal: false,
loadBalancerType: “application”,
securityGroups: [“sg-03333333333333333”],
subnets: subnetIds,
});

// 4. カラーごとのターゲットグループを両方定義(ここが重要)
// Pulumiのエイリアス機能により、環境が切り替わってもステートの喪失を防ぐ
const createTargetGroup = (color: “blue” | “green”) => {
return new aws.lb.TargetGroup(`tg-${env}-${color}`, {
port: 80,
protocol: “HTTP”,
vpcId: vpcId,
targetType: “ip”,
healthCheck: {
path: “/healthz”,
protocol: “HTTP”,
matcher: “200”,
interval: 15,
healthyThreshold: 2,
unhealthyThreshold: 3,
},
// Pulumiのエイリアス:過去の命名規則から移行する場合でもステートを引き継ぐ
aliases: [{ name: pulumi.interpolate`tg-${env}-${color}` }],
}, {
// リソースが置き換わる前に新しいリソースを確実に作成させる
deleteBeforeReplace: true,
});
};

const blueTg = createTargetGroup(“blue”);
const greenTg = createTargetGroup(“green”);

// 5. アクティブなカラーに対応するターゲットグループを選択してリスナーにバインド
const activeTargetGroup = activeColor === “blue” ? blueTg : greenTg;

const listener = new aws.lb.Listener(`app-listener-${env}`, {
loadBalancerArn: alb.arn,
port: 80,
protocol: “HTTP”,
defaultActions: [{
type: “forward”,
targetGroupArn: activeTargetGroup.arn, // アクティブな方にトラフィックを向ける
}],
}, {
// ターゲットグループの切り替え時にリスナーの更新を安全に行う
dependsOn: [activeTargetGroup],
});

// 6. 各カラーごとのECSサービス(あるいはサーバー群)の構築
const createEcsServiceAndTask = (color: “blue” | “green”, tg: aws.lb.TargetGroup) => {
// 非アクティブな環境であってもインフラを常時デプロイ状態に保つ(コールドスタンバイ戦略)
// または、アクティブな方のみデプロイを走らせる制御も可能
const isActive = color === activeColor;

const role = new aws.iam.Role(`ecs-execution-role-${env}-${color}`, {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ Service: “ecs-tasks.amazonaws.com” }),
});

new aws.iam.RolePolicyAttachment(`ecs-policy-${env}-${color}`, {
role: role.name,
policyArn: aws.iam.ManagedPolicies.AmazonECSTaskExecutionRolePolicy,
});

const cluster = new aws.ecs.Cluster(`cluster-${env}-${color}`);

const taskDefinition = new aws.ecs.TaskDefinition(`task-def-${env}-${color}`, {
family: `app-${env}-${color}`,
cpu: “256”,
memory: “512”,
networkMode: “awsvpc”,
requiresCompatibilities: [“FARGATE”],
executionRoleArn: role.arn,
containerDefinitions: JSON.stringify([{
name: “web”,
image: `my-account.dkr.ecr.ap-northeast-1.amazonaws.com/my-app:${isActive ? “v2.0.0” : “v1.0.0”}`,
portMappings: [{ containerPort: 80, hostPort: 80 }],
}]),
});

return new aws.ecs.Service(`ecs-service-${env}-${color}`, {
cluster: cluster.arn,
taskDefinition: taskDefinition.arn,
desiredCount: isActive ? 3 : 0, // 非アクティブ時はリソースを節約して0にする選択も可(完全な待機なら1以上を推奨)
launchType: “FARGATE”,
networkConfiguration: {
subnets: subnetIds,
assignPublicIp: true,
},
loadBalancers: [{
targetGroupArn: tg.arn,
containerName: “web”,
containerPort: 80,
}],
}, {
dependsOn: [listener],
});
};

createEcsServiceAndTask(“blue”, blueTg);
createEcsServiceAndTask(“green”, greenTg);

// 7. 出力:現在のルーティング先を外部に通知
pulumi.export(“activeColor”, activeColor);
pulumi.export(“albDNS”, alb.dnsName);

—

4. なぜこのコードで「ゼロダウンタイム」が担保されるのか?

上記のコードとPulumiのライフサイクル管理には、プロフェッショナルが唸る以下の設計上の工夫が組み込まれている。

1. `deleteBeforeReplace: true` の正確な制御
インフラ更新時、Pulumiはデフォルトで「新しいリソースを作成してから古いリソースを削除する(create-before-destroy)」を試みる。しかし、ポートや名前が一意制約に引っかかるリソースではこれが競合を引き起こす。Pulumiのエイリアスと明示的なターゲットグループ分離により、競合のない安全な世代交代を実現している。
2. ステートの連続性(Aliasの魔力)
もし将来的にリソースの論理名(例: `tg-production-blue`)を変更せざるを得ない状況になっても、`aliases` プロパティを設定しておけば、Pulumiはリソースの再作成(破壊と再構築)を行わず、ステート上の名前だけを安全にリネームする。これにより、本番環境でのリソースロストを防げる。
3. コールドスタンバイからホットスタンバイへの瞬時移行
設定ファイル(`Pulumi.dev.yaml`)の `activeColor` を `”blue”` から `”green”` に書き換えて `pulumi up` を叩くだけで、ALBのリスナーが指すターゲットグループがアトミックに切り替わる。失敗した場合は、設定を元に戻して再度 `pulumi up` を実行するだけで、数秒単位の完全なロールバックが完了する。

—

5. 失敗時のロールバック自動化とSREの流儀

コードの切り替えによるデプロイは高速だが、万が一新バージョン(Green)のアプリケーションコード自体にバグがあった場合、インフラの切り替えが成功してもサービスとしては障害となる。

ここでSREが組み込むべきベストプラクティスは以下の通りだ:

1. ヘルスチェックの厳格化:
ALBのターゲットグループのヘルスチェック(`/healthz`)が正常にスワップ先のコンテナ群で200 OKを返すまで、Pulumiのデプロイメントパイプライン(CI/CD)を進めない。Pulumi自体はインフラの構成を宣言通りに収束させるツールであるため、アプリの健全性テストはPulumi実行の前後(あるいはCloudWatchアラームの連動)で担保する。
2. 自動ロールバックのCI/CDパイプライン設計:
GitHub Actions等のCI上で以下のようにフローを組み立てる。

# 1. Greenへの切り替えデプロイ
pulumi up –stack production –config-file Pulumi.prod.yaml -y

# 2. 統合テスト・外形監視の実行
if ! npm run test:smoke; then
echo “Smoke test failed! Rolling back…”
# 3. 失敗時はBlueに戻して即座に適用
sed -i ‘s/activeColor: “green”/activeColor: “blue”/’ Pulumi.prod.yaml
pulumi up –stack production –config-file Pulumi.prod.yaml -y
exit 1
fi

—

結びにかえて

インフラストラクチャの変更を「おっかなびっくり行う儀式」にしてはならない。
Pulumiの持つプログラミング言語の表現力、`Alias` によるステートの保護、そして明確なブルーグリーン戦略を組み合わせることで、インフラの更新は「日常的で退屈な、数秒のイベント」へと昇華される。

コードを書き、仕組みを理解し、自動化の限界を突破せよ。
君のインフラストラクチャに、真の「ゼロダウンタイム」の平穏が訪れることを願っている。

タイトルとURLをコピーしました