Tại sao File YAML Cứ Gây Rắc Rối
Nếu bạn đã từng quản lý hạ tầng cloud, chắc hẳn bạn đã gặp cảnh này: một file Terraform chạy ngon trên máy local lại lỗi trên môi trường staging vì ai đó quên cập nhật một biến. Hoặc một template CloudFormation dài 800 dòng YAML mà không có cách nào để loop, trừu tượng hóa, hay test bất cứ thứ gì cho ra hồn.
Nguyên nhân gốc rễ không phải do bản thân các công cụ. Các declarative DSL như HCL và YAML có giới hạn cứng — chúng không phải ngôn ngữ lập trình thật sự. Bạn không thể viết hàm, chạy unit test, hay import thư viện. Mỗi khi hạ tầng phức tạp dần lên, bạn lại phải vật lộn với ngôn ngữ thay vì giải quyết vấn đề thực sự.
Pulumi tiếp cận theo hướng khác: viết hạ tầng bằng TypeScript, Python, Go hoặc C#. Ngôn ngữ thật, hệ thống kiểu thật, hỗ trợ IDE thật. Tôi đã dùng nó trên bốn môi trường AWS riêng biệt trong production — với các team từ 3 đến 15 kỹ sư — và nó hoạt động tốt, đặc biệt khi team đã quen với TypeScript từ công việc app hàng ngày.
Pulumi Hoạt động như thế nào
Pulumi không chỉ đơn giản là “Terraform nhưng dùng TypeScript”. Mô hình của nó thực sự khác biệt:
- Quản lý state: Pulumi theo dõi state qua Pulumi Cloud (hoặc self-hosted backend như S3). Không cần loay hoay với file state thủ công.
- Tính năng ngôn ngữ thật sự: Dùng vòng lặp
forđể tạo nhiều S3 bucket, interface để kiểm tra kiểu dữ liệu config, hayasync/awaitđể kết hợp các resource phụ thuộc nhau. - Component resource: Đóng gói VPC + subnet + security group thành một class tái sử dụng. Gọi nó xuyên suốt các stack như bất kỳ module nào khác.
- Policy as Code: Thực thi các quy tắc (“không cho phép S3 bucket public”) theo kiểu lập trình trước khi triển khai.
Một điểm đánh đổi thực tế: nếu team bạn không có nền tảng lập trình, thời gian làm quen sẽ lâu hơn Terraform. Nhưng với ai đã viết TypeScript hàng ngày, chi phí học thêm rất nhỏ — thường chỉ vài tiếng để quen, không phải vài ngày.
Cài đặt
Yêu cầu trước
Bạn cần Node.js 18+ và Pulumi CLI. Trên macOS hoặc Linux:
# Cài Pulumi CLI
curl -fsSL https://get.pulumi.com | sh
# Kiểm tra
pulumi version
Trên Windows, dùng winget:
winget install pulumi
Tiếp theo, cấu hình AWS CLI cho region bạn muốn dùng:
aws configure
# Nhập Access Key ID, Secret, region (ví dụ: ap-northeast-1)
Tạo Project Pulumi Mới
mkdir my-infra && cd my-infra
pulumi new aws-typescript
Lệnh này tạo sẵn một project với:
Pulumi.yaml— metadata của projectPulumi.dev.yaml— config riêng cho từng stackindex.ts— code hạ tầng của bạnpackage.json— các dependency Node.js
npm install
Viết Resource Đầu tiên
S3 Bucket với Versioning
Mở index.ts. Scaffold mặc định đã tạo sẵn một S3 bucket — đây là phiên bản gọn hơn với versioning và tag:
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("my-app-bucket", {
versioning: {
enabled: true,
},
tags: {
Environment: "production",
ManagedBy: "pulumi",
},
});
export const bucketName = bucket.id;
export const bucketArn = bucket.arn;
Các dòng export để lộ output — có thể truy vấn sau bằng pulumi stack output bucketName.
Vòng lặp thay vì Copy-Paste
Cần ba S3 bucket cho các môi trường khác nhau? Trong Terraform, bạn phải dùng count hoặc for_each rồi vật lộn với cú pháp index. Trong Pulumi, chỉ cần map là xong:
import * as aws from "@pulumi/aws";
const environments = ["dev", "staging", "production"];
const buckets = environments.map(env =>
new aws.s3.Bucket(`app-bucket-${env}`, {
versioning: { enabled: env === "production" },
tags: { Environment: env },
})
);
export const bucketNames = buckets.map(b => b.id);
Ba resource, không lặp lại một dòng nào. Logic versioning — chỉ bật trong production — chỉ là một điều kiện đơn giản. Không cần workaround gì cả.
Ví dụ Đầy đủ hơn: EC2 với Security Group
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
const config = new pulumi.Config();
const instanceType = config.get("instanceType") || "t3.micro";
// Security group
const sg = new aws.ec2.SecurityGroup("web-sg", {
description: "Cho phép HTTP và SSH",
ingress: [
{ protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] }, // chỉ để demo — hãy giới hạn IP của bạn trong prod
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
],
egress: [
{ protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] },
],
});
// AMI Amazon Linux 2 mới nhất
const ami = aws.ec2.getAmi({
mostRecent: true,
owners: ["amazon"],
filters: [{ name: "name", values: ["amzn2-ami-hvm-*-x86_64-gp2"] }],
});
// EC2 instance
const server = new aws.ec2.Instance("web-server", {
instanceType: instanceType,
ami: ami.then(a => a.id),
vpcSecurityGroupIds: [sg.id],
tags: { Name: "pulumi-web-server" },
});
export const publicIp = server.publicIp;
export const publicDns = server.publicDns;
pulumi.Config() đọc từ file config của stack. Đặt giá trị bằng lệnh:
pulumi config set instanceType t3.small
Cùng một code, khác kích thước instance theo từng stack. Không cần sửa source, không cần loay hoay với file biến.
Stack: Một Codebase, Nhiều Môi trường
# Tạo stack staging
pulumi stack init staging
pulumi config set instanceType t3.small
# Quay lại dev
pulumi stack select dev
Mỗi stack có state và config riêng, triển khai độc lập. Nó thay thế cách làm khổ sở là copy-paste thư mục Terraform cho từng môi trường — thứ mà team nào rồi cũng hối hận.
Triển khai và Kiểm tra
Xem trước trước khi Áp dụng
Luôn chạy preview trước:
pulumi preview
Tương tự terraform plan, lệnh này hiển thị chính xác những gì Pulumi sẽ tạo, cập nhật hoặc xóa — với diff ở mức thuộc tính, không chỉ tên resource.
Triển khai
pulumi up
Pulumi hỏi xác nhận rồi mới áp dụng. Output trông như sau:
Updating (dev):
Type Name Status
+ pulumi:pulumi:Stack my-infra-dev created
+ aws:ec2:SecurityGroup web-sg created
+ aws:ec2:Instance web-server created
Outputs:
publicIp: "54.123.45.67"
publicDns: "ec2-54-123-45-67.ap-northeast-1.compute.amazonaws.com"
Resources:
+ 3 created
Duration: 42s
Kiểm tra Output của Stack
pulumi stack output publicIp
# 54.123.45.67
pulumi stack output --json
# { "publicDns": "...", "publicIp": "54.123.45.67" }
Xem Resource Graph
Gói free của Pulumi Cloud cho bạn xem resource graph trực quan, lịch sử triển khai và diff theo từng lần cập nhật. Muốn giữ state trên hạ tầng của mình? Trỏ nó về một S3 bucket:
pulumi login s3://your-state-bucket
Dọn dẹp
pulumi destroy
Xóa sạch mọi thứ được theo dõi trong state của stack. Không còn resource bị bỏ sót do lỡ tay click trên console.
Unit Test cho Hạ tầng
Vì là TypeScript, bạn có thể viết unit test đàng hoàng. SDK testing của Pulumi cho phép mock output của resource mà không cần đụng đến AWS thật:
npm install --save-dev @pulumi/pulumi mocha ts-node @types/mocha
// infra.test.ts
import * as pulumi from "@pulumi/pulumi";
pulumi.runtime.setMocks({
newResource: (args) => ({ id: `${args.name}-id`, state: args.inputs }),
call: (args) => ({ outputs: {} }),
});
describe("S3 Bucket", () => {
it("nên bật versioning trong môi trường production", async () => {
const infra = await import("./index");
// Thêm các assertion tại đây
});
});
Chạy mocha trên code hạ tầng giúp phát hiện lỗi cấu hình trước khi bất cứ thứ gì chạm vào tài khoản thật. Chỉ riêng điều đó đã cứu tôi khỏi không ít sự cố chiều thứ Sáu.
Bước tiếp theo
Các pattern trên xử lý được phần lớn công việc IaC hàng ngày. Khi đã quen, có một số lĩnh vực đáng khám phá thêm:
- Pulumi Component Resource: xây dựng các package hạ tầng tái sử dụng (một “VPC module” dưới dạng TypeScript class)
- CrossWalk for AWS: các abstraction AWS cấp cao hơn, tích hợp sẵn best practice theo mặc định
- Automation API: nhúng Pulumi vào ứng dụng backend của bạn để điều khiển triển khai theo kiểu lập trình
- Policy as Code (CrossGuard): định nghĩa và thực thi các quy tắc hạ tầng trước khi bất cứ thứ gì được triển khai
Việc chuyển từ các công cụ dựa trên YAML sang ngôn ngữ lập trình thật sự sẽ có chút bỡ ngỡ trong một hai ngày đầu — rồi đến một lúc nào đó bạn sẽ “ngộ” ra. Sau đó, quay lại HCL thô hay CloudFormation cảm giác như đang sửa XML bằng tay. Có những thứ một khi đã biết thì không thể quên được nữa.

