CDK for Terraform (CDKTF) × Python:HCLを書かずにクラウドリソースをデプロイする

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

HCLをPythonに置き換えたい理由

Terraformを使ったことがあれば、HCL(HashiCorp Configuration Language)が十分に機能的であることはわかるはずです。ただし、少し変わったループが必要になったり、ブロックをあちこちコピー&ペーストせずにスタック間でロジックを再利用したいと思ったとき、話は変わってきます。HCLは設計上宣言的であり、シンプルなインフラには最適ですが、複雑さが増すにつれて、「関数が書けたらいいのに」と思い始めるでしょう。

まさにそのギャップを埋めるのがCDK for Terraformです。CDKTFでは、インフラのコードをPython(またはTypeScript、Go、Java、C#)で記述でき、内部でTerraform JSON設定を生成します。実際のTerraformのstate、プロバイダー、バックエンドはすべてそのまま — 本物のプログラミング言語でそれらを定義できるというだけです。

私は複数のAWS本番環境でこのアプローチを適用してきましたが、結果は一貫して安定しています。生成されたTerraform JSONは有効で予測可能であり、すでにterraform planterraform applyを実行している既存のCI/CDパイプラインとも問題なく連携します。

CDKTFが適している場面

  • HCLでは書きづらい条件分岐やループが必要な場合
  • チームがすでにPythonに慣れており、HCLの構文が馴染みにくい場合
  • インフラのパターンをPythonパッケージとして共有したい場合
  • インフラが動的に生成される内部開発者プラットフォームを構築している場合

インフラが小規模でシンプルな場合は、通常のTerraformで十分です。しかし、共通設定を持つ数十の環境を管理するようになると、CDKTFのメリットが現れ始めます。

インストール

CDKTFには2つの実行要件があります:Node.js(CLI用)とPython(スタックコード用)。Pythonで記述するにもかかわらず、CLIはNode.jsツールなので両方が必要です。

ステップ1:Node.jsをインストール

CDKTF CLIにはNode.js 18以降が必要です。Ubuntu/Debianの場合:

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version  # v20.x以降であることを確認

macOSでHomebrewを使う場合:

brew install node

ステップ2:CDKTF CLIをインストール

npm install -g cdktf-cli
cdktf --version  # インストールを確認

ステップ3:Terraformをインストール

CDKTFはTerraform設定を生成するため、実際のプロビジョニングにはTerraform本体が必要です:

# Ubuntu/Debianの場合
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
terraform --version

ステップ4:CDKTFプロジェクトの初期化

新しいディレクトリを作成してPythonプロジェクトを初期化します:

mkdir my-cdktf-project && cd my-cdktf-project
cdktf init --template=python --local

--localフラグはTerraformのstateをローカル(terraform.tfstateファイル)に保存します。本番環境ではS3やTerraform Cloudに向けることになりますが、学習目的ではローカルstateで十分です。

初期化後、ディレクトリ構造は以下のようになります:

my-cdktf-project/
├── main.py          ← スタック定義をここに記述
├── cdktf.json       ← CDKTF設定(プロバイダー、出力ディレクトリ)
├── requirements.txt
└── .gen/            ← 自動生成されたプロバイダーバインディング(編集不可)

ステップ5:Python仮想環境のセットアップ

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

設定

いよいよ本題です — Pythonで実際のインフラを記述します。この例では、バージョニングを有効にしたAWS S3バケットをデプロイします。小さな例ですが、あらゆるリソースで使う完全なパターンを示しています。

AWSプロバイダーを追加

cdktf.jsonを編集してAWSプロバイダーを追加します:

{
  "language": "python",
  "app": "pipenv run python main.py",
  "terraformProviders": ["aws@~> 5.0"],
  "terraformModules": [],
  "output": "cdktf.out"
}

次にプロバイダーバインディングを生成します。AWSプロバイダーのスキーマをダウンロードし、すべてのAWSリソースに対して型付きPythonクラスを生成します:

cdktf get

これには1〜2分かかります。完了すると、.gen/providers/aws/以下にAWSリソースタイプごとに1つ、数千のクラスが生成されます。

Pythonでスタックを記述

main.pyを開いてデフォルトの内容を以下に置き換えます:

from constructs import Construct
from cdktf import App, TerraformStack, TerraformOutput
from cdktf_cdktf_provider_aws.provider import AwsProvider
from cdktf_cdktf_provider_aws.s3_bucket import S3Bucket
from cdktf_cdktf_provider_aws.s3_bucket_versioning import (
    S3BucketVersioning,
    S3BucketVersioningVersioningConfiguration,
)


class MyInfraStack(TerraformStack):
    def __init__(self, scope: Construct, id: str, env: str):
        super().__init__(scope, id)

        # AWSプロバイダーを設定
        AwsProvider(self, "AWS", region="ap-northeast-1")

        # S3バケットを作成
        bucket = S3Bucket(
            self,
            "app-bucket",
            bucket=f"my-app-assets-{env}",
            tags={"Environment": env, "ManagedBy": "cdktf"},
        )

        # バケットのバージョニングを有効化
        S3BucketVersioning(
            self,
            "bucket-versioning",
            bucket=bucket.id,
            versioning_configuration=S3BucketVersioningVersioningConfiguration(
                status="Enabled"
            ),
        )

        # 後で参照できるようにバケット名を出力
        TerraformOutput(
            self,
            "bucket_name",
            value=bucket.bucket,
            description="作成されたS3バケットの名前",
        )


app = App()
MyInfraStack(app, "staging", env="staging")
MyInfraStack(app, "production", env="production")
app.synth()

ここで何が起きているか注目してください:envパラメータにより、何も複製することなく同じスタックを2回インスタンス化できます — stagingとproductionで1回ずつ。これがHCLに対するPythonの強みであり、パラメータ化されたスタックはクラスのインスタンスとして生成するだけです。

Terraform設定を合成

CDKTFは直接プロビジョニングしません — まずTerraform JSONを生成し、その後それを適用します。以下を実行してください:

cdktf synth

これによりcdktf.out/stacks/staging/cdktf.out/stacks/production/が作成され、それぞれにcdk.tf.jsonファイルが含まれます。そのファイルを確認してCDKTFが何を生成したか検証できます — 標準のTerraform JSONなので、通常のTerraform設定と同じように読めます。

AWSへデプロイ

AWSの認証情報が設定されていることを確認してから(環境変数、~/.aws/credentials、またはIAMロール)、以下を実行します:

# 適用前に変更をプレビュー
cdktf plan staging

# stagingに適用
cdktf deploy staging

# 両方のスタックを一度に適用
cdktf deploy --all

おなじみのTerraformプラン出力が表示されます — CDKTFは各スタックの基礎となるterraform applyにそのまま処理を委ねます。

検証とモニタリング

デプロイが完了したら、すべてが正しく反映されていることを確認し、ドリフトを早期に検出できるよう準備しましょう。

スタック出力を確認

cdktf deployが完了すると、出力はターミナルに直接表示されます。後で再度確認するには:

cdktf output staging

定義したbucket_nameの出力が表示されます — スタック間での値の受け渡しや、アプリケーション設定への組み込みに役立ちます。

AWSでリソースを確認

# バケット一覧を表示して新しいバケットを確認
aws s3 ls | grep my-app-assets

# バージョニングが実際に有効になっているか確認
aws s3api get-bucket-versioning --bucket my-app-assets-staging

バージョニング確認の期待される出力:

{
    "Status": "Enabled"
}

設定ドリフトを検出

誰かがAWSコンソールでリソースを手動で変更した場合、CDKTF(Terraform経由)は次のプランで設定のズレを検出します:

cdktf diff staging

これにより現在のstateに対してterraform planが実行され、コードの外で変更されたものが表示されます。これをCIでスケジュール実行することは、問題が起きる前に意図しない変更を検出する効率的な方法です。

使い終わったらリソースを削除

テスト環境を削除したい場合:

cdktf destroy staging

本番環境に近いものに対してこのコマンドを実行する前には必ず二重確認してください — データベースやデータが入ったS3バケットなどのステートフルなリソースは取り消しができません。

CI/CDとの統合

プルリクエスト時にプランを実行し、マージ時に適用する最小限のGitHub Actionsワークフロー:

name: CDKTFデプロイ
on:
  push:
    branches: [main]
  pull_request:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: npm install -g cdktf-cli
      - run: pip install -r requirements.txt
      - run: cdktf get
      - name: プラン(PRのみ)
        if: github.event_name == 'pull_request'
        run: cdktf plan --all
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      - name: デプロイ(mainブランチのみ)
        if: github.ref == 'refs/heads/main'
        run: cdktf deploy --all --auto-approve
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

ここでのパターン — PRでプラン、マージで適用 — は、通常のTerraformでチームが使うのと同じワークフローです。CDKTFは既存のプロセスを乱さずそのまま組み込めます。

注意点として、cdktf getのステップはプロバイダーバインディングを再生成します。HashiCorpがパイプライン実行中に新しいプロバイダーバージョンをリリースしたときの予期しない問題を避けるため、cdktf.jsonでプロバイダーのバージョンを固定しておきましょう。

Share: