Infrastructure as Code với Pulumi và TypeScript: Quản lý Tài nguyên Cloud như Code Thật sự

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

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, hay async/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 project
  • Pulumi.dev.yaml — config riêng cho từng stack
  • index.ts — code hạ tầng của bạn
  • package.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.

Share: