#บทนำ

Pulumi เป็นเครื่องมือ Infrastructure as Code (IaC) ที่ช่วยสร้างและจัดการ Cloud Infrastructure ด้วยภาษาโปรแกรม เช่น TypeScript, Python, Go และ C# แทนการกำหนดโครงสร้างพื้นฐานด้วยไฟล์คอนฟิกเพียงอย่างเดียว บทความนี้ใช้ TypeScript เพื่อสร้าง AWS EC2 พร้อม Security Group และติดตั้ง Nginx ผ่าน User Data

ข้อควรระวัง: AWS EC2, EBS และทรัพยากรเครือข่ายบางชนิดอาจมีค่าใช้จ่าย แม้จะทดลองช่วงสั้น ๆ ควรตรวจสอบราคาและสิทธิ Free Tier ของบัญชีก่อนเริ่ม และลบทรัพยากรเมื่อเสร็จสิ้น

#1. สถาปัตยกรรม

flowchart LR
    Dev[Developer] --> CLI[Pulumi CLI]
    CLI --> State[Pulumi State Backend]
    CLI --> AWS[AWS Provider]
    AWS --> SG[Security Group: TCP 80]
    AWS --> EC2[EC2 Instance]
    SG --> EC2
    EC2 --> Nginx[Nginx Web Server]
    Browser[Web Browser] -->|HTTP 80| Nginx

ตัวอย่างนี้ใช้ Default VPC ของ Region ที่กำหนด และเปิดเฉพาะ HTTP TCP/80 สู่สาธารณะ ไม่เปิด SSH เพื่อจำกัดพื้นผิวการโจมตี เหมาะกับการสาธิตเท่านั้น; ระบบจริงควรใช้ HTTPS, Load Balancer และการจัดการเครือข่ายที่เหมาะสม

#2. สิ่งที่ต้องเตรียม

  • บัญชี AWS และ IAM credentials ที่มีสิทธิสร้าง/ลบ EC2, Security Group และอ่านข้อมูล VPC/AMI
  • AWS CLI และ Pulumi CLI
  • Node.js LTS และ npm
  • AWS Region ที่มี Default VPC เช่น ap-southeast-1 (ตรวจสอบในบัญชีจริง)

ตั้งค่า credentials ผ่าน AWS CLI:

aws configure --profile pulumi-lab
export AWS_PROFILE=pulumi-lab
aws sts get-caller-identity

บน Windows PowerShell ใช้ $env:AWS_PROFILE="pulumi-lab" แทน export

#3. สร้างโปรเจกต์ Pulumi

mkdir pulumi-aws-ec2 && cd pulumi-aws-ec2
pulumi new aws-typescript
npm install
pulumi config set aws:region ap-southeast-1

เมื่อระบบถามให้เลือก Stack เช่น dev และเลือก Pulumi Cloud หรือ State Backend ที่องค์กรกำหนด หลีกเลี่ยงการเก็บไฟล์ State ที่มีข้อมูลอ่อนไหวใน Repository สาธารณะ

#4. เขียน Infrastructure ใน index.ts

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config();
const instanceType = config.get("instanceType") || "t3.micro";

const defaultVpc = aws.ec2.getVpc({ default: true });
const defaultSubnets = defaultVpc.then(vpc =>
  aws.ec2.getSubnets({
    filters: [{ name: "vpc-id", values: [vpc.id] }],
  })
);

const webSecurityGroup = new aws.ec2.SecurityGroup("web-sg", {
  vpcId: defaultVpc.then(vpc => vpc.id),
  description: "Allow HTTP for Pulumi workshop",
  ingress: [{
    protocol: "tcp",
    fromPort: 80,
    toPort: 80,
    cidrBlocks: ["0.0.0.0/0"],
    description: "Public HTTP demo",
  }],
  egress: [{
    protocol: "-1",
    fromPort: 0,
    toPort: 0,
    cidrBlocks: ["0.0.0.0/0"],
  }],
});

const ami = aws.ec2.getAmi({
  mostRecent: true,
  owners: ["amazon"],
  filters: [
    { name: "name", values: ["al2023-ami-2023.*-x86_64"] },
    { name: "architecture", values: ["x86_64"] },
    { name: "virtualization-type", values: ["hvm"] },
  ],
});

const web = new aws.ec2.Instance("web-server", {
  ami: ami.then(result => result.id),
  instanceType,
  subnetId: defaultSubnets.then(s => s.ids[0]),
  vpcSecurityGroupIds: [webSecurityGroup.id],
  associatePublicIpAddress: true,
  rootBlockDevice: { volumeSize: 8, volumeType: "gp3" },
  userData: `#!/bin/bash
set -euxo pipefail
dnf install -y nginx
systemctl enable --now nginx
cat > /usr/share/nginx/html/index.html <<'HTML'
<!doctype html>
<html lang="en"><meta charset="utf-8"><title>Pulumi EC2</title>
<body><h1>Hello from Pulumi + AWS EC2!</h1></body></html>
HTML
`,
  tags: { Name: "pulumi-workshop-web", Environment: "dev" },
});

export const instanceId = web.id;
export const publicIp = web.publicIp;
export const websiteUrl = pulumi.interpolate`http://${web.publicIp}`;

หมายเหตุ: AMI ของ Amazon Linux 2023 และชนิดอินสแตนซ์ต้องรองรับ Region ที่เลือก การเลือก AMI ล่าสุดอาจทำให้ Pulumi เสนอเปลี่ยนอินสแตนซ์ในอนาคต ควรตรึง AMI ID สำหรับระบบ Production; ตัวอย่างนี้ใช้ Default Subnet แรกเพื่อความง่าย ไม่รับประกันความพร้อมใช้งานทุกบัญชี

#5. Deploy

pulumi config set instanceType t3.micro
pulumi preview
pulumi up

ตรวจสอบรายการทรัพยากรและค่าใช้จ่ายก่อนยืนยัน หลัง Deploy เสร็จ:

pulumi stack output websiteUrl
curl "$(pulumi stack output websiteUrl)"

Nginx อาจใช้เวลา 1–3 นาทีในการติดตั้งหลัง EC2 เริ่มทำงาน

#6. ตรวจสอบผ่าน AWS CLI

INSTANCE_ID=$(pulumi stack output instanceId)
aws ec2 describe-instances \
  --instance-ids "$INSTANCE_ID" \
  --query 'Reservations[].Instances[].[InstanceId,State.Name,PublicIpAddress]' \
  --output table

#7. แก้ไขและอัปเดต

ลองเปลี่ยนข้อความ HTML ใน userData แล้วเรียก:

pulumi preview
pulumi up

ข้อควรทราบ: การแก้ไข User Data ไม่ได้รับประกันว่าจะรันสคริปต์อีกครั้งบนอินสแตนซ์เดิม การตั้งค่า User Data อาจทำให้ EC2 หยุด/เริ่มใหม่หรือถูกแทนที่ตาม Provider และการเปลี่ยนแปลงที่เกิดขึ้น สำหรับ Production ควรใช้ Image Baking, Configuration Management หรือกลไก Deployment ของแอปพลิเคชัน

#8. ลบทรัพยากรเพื่อหยุดค่าใช้จ่าย

pulumi destroy
pulumi stack rm

ตรวจสอบใน AWS Console ว่า EC2, EBS Volume และ Security Group ของ Workshop ถูกลบแล้ว และตรวจสอบ Billing ด้วย

#9. แนวทางสำหรับ Production

  1. ใช้ Private Subnet และ ALB พร้อม HTTPS/ACM แทนการเปิด HTTP ตรงสู่ EC2
  2. ใช้ IAM Role สำหรับ EC2 แทนการฝังคีย์ใน Source Code
  3. ใช้ SSM Session Manager แทนการเปิด SSH สู่ Internet
  4. กำหนด Tags, Budget Alerts, Logging และ Monitoring ด้วย CloudWatch
  5. ใช้ Stack แยก dev, staging, prod และ CI/CD ตรวจ pulumi preview ก่อน Deploy
  6. ตรวจสอบ Policy, Secrets และการควบคุมสิทธิของ Pulumi State Backend

#10. สรุป

Pulumi ช่วยจัดการ AWS Infrastructure แบบประกาศสถานะที่ต้องการ (Declarative) ผ่าน TypeScript โดย Workshop นี้สาธิตการสร้าง EC2, Security Group, การติดตั้ง Nginx, การส่งออก URL และการลบทรัพยากรอย่างเป็นระบบ เหมาะสำหรับต่อยอดสู่การ Deploy แอปพลิเคชันและ CI/CD

#เอกสารอ้างอิง