TerraformからPulumiへの移行:State地獄からの脱出と、真のIaC自動化アーキテクチャ
こんにちは。長年、数千リソースを超える巨大なクラウドインフラのライフサイクルと向き合ってきたインフラストラクチャー・アーキテクトだ。
HCL(HashiCorp Configuration Language)による静的な構成管理に限界を感じ、動的プログラミング言語(TypeScriptやPythonなど)による柔軟性、テスト容易性、そして真の冪等性を求めてPulumiへ移行したい——そう考えるSREチームは急増している。
しかし、「既存のTerraform Stateをどうするか」「膨大なHCLをどう書き換えるか」「ダウンタイムなしで移行できるのか」という壁の前に、多くのエンジニアが足踏みをしている。
本記事では、TerraformからPulumiへの移行における「Stateインポートの極意」「コード変換の自動化戦略」「現場で確実に踏む地雷の回避策」を、低レイヤの挙動や内部アーキテクチャの知見を交えて徹底的に解説する。
—
1. TerraformのStateファイルをPulumiへ移行するアプローチ
TerraformからPulumiへの移行において、最も恐れられているのが「リソースの再作成(Re-creation)」だ。誤った手順を踏めば、プロダクション環境のデータベースやロードバランサーがダウンタイムを引き起こす。
我々が目指すべきは、「既存リソースの破壊をゼロにし、インフラの所有権(Ownership)を安全に移譲すること」である。
内部アーキテクチャの理解:Terraform State vs Pulumi State
- Terraform State: リソースの属性値とプロバイダ固有のID(URNに相当するもの)をJSONで保持するフラットな構造。
- Pulumi State: リソースの依存関係(DAG: 有向非巡回グラフ)をメモリ上に構築し、暗号化されたステートバックエンド(S3, Pulumi Service等)にスタック単位で保存する。
移行のメカニズム:`pulumi import` とドリフト回避
TerraformのStateファイルを直接PulumiのStateにコンバートする公式なマジックツールは存在しない。なぜなら、プロバイダが要求するスキーマやリソースのURN(Uniform Resource Name)の命名規則が根本的に異なるからだ。
したがって、正攻法は以下のステップになる。
1. Terraform側でリソースを削除せず、`terraform state rm` で管理対象外(Untrack)にする。
2. Pulumiの `import` 機能またはコード内の `import` オプションを使い、既存のクラウドIDとPulumiリソースを紐付ける。
ここで手動作業を挟むのは、SREの恥だ。完全自動化のためのPythonスクリプトによるStateマッピングの概念図を見てほしい。
terraform_to_pulumi_mapper.py
概念実証:TerraformのstateからリソースIDを抽出し、Pulumiインポート用コードを動的生成する
import json
import subprocess
def parse_terraform_state(state_file_path):
with open(state_file_path, ‘r’) as f:
state_data = json.load(f)
resources = []
for module in state_data.get(‘modules’, [state_data]): # TF v1.x 互換
for resource in module.get(‘resources’, []):
if resource.get(‘mode’) == ‘managed’:
resources.append({
‘type’: resource[‘type’],
‘name’: resource[‘name’],
‘id’: resource[‘primary’][‘id’],
‘attributes’: resource[‘primary’][‘attributes’]
})
return resources
実際のパイプラインでは、これらを元に Pulumiのプログラムを自動生成、
または pulumi.ResourceOptions(import_=…) を動的に挿入する。
—
2. HCLからTypeScript/Pythonへのコード書き換え戦略
「HCLからTypeScriptへ」の移行は、単なる構文の置き換えではない。「宣言的設定の記述」から「ソフトウェアエンジニアリング」へのパラダイムシフトである。
なぜTypeScript / Pythonなのか?
HCLでは、複雑な条件分岐(`count` や `for_each` のハック)や外部APIの動的参照を書くために苦悶してきたはずだ。PulumiをTypeScriptやPythonで書くことで、以下の恩恵を受ける。
- 型安全性 (Type Safety): AWSのIAMポリシーやKubernetesマニフェストをIDEの補完と静的型チェック(TypeScriptならzod等との組み合わせも)で記述できる。
- 抽象化とカプセル化 (ComponentResources): 「社内ニッチなベストプラクティスを満たしたセキュアなVPC」をクラスとして定義し、チーム全体で再利用できる。
HCLからTSへの変換ハック:`tf2pulumi` の限界と実務的アプローチ
Pulumi社は公式で `tf2pulumi` という変換ツールを提供しているが、複雑なモジュール構造や最新のプロバイダバージョンに対しては完璧に動作しないことが多い。
【エキスパートの知見】
すべてのHCLを一度に変換しようとしてはならない。モジュール単位、あるいはレイヤー単位(Networking -> Database -> Application)で段階的に移行する。
以下は、TerraformのAWS S3バケット定義(HCL)を、Pulumi(TypeScript)の堅牢なコードへリファクタリングした例だ。
Terraform (HCL):
resource “aws_s3_bucket” “secure_logs” {
bucket = “my-company-audit-logs”
}
resource “aws_s3_bucket_server_side_encryption_by_default” “example” {
bucket = aws_s3_bucket.secure_logs.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = “AES256”
}
}
}
Pulumi (TypeScript):
import as aws from “@pulumi/aws”;
// 宣言的でありながら、TypeScriptのオブジェクト指向の恩恵を受ける
const secureLogsBucket = new aws.s3.Bucket(“secure-logs”, {
bucket: “my-company-audit-logs”,
serverSideEncryptionConfiguration: {
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: “AES256”,
},
},
},
// Pulumiならではの強力な機能:削除保護やライフサイクルポリシーを関数としてカプセル化可能
}, {
protect: true, // 本番環境での誤削除を防止する低レイヤ防護
import: “my-company-audit-logs” // 既存リソースのシームレスな取り込み
});
export const bucketName = secureLogsBucket.id;
—
3. 移行時に発生しやすいトラブルと回避策
数々の現場でTerraformからPulumiへの移行を指揮してきた中で、エンジニアが必ずハマる「罠」と、その回避策(知見)を共有しよう。
トラブル1:プロバイダのバージョン不一致による意図しない差分 (Diff)
- 現象: Terraformで使用していたAWSプロバイダのバージョンと、Pulumiの `@pulumi/aws` が内部でラップしているTerraformプロバイダのバージョンが微妙に異なり、初回の `pulumi preview` で大量の「再作成(ForceNew)」が検知される。
- 回避策: Pulumiのパッケージバージョン選定時は、必ず対応するTerraform Providerのバージョンマトリクスを確認すること。また、`ignoreChanges` オプションを活用して、移行期における不要な差分をマスクせよ。
const myInstance = new aws.ec2.Instance(“web”, {
// …設定…
}, {
ignoreChanges: [“ami”], // 特定の属性の差異を無視し、移行時の破壊を防ぐ
});
トラブル2:非同期処理とリソース依存関係(DAG)の崩壊
- 現象: TypeScriptやPythonで記述する際、プログラミング言語の非同期処理(`async/await` や Promise)を誤解し、PulumiのOutput型(遅延評価される値)を通常の文字列として扱ってしまい、ランタイムエラーや依存関係の逆転が発生する。
- 回避策: Pulumiの `Output
` オブジェクトの概念を完全に理解すること。値を直接取り出すのではなく、`.apply()` メソッドや `pulumi.all()` を用いてリアクティブにデータを結びつける必要がある。
// 誤り: 出力値から直接文字列を取り出そうとする(undefinedになる)
// const dbHost = dbInstance.endpoint;
// console.log(dbHost.address);
// 正解: .apply を使って非同期に値を受け渡す
const connectionString = pulumi.all([dbInstance.endpoint, dbUser.name]).apply(
([endpoint, user]) => `postgresql://${user}:password@${endpoint}/db`
);
—
4. 段階的な移行を進めるためのベストプラクティス
一撃でのビッグバン移行は、インフラストラクチャーの切腹に等しい。以下のステップを踏むことで、リスクを極限まで低減せよ。
ステップ1:サンドボックス環境でのプロトタイピング
本番環境ではなく、完全に隔離されたAWSアカウントまたはGCPプロジェクトで、TerraformコードをPulumiへ変換し、`pulumi up` から `pulumi destroy` までの一連のサイクルが意図通りに回ることを検証する。
ステップ2:レイヤー分離と段階的インポート (Hybrid State Operation)
巨大なTerraform Monolith(単一の巨大なState)を移行してはならない。Terraformの段階的解体を進める。
1. VPCや基盤ネットワークレイヤー: 変更頻度が低いため、Terraformのまま残すか、最後に移行する。
2. ステートレスなアプリケーション(ECS, Lambda, Kubernetes Apps): 最も移行が容易なため、最初にPulumiへ書き換えてインポートする。
3. データベース等のステートフルリソース: 最も慎重に。`import` オプションをコードに明記し、`pulumi preview` で差分が「0」であることを確認してから `pulumi up` を実行する。
ステップ3:CI/CDパイプラインの統合
Pulumiは、従来のTerraform Cloud / GitHub ActionsによるTerraform実行フローとは異なり、Pulumi Service(SaaS)または独自のバックエンド(S3/GCS + DynamoDB等)を選択できる。
GitHub Actionsにおける、最高効率かつ安全なPulumi実行パイプラインのサンプルを提示する。
name: Pulumi Production Deployment
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ’20’
cache: ‘npm’
- name: Install Dependencies
run: npm ci
- name: Run Pulumi Preview (Dry-Run)
uses: pulumi/action-pulumi-budgets@v3 # または standard action
with:
command: preview
stack-name: production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
- name: Run Pulumi Up (Apply)
if: github.ref == ‘refs/heads/main’
uses: pulumi/action-pulumi@v3
with:
command: up
stack-name: production
options: –yes
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
—
結びに代えて:IaCの未来を握るのは「エンジニアリングの拡張性」だ
HCLの呪縛から解放され、TypeScriptやPythonという汎用言語でインフラを記述できるようになると、インフラストラクチャーは単なる「設定ファイル」から「真のアプリケーションコード」へと昇華する。
単体テスト(Jestやpytestを用いたモックテスト)、静的解析、そして高度なモジュール抽象化。これらを駆使することで、あなたの組織のインフラ開発生産性は劇的に跳ね上がるだろう。
恐れることはない。入念なStateの切り離しと、`.apply()` による非同期データのハンドリングさえマスターすれば、Pulumiの世界はあなたにとって極めて快適で強力な武器となるはずだ。
さあ、コードを開き、最初のスタックをデプロイしよう。