Terraform CDK 程式語言基礎架構

Terraform CDK Infrastructure as Code with Programming Languages

前言

隨著雲端基礎架構的複雜度不斷提升,Infrastructure as Code (IaC) 已成為現代 DevOps 實踐的核心。Terraform 作為業界領先的 IaC 工具,其原生的 HCL (HashiCorp Configuration Language) 雖然簡潔易讀,但對於習慣使用通用程式語言的開發者來說,可能會感到功能受限。Terraform CDK (CDKTF) 正是為了解決這個問題而誕生的解決方案。

本文將深入探討 CDKTF 的核心概念、實際應用範例,以及如何利用熟悉的程式語言來管理雲端基礎架構。


什麼是 Terraform CDK (CDKTF)

Terraform CDK 是 HashiCorp 與 AWS 合作開發的開源專案,讓開發者能夠使用熟悉的程式語言來定義和佈建基礎架構。CDKTF 會將程式碼編譯成標準的 Terraform JSON 配置檔,然後透過 Terraform 核心引擎執行實際的資源管理。

核心概念

CDKTF 的架構主要包含以下幾個層次:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
┌─────────────────────────────────────────────────┐
│         Application Code (TS/Python/Go...)      │
├─────────────────────────────────────────────────┤
│              CDKTF Constructs                   │
├─────────────────────────────────────────────────┤
│           Generated Provider Bindings           │
├─────────────────────────────────────────────────┤
│              Terraform JSON                     │
├─────────────────────────────────────────────────┤
│              Terraform Core                     │
└─────────────────────────────────────────────────┘

關鍵元件說明:

元件說明
App應用程式的頂層容器,包含一個或多個 Stack
Stack資源的邏輯分組,對應到一個 Terraform state
Construct可重複使用的基礎架構元件,類似於 Terraform modules
Resource實際的雲端資源,如 EC2、S3 等

CDKTF 與原生 HCL 的比較

HCL 範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# main.tf
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  tags = {
    Name = "web-server"
    Environment = "production"
  }
}

resource "aws_security_group" "web_sg" {
  name        = "web-security-group"
  description = "Security group for web server"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

CDKTF TypeScript 等效範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { Instance } from "@cdktf/provider-aws/lib/instance";
import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group";

class WebServerStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new AwsProvider(this, "AWS", {
      region: "us-west-2",
    });

    const webSg = new SecurityGroup(this, "web_sg", {
      name: "web-security-group",
      description: "Security group for web server",
      ingress: [
        {
          fromPort: 80,
          toPort: 80,
          protocol: "tcp",
          cidrBlocks: ["0.0.0.0/0"],
        },
      ],
      egress: [
        {
          fromPort: 0,
          toPort: 0,
          protocol: "-1",
          cidrBlocks: ["0.0.0.0/0"],
        },
      ],
    });

    new Instance(this, "web", {
      ami: "ami-0c55b159cbfafe1f0",
      instanceType: "t3.micro",
      vpcSecurityGroupIds: [webSg.id],
      tags: {
        Name: "web-server",
        Environment: "production",
      },
    });
  }
}

const app = new App();
new WebServerStack(app, "web-server-stack");
app.synth();

比較分析

特性HCLCDKTF
學習曲線需要學習新語言使用熟悉的程式語言
IDE 支援有限的自動補全完整的 IntelliSense
型別安全執行時期檢查編譯時期型別檢查
抽象能力ModulesClasses, Functions, Inheritance
邏輯控制有限的條件與迴圈完整的程式語言能力
測試需要額外工具原生單元測試支援
重構手動處理IDE 輔助重構

支援的程式語言

CDKTF 目前支援以下程式語言:

正式支援 (Generally Available)

語言套件管理器專案初始化
TypeScriptnpm / yarncdktf init --template=typescript
Pythonpip / pipenvcdktf init --template=python
Gogo modulescdktf init --template=go
JavaMaven / Gradlecdktf init --template=java
C#NuGetcdktf init --template=csharp

語言選擇建議

  • TypeScript: 最成熟的支援,豐富的社群資源,推薦作為首選
  • Python: 適合資料工程師和熟悉 Python 生態系統的團隊
  • Go: 適合需要高效能和並發處理的場景
  • Java/C#: 適合企業環境中已有相關技術棧的團隊

TypeScript 專案建立與範例

環境準備

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 安裝 Node.js (建議使用 LTS 版本)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs

# 安裝 Terraform
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

# 安裝 CDKTF CLI
npm install -g cdktf-cli

建立新專案

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 建立專案目錄
mkdir my-cdktf-project && cd my-cdktf-project

# 初始化 TypeScript 專案
cdktf init --template=typescript --local

# 專案結構
.
├── cdktf.json          # CDKTF 配置檔
├── help                # 說明文件
├── jest.config.js      # Jest 測試配置
├── main.ts             # 主程式進入點
├── package.json        # Node.js 套件配置
├── setup.js            # 測試設定
└── tsconfig.json       # TypeScript 配置

新增 AWS Provider

1
2
# 新增 AWS Provider
cdktf provider add aws

完整範例:建立 VPC 與 EC2

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// main.ts
import { Construct } from "constructs";
import { App, TerraformStack, TerraformOutput } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { Vpc } from "@cdktf/provider-aws/lib/vpc";
import { Subnet } from "@cdktf/provider-aws/lib/subnet";
import { InternetGateway } from "@cdktf/provider-aws/lib/internet-gateway";
import { RouteTable } from "@cdktf/provider-aws/lib/route-table";
import { RouteTableAssociation } from "@cdktf/provider-aws/lib/route-table-association";
import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group";
import { Instance } from "@cdktf/provider-aws/lib/instance";

interface WebStackConfig {
  region: string;
  vpcCidr: string;
  subnetCidr: string;
  instanceType: string;
  environment: string;
}

class WebInfrastructureStack extends TerraformStack {
  constructor(scope: Construct, id: string, config: WebStackConfig) {
    super(scope, id);

    // Provider 設定
    new AwsProvider(this, "AWS", {
      region: config.region,
    });

    // VPC
    const vpc = new Vpc(this, "main_vpc", {
      cidrBlock: config.vpcCidr,
      enableDnsHostnames: true,
      enableDnsSupport: true,
      tags: {
        Name: `${config.environment}-vpc`,
        Environment: config.environment,
      },
    });

    // Internet Gateway
    const igw = new InternetGateway(this, "igw", {
      vpcId: vpc.id,
      tags: {
        Name: `${config.environment}-igw`,
      },
    });

    // Public Subnet
    const publicSubnet = new Subnet(this, "public_subnet", {
      vpcId: vpc.id,
      cidrBlock: config.subnetCidr,
      availabilityZone: `${config.region}a`,
      mapPublicIpOnLaunch: true,
      tags: {
        Name: `${config.environment}-public-subnet`,
      },
    });

    // Route Table
    const routeTable = new RouteTable(this, "public_rt", {
      vpcId: vpc.id,
      route: [
        {
          cidrBlock: "0.0.0.0/0",
          gatewayId: igw.id,
        },
      ],
      tags: {
        Name: `${config.environment}-public-rt`,
      },
    });

    // Route Table Association
    new RouteTableAssociation(this, "public_rta", {
      subnetId: publicSubnet.id,
      routeTableId: routeTable.id,
    });

    // Security Group
    const webSg = new SecurityGroup(this, "web_sg", {
      name: `${config.environment}-web-sg`,
      description: "Security group for web servers",
      vpcId: vpc.id,
      ingress: [
        {
          description: "HTTP",
          fromPort: 80,
          toPort: 80,
          protocol: "tcp",
          cidrBlocks: ["0.0.0.0/0"],
        },
        {
          description: "HTTPS",
          fromPort: 443,
          toPort: 443,
          protocol: "tcp",
          cidrBlocks: ["0.0.0.0/0"],
        },
        {
          description: "SSH",
          fromPort: 22,
          toPort: 22,
          protocol: "tcp",
          cidrBlocks: ["0.0.0.0/0"],
        },
      ],
      egress: [
        {
          fromPort: 0,
          toPort: 0,
          protocol: "-1",
          cidrBlocks: ["0.0.0.0/0"],
        },
      ],
      tags: {
        Name: `${config.environment}-web-sg`,
      },
    });

    // EC2 Instance
    const webServer = new Instance(this, "web_server", {
      ami: "ami-0c55b159cbfafe1f0", // Amazon Linux 2
      instanceType: config.instanceType,
      subnetId: publicSubnet.id,
      vpcSecurityGroupIds: [webSg.id],
      associatePublicIpAddress: true,
      userData: `#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello from CDKTF!</h1>" > /var/www/html/index.html
`,
      tags: {
        Name: `${config.environment}-web-server`,
        Environment: config.environment,
      },
    });

    // Outputs
    new TerraformOutput(this, "vpc_id", {
      value: vpc.id,
      description: "VPC ID",
    });

    new TerraformOutput(this, "instance_public_ip", {
      value: webServer.publicIp,
      description: "Web server public IP",
    });

    new TerraformOutput(this, "instance_public_dns", {
      value: webServer.publicDns,
      description: "Web server public DNS",
    });
  }
}

// 應用程式進入點
const app = new App();

// 開發環境
new WebInfrastructureStack(app, "dev-web-infra", {
  region: "ap-northeast-1",
  vpcCidr: "10.0.0.0/16",
  subnetCidr: "10.0.1.0/24",
  instanceType: "t3.micro",
  environment: "dev",
});

// 生產環境
new WebInfrastructureStack(app, "prod-web-infra", {
  region: "ap-northeast-1",
  vpcCidr: "10.1.0.0/16",
  subnetCidr: "10.1.1.0/24",
  instanceType: "t3.small",
  environment: "prod",
});

app.synth();

建立可重用的 Construct

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// constructs/web-server.ts
import { Construct } from "constructs";
import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group";
import { Instance } from "@cdktf/provider-aws/lib/instance";

export interface WebServerConfig {
  vpcId: string;
  subnetId: string;
  instanceType?: string;
  name: string;
}

export class WebServer extends Construct {
  public readonly instance: Instance;
  public readonly securityGroup: SecurityGroup;

  constructor(scope: Construct, id: string, config: WebServerConfig) {
    super(scope, id);

    this.securityGroup = new SecurityGroup(this, "sg", {
      name: `${config.name}-sg`,
      vpcId: config.vpcId,
      ingress: [
        {
          fromPort: 80,
          toPort: 80,
          protocol: "tcp",
          cidrBlocks: ["0.0.0.0/0"],
        },
      ],
      egress: [
        {
          fromPort: 0,
          toPort: 0,
          protocol: "-1",
          cidrBlocks: ["0.0.0.0/0"],
        },
      ],
    });

    this.instance = new Instance(this, "instance", {
      ami: "ami-0c55b159cbfafe1f0",
      instanceType: config.instanceType || "t3.micro",
      subnetId: config.subnetId,
      vpcSecurityGroupIds: [this.securityGroup.id],
      tags: {
        Name: config.name,
      },
    });
  }
}

Python 專案範例

建立 Python 專案

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 初始化 Python 專案
cdktf init --template=python --local

# 專案結構
.
├── cdktf.json
├── help
├── main.py
├── Pipfile
├── Pipfile.lock
└── pytest.ini

# 安裝依賴
pipenv install
pipenv shell

# 新增 AWS Provider
cdktf provider add aws

Python 完整範例

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#!/usr/bin/env 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.vpc import Vpc
from cdktf_cdktf_provider_aws.subnet import Subnet
from cdktf_cdktf_provider_aws.internet_gateway import InternetGateway
from cdktf_cdktf_provider_aws.route_table import RouteTable, RouteTableRoute
from cdktf_cdktf_provider_aws.route_table_association import RouteTableAssociation
from cdktf_cdktf_provider_aws.security_group import SecurityGroup, SecurityGroupIngress, SecurityGroupEgress
from cdktf_cdktf_provider_aws.instance import Instance
from dataclasses import dataclass
from typing import List, Optional


@dataclass
class EnvironmentConfig:
    """環境配置類別"""
    name: str
    region: str
    vpc_cidr: str
    subnet_cidrs: List[str]
    instance_type: str = "t3.micro"


class NetworkConstruct(Construct):
    """網路基礎架構 Construct"""

    def __init__(
        self,
        scope: Construct,
        id: str,
        vpc_cidr: str,
        subnet_cidrs: List[str],
        region: str,
        environment: str,
    ):
        super().__init__(scope, id)

        # VPC
        self.vpc = Vpc(
            self,
            "vpc",
            cidr_block=vpc_cidr,
            enable_dns_hostnames=True,
            enable_dns_support=True,
            tags={"Name": f"{environment}-vpc", "Environment": environment},
        )

        # Internet Gateway
        self.igw = InternetGateway(
            self,
            "igw",
            vpc_id=self.vpc.id,
            tags={"Name": f"{environment}-igw"},
        )

        # Public Subnets
        self.public_subnets = []
        availability_zones = ["a", "b", "c"]

        for i, cidr in enumerate(subnet_cidrs):
            az = f"{region}{availability_zones[i % len(availability_zones)]}"
            subnet = Subnet(
                self,
                f"public_subnet_{i}",
                vpc_id=self.vpc.id,
                cidr_block=cidr,
                availability_zone=az,
                map_public_ip_on_launch=True,
                tags={
                    "Name": f"{environment}-public-subnet-{az}",
                    "Environment": environment,
                },
            )
            self.public_subnets.append(subnet)

        # Route Table
        self.public_route_table = RouteTable(
            self,
            "public_rt",
            vpc_id=self.vpc.id,
            route=[
                RouteTableRoute(
                    cidr_block="0.0.0.0/0",
                    gateway_id=self.igw.id,
                )
            ],
            tags={"Name": f"{environment}-public-rt"},
        )

        # Route Table Associations
        for i, subnet in enumerate(self.public_subnets):
            RouteTableAssociation(
                self,
                f"public_rta_{i}",
                subnet_id=subnet.id,
                route_table_id=self.public_route_table.id,
            )


class WebServerConstruct(Construct):
    """Web 伺服器 Construct"""

    def __init__(
        self,
        scope: Construct,
        id: str,
        vpc_id: str,
        subnet_id: str,
        instance_type: str,
        environment: str,
        allowed_ssh_cidrs: Optional[List[str]] = None,
    ):
        super().__init__(scope, id)

        allowed_ssh_cidrs = allowed_ssh_cidrs or ["0.0.0.0/0"]

        # Security Group
        self.security_group = SecurityGroup(
            self,
            "sg",
            name=f"{environment}-web-sg",
            description="Security group for web servers",
            vpc_id=vpc_id,
            ingress=[
                SecurityGroupIngress(
                    description="HTTP",
                    from_port=80,
                    to_port=80,
                    protocol="tcp",
                    cidr_blocks=["0.0.0.0/0"],
                ),
                SecurityGroupIngress(
                    description="HTTPS",
                    from_port=443,
                    to_port=443,
                    protocol="tcp",
                    cidr_blocks=["0.0.0.0/0"],
                ),
                SecurityGroupIngress(
                    description="SSH",
                    from_port=22,
                    to_port=22,
                    protocol="tcp",
                    cidr_blocks=allowed_ssh_cidrs,
                ),
            ],
            egress=[
                SecurityGroupEgress(
                    from_port=0,
                    to_port=0,
                    protocol="-1",
                    cidr_blocks=["0.0.0.0/0"],
                )
            ],
            tags={"Name": f"{environment}-web-sg"},
        )

        # User Data Script
        user_data = """#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello from CDKTF Python!</h1>" > /var/www/html/index.html
"""

        # EC2 Instance
        self.instance = Instance(
            self,
            "instance",
            ami="ami-0c55b159cbfafe1f0",
            instance_type=instance_type,
            subnet_id=subnet_id,
            vpc_security_group_ids=[self.security_group.id],
            associate_public_ip_address=True,
            user_data=user_data,
            tags={
                "Name": f"{environment}-web-server",
                "Environment": environment,
            },
        )


class WebInfrastructureStack(TerraformStack):
    """完整的 Web 基礎架構 Stack"""

    def __init__(self, scope: Construct, id: str, config: EnvironmentConfig):
        super().__init__(scope, id)

        # AWS Provider
        AwsProvider(self, "AWS", region=config.region)

        # Network Layer
        network = NetworkConstruct(
            self,
            "network",
            vpc_cidr=config.vpc_cidr,
            subnet_cidrs=config.subnet_cidrs,
            region=config.region,
            environment=config.name,
        )

        # Web Server Layer
        web_server = WebServerConstruct(
            self,
            "web",
            vpc_id=network.vpc.id,
            subnet_id=network.public_subnets[0].id,
            instance_type=config.instance_type,
            environment=config.name,
        )

        # Outputs
        TerraformOutput(
            self,
            "vpc_id",
            value=network.vpc.id,
            description="VPC ID",
        )

        TerraformOutput(
            self,
            "web_server_public_ip",
            value=web_server.instance.public_ip,
            description="Web server public IP address",
        )

        TerraformOutput(
            self,
            "web_server_public_dns",
            value=web_server.instance.public_dns,
            description="Web server public DNS name",
        )


# 主程式
app = App()

# 開發環境配置
dev_config = EnvironmentConfig(
    name="dev",
    region="ap-northeast-1",
    vpc_cidr="10.0.0.0/16",
    subnet_cidrs=["10.0.1.0/24", "10.0.2.0/24"],
    instance_type="t3.micro",
)

# 生產環境配置
prod_config = EnvironmentConfig(
    name="prod",
    region="ap-northeast-1",
    vpc_cidr="10.1.0.0/16",
    subnet_cidrs=["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"],
    instance_type="t3.small",
)

# 建立 Stacks
WebInfrastructureStack(app, "dev-infrastructure", dev_config)
WebInfrastructureStack(app, "prod-infrastructure", prod_config)

app.synth()

使用現有 Terraform Providers

CDKTF 可以使用所有現有的 Terraform Providers,包括官方和社群維護的 providers。

新增 Provider 的方式

方式一:使用 CLI 命令

1
2
3
4
5
6
7
8
# 新增官方 Provider
cdktf provider add aws
cdktf provider add google
cdktf provider add azurerm

# 新增社群 Provider
cdktf provider add hashicorp/random
cdktf provider add cloudflare/cloudflare

方式二:修改 cdktf.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "language": "typescript",
  "app": "npx ts-node main.ts",
  "projectId": "your-project-id",
  "sendCrashReports": "false",
  "terraformProviders": [
    "hashicorp/aws@~> 5.0",
    "hashicorp/random@~> 3.5",
    "cloudflare/cloudflare@~> 4.0"
  ],
  "terraformModules": [],
  "context": {}
}

然後執行:

1
cdktf get

使用 Terraform Modules

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { TerraformHclModule } from "cdktf";

// 使用 Terraform Registry 的 Module
const vpcModule = new TerraformHclModule(this, "vpc", {
  source: "terraform-aws-modules/vpc/aws",
  version: "5.0.0",
  variables: {
    name: "my-vpc",
    cidr: "10.0.0.0/16",
    azs: ["ap-northeast-1a", "ap-northeast-1c"],
    private_subnets: ["10.0.1.0/24", "10.0.2.0/24"],
    public_subnets: ["10.0.101.0/24", "10.0.102.0/24"],
    enable_nat_gateway: true,
    single_nat_gateway: true,
  },
});

// 引用 Module 的 Output
new TerraformOutput(this, "vpc_id", {
  value: vpcModule.get("vpc_id"),
});

使用多個 Provider

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { GoogleProvider } from "@cdktf/provider-google/lib/provider";

class MultiCloudStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // AWS Provider (預設)
    new AwsProvider(this, "aws", {
      region: "ap-northeast-1",
    });

    // AWS Provider (不同區域)
    new AwsProvider(this, "aws_us", {
      alias: "us",
      region: "us-west-2",
    });

    // Google Cloud Provider
    new GoogleProvider(this, "google", {
      project: "my-gcp-project",
      region: "asia-northeast1",
    });
  }
}

測試與部署流程

單元測試 (TypeScript + Jest)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// __tests__/main-test.ts
import { Testing } from "cdktf";
import { WebInfrastructureStack } from "../main";

describe("WebInfrastructureStack", () => {
  it("should create a VPC", () => {
    const app = Testing.app();
    const stack = new WebInfrastructureStack(app, "test-stack", {
      region: "ap-northeast-1",
      vpcCidr: "10.0.0.0/16",
      subnetCidr: "10.0.1.0/24",
      instanceType: "t3.micro",
      environment: "test",
    });

    const synthesized = Testing.synth(stack);

    // 驗證 VPC 資源存在
    expect(Testing.toHaveResource(synthesized, "aws_vpc")).toBeTruthy();
  });

  it("should create an EC2 instance with correct type", () => {
    const app = Testing.app();
    const stack = new WebInfrastructureStack(app, "test-stack", {
      region: "ap-northeast-1",
      vpcCidr: "10.0.0.0/16",
      subnetCidr: "10.0.1.0/24",
      instanceType: "t3.micro",
      environment: "test",
    });

    const synthesized = Testing.synth(stack);

    // 驗證 EC2 資源配置
    expect(
      Testing.toHaveResourceWithProperties(synthesized, "aws_instance", {
        instance_type: "t3.micro",
      })
    ).toBeTruthy();
  });

  it("should have security group with HTTP ingress", () => {
    const app = Testing.app();
    const stack = new WebInfrastructureStack(app, "test-stack", {
      region: "ap-northeast-1",
      vpcCidr: "10.0.0.0/16",
      subnetCidr: "10.0.1.0/24",
      instanceType: "t3.micro",
      environment: "test",
    });

    const synthesized = Testing.synth(stack);

    expect(
      Testing.toHaveResourceWithProperties(synthesized, "aws_security_group", {
        ingress: expect.arrayContaining([
          expect.objectContaining({
            from_port: 80,
            to_port: 80,
            protocol: "tcp",
          }),
        ]),
      })
    ).toBeTruthy();
  });

  it("should produce valid Terraform configuration", () => {
    const app = Testing.app();
    const stack = new WebInfrastructureStack(app, "test-stack", {
      region: "ap-northeast-1",
      vpcCidr: "10.0.0.0/16",
      subnetCidr: "10.0.1.0/24",
      instanceType: "t3.micro",
      environment: "test",
    });

    // 驗證產生的 Terraform 配置是有效的
    expect(Testing.fullSynth(stack)).toBeValidTerraform();
  });
});

Python 單元測試 (pytest)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# tests/test_main.py
import pytest
from cdktf import Testing
from main import WebInfrastructureStack, EnvironmentConfig


class TestWebInfrastructureStack:
    @pytest.fixture
    def config(self):
        return EnvironmentConfig(
            name="test",
            region="ap-northeast-1",
            vpc_cidr="10.0.0.0/16",
            subnet_cidrs=["10.0.1.0/24"],
            instance_type="t3.micro",
        )

    def test_should_create_vpc(self, config):
        app = Testing.app()
        stack = WebInfrastructureStack(app, "test-stack", config)
        synthesized = Testing.synth(stack)

        assert Testing.to_have_resource(synthesized, "aws_vpc")

    def test_should_create_ec2_with_correct_type(self, config):
        app = Testing.app()
        stack = WebInfrastructureStack(app, "test-stack", config)
        synthesized = Testing.synth(stack)

        assert Testing.to_have_resource_with_properties(
            synthesized,
            "aws_instance",
            {"instance_type": "t3.micro"},
        )

    def test_should_be_valid_terraform(self, config):
        app = Testing.app()
        stack = WebInfrastructureStack(app, "test-stack", config)

        assert Testing.to_be_valid_terraform(Testing.full_synth(stack))

CDKTF CLI 工作流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 產生 Terraform 配置檔
cdktf synth

# 查看執行計畫
cdktf diff

# 部署基礎架構
cdktf deploy

# 部署特定 Stack
cdktf deploy dev-web-infra

# 部署所有 Stacks
cdktf deploy '*'

# 銷毀基礎架構
cdktf destroy

# 查看 Outputs
cdktf output

CI/CD 整合範例 (GitHub Actions)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# .github/workflows/cdktf.yml
name: CDKTF CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - 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 tests
        run: npm test

      - name: Synthesize
        run: npx cdktf synth

  plan:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.6.0"

      - name: Install dependencies
        run: npm ci

      - name: CDKTF Diff
        run: npx cdktf diff

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.6.0"

      - name: Install dependencies
        run: npm ci

      - name: CDKTF Deploy
        run: npx cdktf deploy --auto-approve

最佳實務與限制

最佳實務

1. 專案結構組織

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
my-cdktf-project/
├── constructs/           # 可重用的 Construct 元件
   ├── network.ts
   ├── compute.ts
   └── database.ts
├── stacks/               # Stack 定義
   ├── dev.ts
   ├── staging.ts
   └── production.ts
├── configs/              # 環境配置
   ├── dev.json
   ├── staging.json
   └── production.json
├── __tests__/            # 測試檔案
   └── stacks.test.ts
├── main.ts               # 進入點
├── cdktf.json
└── package.json

2. 使用 Interface 和 Type 確保型別安全

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
interface DatabaseConfig {
  instanceClass: string;
  allocatedStorage: number;
  engine: "mysql" | "postgres";
  engineVersion: string;
  multiAz: boolean;
}

interface ApplicationConfig {
  environment: "dev" | "staging" | "prod";
  region: string;
  database: DatabaseConfig;
}

3. 實作標籤策略

1
2
3
4
5
6
7
8
function applyTags(environment: string, project: string): Record<string, string> {
  return {
    Environment: environment,
    Project: project,
    ManagedBy: "CDKTF",
    CreatedAt: new Date().toISOString().split("T")[0],
  };
}

4. 使用 Aspects 進行全域修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { Aspects, IAspect } from "cdktf";
import { IConstruct } from "constructs";

class TaggingAspect implements IAspect {
  constructor(private tags: Record<string, string>) {}

  visit(node: IConstruct): void {
    if ("tagsInput" in node) {
      const resource = node as any;
      resource.tags = { ...resource.tags, ...this.tags };
    }
  }
}

// 應用到 Stack
Aspects.of(stack).add(
  new TaggingAspect({
    CostCenter: "engineering",
    Team: "platform",
  })
);

5. Remote State 管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { S3Backend } from "cdktf";

class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new S3Backend(this, {
      bucket: "my-terraform-state-bucket",
      key: `${id}/terraform.tfstate`,
      region: "ap-northeast-1",
      encrypt: true,
      dynamodbTable: "terraform-state-lock",
    });
  }
}

限制與注意事項

1. 效能考量

情境說明
大型專案CDKTF 的合成 (synth) 過程在大型專案中可能較慢
Provider 綁定某些大型 Provider (如 AWS) 的綁定檔案較大,影響安裝時間
記憶體使用複雜的專案可能需要較多記憶體進行合成

2. 功能限制

1
2
3
4
// CDKTF 不支援的 HCL 功能
// 1. 部分 Terraform 函數可能沒有直接對應
// 2. Provisioners 支援有限
// 3. 某些進階的 lifecycle 設定可能需要額外處理

3. 除錯建議

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 啟用詳細日誌
export CDKTF_LOG_LEVEL=debug

# 查看產生的 Terraform 配置
cdktf synth
cat cdktf.out/stacks/my-stack/cdk.tf.json | jq .

# 直接使用 Terraform 命令除錯
cd cdktf.out/stacks/my-stack
terraform plan

4. 版本相容性

1
2
3
4
5
6
7
8
9
// package.json - 確保版本相容
{
  "dependencies": {
    "cdktf": "^0.20.0",
    "cdktf-cli": "^0.20.0",
    "constructs": "^10.0.0",
    "@cdktf/provider-aws": "^19.0.0"
  }
}

何時選擇 CDKTF

適合使用 CDKTF 的情境:

  • 團隊熟悉 TypeScript/Python 等程式語言
  • 需要複雜的邏輯控制和抽象
  • 希望利用程式語言的測試框架
  • 需要與現有程式碼整合
  • 希望使用 IDE 的強大功能(自動補全、重構)

建議使用原生 HCL 的情境:

  • 簡單的基礎架構定義
  • 團隊已熟悉 HCL
  • 不需要複雜的邏輯處理
  • 效能是關鍵考量

結語

Terraform CDK 為基礎架構即程式碼帶來了新的可能性,讓開發者能夠使用熟悉的程式語言來管理雲端資源。透過強型別系統、IDE 支援和原生測試框架,CDKTF 大幅提升了開發體驗和程式碼品質。

雖然 CDKTF 有其學習曲線和一些限制,但對於需要複雜邏輯處理、程式碼重用和整合現有開發流程的團隊來說,它是一個值得考慮的選項。隨著 HashiCorp 持續投入開發,CDKTF 的功能和穩定性也在不斷提升。

建議讀者從小型專案開始嘗試,逐步累積經驗後再應用到生產環境中。

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy