クラウド費用をゼロにする極限のオフラインIaC:Pulumi × LocalStackで構築する超高速ローカル開発要塞
クラウドインフラストラクチャの規模が拡大するにつれ、開発サイクルのボトルネックは常に「フィードバックループの遅延」と「クラウドベンダーへの無駄な従量課金」という二つの悪魔に支配されてきた。
`pulumi up` を実行するたびにAWSのAPI Gatewayがルーティングを確立し、Lambdaがコンテナをコールドスタートさせ、RDSがストレージをプロビジョニングするのを数分間待つ——この愚行に年間どれだけのエンジニアリング人件費とクラウドクレジットが溶かされていることか。
本稿では、Pulumiの強力なプログラマビリティと、AWS完全互換のモック環境であるLocalStackを極限まで結合させ、クラウド費用を完全にゼロに抑えつつ、手元のマシン上で数秒のフィードバックループを実現するオフラインIaCテスト手法の全貌を解き明かす。
単なる「動かし方」の解説ではない。プロバイダの内部エンドポイント書き換えのメカニズム、ステート管理の罠、そしてCI/CDパイプラインへのシームレスな統合まで、プロダクションコードを知り尽くしたSREが到達した「要塞」の設計図をここに提示する。
—
1. アーキテクチャの核心:なぜPulumiとLocalStackなのか
宣言的IaCと命令型SDKの融合が生む俊敏性
Terraform(HCL)によるLocalStack連携は、プロバイダブロックの `endpoints` 設定の煩雑さや、HCL固有の表現力の限界に常に悩まされてきた。一方、PulumiはTypeScript, Python, Goといった汎用プログラミング言語を用いる。これにより、環境(AWS本番 vs LocalStack)に応じたエンドポイントの動的スイッチングや、モックデータの生成ロジックをコードベースで完全に制御できる。
ローカル完結型アーキテクチャの全体像
+—————————————————————+
| Developer Machine |
| |
| +——————–+ +———————-+ |
| | Pulumi CLI / Code | ———> | LocalStack Container | |
| | (TypeScript / Go) | HTTP/REST | (Docker Compose) | |
| +——————–+ +———————-+ |
| | | |
| | (State Backend) | (Mock APIs) |
| v v |
| +———————————————————+ |
| | Local File / MinIO (S3 compatible state backend) | |
| +———————————————————+ |
+—————————————————————+
このアーキテクチャにおいて、AWSの実際のサービスエンドポイント(`.amazonaws.com`)は一切叩かれない。すべてはlocalhost上で稼働するLocalStackのAPIルーター(デフォルトでは `http://localhost:4566`)にルーティングされる。
—
2. 徹底実装:AWSプロバイダをローカルへ強制ルーティングするコード設計
LocalStackをPulumiから操作する場合の最大の勘所は、「AWSプロバイダに対して明示的にカスタムエンドポイントを指定し、かつ不要なAWS認証情報をバイパスすること」である。
以下に、TypeScriptを用いた堅牢なPulumiスタックの構成を示す。ここでは、環境変数やPulumiのコンフィグを検知して、本番環境とローカル環境をシームレスに切り替えるファクトリーパターンを採用している。
プロジェクト構成
.
├── Pulumi.yaml
├── Pulumi.local.yaml
├── index.ts
└── package.json
`index.ts` : 完璧なプロバイダ抽象化とリソース定義
import as pulumi from “@pulumi/pulumi”;
import as aws from “@pulumi/aws”;
// 1. PulumiのコンフィグからLocalStackモードかどうかを取得
const config = new pulumi.Config();
const isLocal = config.requireBoolean(“isLocal”);
const localstackEndpoint = config.get(“localstackEndpoint”) || “http://localhost:4566”;
// 2. プロバイダの初期化(LocalStack用か本番用かを動的にスイッチ)
let awsProvider: aws.Provider;
if (isLocal) {
// LocalStack用のプロバイダ設定
// ダミーの認証情報を渡す必要がある点に注意(LocalStackは署名を検証しないが形式上の値が必要)
awsProvider = new aws.Provider(“localstack-provider”, {
region: “us-east-1”,
s3UsePathStyle: true, // LocalStackのS3はパススタイルが必須
skipCredentialsValidation: true,
skipMetadataApiCheck: true,
skipRequestingAccountId: true,
accessKey: “mock_access_key”,
secretKey: “mock_secret_key”,
endpoints: [
{
s3: localstackEndpoint,
dynamodb: localstackEndpoint,
lambda: localstackEndpoint,
iam: localstackEndpoint,
apigateway: localstackEndpoint,
cloudwatch: localstackEndpoint,
sqs: localstackEndpoint,
},
],
});
} else {
// 本番環境用プロバイダ(デフォルトのAWS環境を使用)
awsProvider = new aws.Provider(“aws-production”, {
region: aws.config.requireRegion(),
});
}
// 3. インフラストラクチャの定義(S3バケットとDynamoDBテーブル)
// すべてのAWSリソースに上で定義した `awsProvider` を明示的にバインドする
const bucket = new aws.s3.Bucket(“my-local-bucket”, {
bucket: “app-assets-local”,
forceDestroy: true, // テスト容易性のためバケット内の強制削除を許可
}, { provider: awsProvider });
const table = new aws.dynamodb.Table(“my-local-table”, {
name: “user-sessions”,
billingMode: “PAY_PER_REQUEST”,
hashKey: “UserId”,
attributes: [
{ name: “UserId”, type: “S” },
],
}, { provider: awsProvider });
// 4. 出力変数のエクスポート
export const bucketName = bucket.id;
export const tableName = table.id;
export const endpointUsed = isLocal ? localstackEndpoint : “AWS Production”;
`Pulumi.local.yaml` : ローカル用コンフィグ
config:
my-project:isLocal: true
my-project:localstackEndpoint: http://localhost:4566
このコードの美しさは、`{ provider: awsProvider }` という単一のオプションにより、テストコード側で一切のロジック変更を行うことなく、本番のAWSとローカルのLocalStackを完全に入れ替えられる点にある。
—
3. ゼロコスト検証フローの自動化:CLI & APIスクリプトによる極限の高速化
手動で `docker compose up` を叩き、`pulumi up` を叩く——これでは真の自動化とは言えない。開発者がターミナルに1コマンド打つだけで、LocalStackの起動、ヘルスチェックの完了、Pulumiの実行、そして統合テストの走破までを完全自動で行うオーケストレーションスクリプトを構築する。
信頼性の高い自動化スクリプト (`scripts/local-test.sh`)
!/usr/bin/env bash
set -euo pipefail
終了時に必ずクリーンアップを行うトラップを設定
trap ‘echo “🧹 Cleaning up LocalStack…”; docker compose down -v’ EXIT
echo “🚀 Starting LocalStack in detached mode…”
docker compose up -d
echo “⏳ Waiting for LocalStack to be fully operational…”
LocalStackのHealth APIをポーリングして起動完了を検知(タイムアウト付き)
TIMEOUT=60
COUNTER=0
while ! curl -s http://localhost:4566/_localstack/health | grep -q ‘”s3″: “running”‘; do
sleep 2
COUNTER=$((COUNTER + 2))
if [ $COUNTER -ge $TIMEOUT ]; then
echo “❌ Error: LocalStack failed to start within $TIMEOUT seconds.”
exit 1
fi
echo -n “.”
done
echo -e “\n✨ LocalStack is ready!”
echo “📦 Initializing Pulumi Stack (Local)…
状態バックエンドをローカルファイルに指定(クラウド不要)
export PULUMI_BACKEND_URL=”file://~/.pulumi/backups”
pulumi stack select local –create
echo “⚡ Applying Pulumi infrastructure to LocalStack…”
pulumi up –stack local –yes –config-file Pulumi.local.yaml
echo “🧪 Running integration tests against LocalStack resources…”
例: AWS CLI経由でLocalStack上のS3にアクセスできるか検証
export AWS_ACCESS_KEY_ID=”mock_access_key”
export AWS_SECRET_ACCESS_KEY=”mock_secret_key”
export AWS_DEFAULT_REGION=”us-east-1″
LocalStackのエンドポイントを強制してS3バケット一覧を取得
BUCKET_COUNT=$(aws –endpoint-url=http://localhost:4566 s3 ls | grep -c “app-assets-local” || true)
if [ “$BUCKET_COUNT” -eq 1 ]; then
echo “🎉 SUCCESS: Infrastructure verified successfully on LocalStack!”
else
echo “❌ FAILURE: Resource not found on LocalStack.”
exit 1
fi
echo “🔄 Destroying Pulumi stack resources…”
pulumi destroy –stack local –yes
このスクリプトを `package.json` のスクリプトに登録することで、開発者は `npm run test:local` を叩くだけで、数秒のサイクルでIaCの整合性テストを回すことが可能になる。
—
4. 低レイヤ&エキスパート知見:メモリ消費、ステート管理、およびパフォーマンス最適化ハック
LocalStackとPulumiを大規模なプロジェクト(リソース数数百〜数千)で運用する場合、デフォルト設定のままでは必ずパフォーマンスの壁にぶつかる。プロフェッショナルが実践する最適化ハックを共有する。
1. LocalStackのメモリ・CPU消費の最適化 (`docker-compose.yml`)
LocalStackはすべてのAWSサービスをJVMやPythonプロセスとしてメモリ上に展開するため、無制限にリソースを消費する。Dockerコンテナのリソース制限と、「必要なサービスのみを起動する(Services Lazy Loading)」設定が必須である。
version: “3.8”
services:
localstack:
image: localstack/localstack:latest
ports:
- “4566:4566”
environment:
- SERVICES=s3,dynamodb,lambda,iam,apigateway # 使用するサービスを明示的に限定
- DEBUG=0
- DOCKER_HOST=unix:///var/run/docker.sock
volumes:
- “${LOCALSTACK_VOLUME_DIR:-./.localstack}:/var/lib/localstack”
- “/var/run/docker.sock:/var/run/docker.sock”
deploy:
resources:
limits:
cpus: ‘2.0’
memory: 4G
reservations:
cpus: ‘0.5’
memory: 1G
2. Pulumiのプラグインキャッシュと並行実行制御
Pulumiはプロバイダプラグイン(`pulumi-resource-aws`など)のダウンロードに初回数秒を要する。CI/CDパイプラインやコンテナ内での実行時は、以下の環境変数を設定してプラグインのオフラインロードと並行処理の最適化を行え。
- `PULUMI_PLUGIN_HOME`: プラグインの格納先を永続ボリュームやビルドキャッシュに指定する。
- `PULUMI_CONCURRENCY`: リソース作成の並行数を制限し、LocalStackのAPIスロットリング(レートリミット)を回避する(通常は `10` 〜 `20` 程度が妥当)。
3. ステートバックエンドの完全ローカル隔離
Pulumiのステート(状態管理ファイル)は通常 Pulumi Service(SaaS)に保存されるが、オフラインテスト環境においてはセキュリティと速度の観点からローカルファイルバックエンド(またはMinIO等を用いたS3互換バックエンド)を使用すべきだ。
暗号化パスフレーズをローカル用に固定(対話入力を防ぐ)
export PULUMI_CONFIG_PASSPHRASE=”local-development-secret-passphrase-do-not-use-in-prod”
export PULUMI_BACKEND_URL=”file://./.pulumi/state”
—
5. CI/CDパイプラインへの統合:GitHub Actionsの劇的高速化
従来のGitHub ActionsでAWS連携のインフラテストを行おうとすると、AWSのIAMロールの引き受け(OIDC)、リソースのプロビジョニング、テスト後のクリーンアップで15分以上のランタイムを消費し、月々のGitHub Actionsの無料枠を瞬殺していた。
LocalStackとPulumiを組み合わせたパイプラインは、これらの無駄をすべて消し去る。
name: Offline IaC Test
on:
pull_request:
branches: [ main ]
jobs:
pulumi-localstack-test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ’20’
cache: ‘npm’
- name: Setup Pulumi
uses: pulumi/action-install@v3
- name: Install Dependencies
run: npm ci
- name: Run LocalStack & Pulumi Test Suite
run: ./scripts/local-test.sh
env:
PULUMI_CONFIG_PASSPHRASE: “ci-test-passphrase”
このワークフローの実行時間は、わずか60秒〜90秒である。クラウド費用は完全にゼロ、AWSのアカウントクレデンシャル漏洩リスクもゼロ、そして何より開発者がPRを出した瞬間に数分で結果が返ってくる「真の高速フィードバックループ」が手に入る。
—
結びにかえて
インフラストラクチャ・コード(IaC)のécriture(記述)は、もはや「クラウドの本番環境に恐る恐る適用し、祈る」ようなレガシーな手法であってはならない。
Pulumiというモダンなプログラミングモデルと、LocalStackという堅牢なローカルモックの融合は、インフラ開発を「ソフトウェア開発の速度」へと引き上げる。コストという物理的制約からエンジニアを解放し、何度でも失敗し、何度でも秒速で検証できる環境を自らの手で構築することこそが、現代のSREおよびDevOpsエンジニアに求められる究極の美学なのである。