接下來幾天,將會介紹如何開始使用 Pulumi 做 IaC。
首先,來學習如何建立 AWS 的基礎網路環境,建立 VPC、Subnet 應該是 IaC 常做的任務,接著幾天我們會慢慢將我們建立 AWS 網路架構的 IaC 程式變得更加通用、並可在未來重用。
今天的範例中,我們會建立以下資源:
首先,我們要建立一個 Pulumi 專案並安裝 aws 的 package,讓我們可以操作 AWS 的資源。
在這裡,我們直接使用 aws-typescript 專案範本來建立專案,這個專案範本會使用 typescript 做為 Pulumi 開發程式語言,並預先安裝好 @pulumi/aws
套件。
建立專案的時候,會詢問我們預設要使用的 AWS 區域,輸入 ap-east-1
即可。
$ mkdir aws-vpc-ts
$ cd aws-vpc-ts
$ pulumi new aws-typescript
This command will walk you through creating a new Pulumi project.
Enter a value or leave blank to accept the (default), and press <ENTER>.
Press ^C at any time to quit.
project name: (aws-vpc-ts)
project description: (A minimal AWS TypeScript Pulumi program)
Created project 'aws-vpc-ts'
Please enter your desired stack name.
To create a stack in an organization, use the format <org-name>/<stack-name> (e.g. `acmecorp/dev`).
stack name: (dev)
Created stack 'dev'
aws:region: The AWS region to deploy into: (us-east-1) ap-east-1
Saved config
Installing dependencies...
added 219 packages, and audited 220 packages in 15s
68 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Finished installing dependencies
Your new project is ready to go! ✨
To perform an initial deployment, run `pulumi up`
小提醒:
- 如果輸入想要更改預設的可用區域,可以直接打開 Pulumi.dev.yaml,修改裡面的 aws:region 的內容即可。
- Pulumi aws package 預設會使用 AWS CLI 的 default profile 的 credentials 對 AWS 做操作,如果想要修改使用的 profile,可以透過
pulumi config set aws:profile <other-profile-name>
設定。之後我們講到 config 的時候,會再介紹這個指令。- 如果要使用 Python,可以透過 aws-python 專案範本來建立專案。
接著就可以打開 index.ts
開始我們自動化 AWS 資源的旅程了。
打開 index.ts
會發現裡面已經有一些程式碼了,這是 Pulumi aws-typescript 專案範本預設的內容,會建立一個 S3 Bucket。我們可以把所有內容刪除,從頭開始寫。
// 這些程式碼都可以刪除
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";
// Create an AWS resource (S3 Bucket)
const bucket = new aws.s3.Bucket("my-bucket");
// Export the name of the bucket
export const bucketName = bucket.id;
首先我們需要引用 @pulumi/aws 模組,並建立一個 aws.ec2.Vpc。
在 TypeScript 中,所有的雲端資源都是繼承自 pulumi.CustomResource
類別。而所有雲端資源的建構子都長的一樣,接受三個參數,分別是 name
、ResourceArguments
、PulumiOptions
。
name
: 這個資源在 pulumi 中的名稱,必需要獨一無二,不能與別人重複。這個名稱是 pulumi 所用,與雲端資源無任何關係ResourceArguments
:所要建立的資源需要的參數,例如我們建立 VPC 可能需要填入 cidrBlock
,至於哪些參數是必填、哪些不一定要填,就需要查文件了。PulumiOptions
:Pulumi 管理用的選項,例如可以設定保護,讓這個資源建立後不會再將來再被 Pulumi 刪除。或是可以用來忽略屬性更新導致的資源更新。例如我們使用 pulumi 取得目前最新的 AMI,並設定到 ec2 上,但這個 AMI 未來如果有更新,我不想更新 ec2 (因為更新 AMI 需要重建機器)。這個需求就可以設定 ignoreChange 的選項。以下是使用 TypeScript 建立 aws.ec2.Vpc 資源的方式,我們目前暫時不需要用到第三個參數 PulumiOptions
,因此可以先省略不寫。
import * as aws from "@pulumi/aws";
// 新增一個 VPC
const myVpc = new aws.ec2.Vpc("my-vpc", {
cidrBlock: "10.120.0.0/16"
});
但在 Python 中建立資源的邏輯就與 TypeScript 不同,Python 是將 ResourceArguments
使用 Keyword Argument (Named Argument) 的方式,PulumiOptions
則是透過 opts
設定。
例如以下範例就是新增一個 VPC,CIDR block 為 10.120.0.0/16
,並透過 protect=True 讓此資源未來不會被 Pulumi 刪除。
import pulumi
import pulumi_aws as aws
# 在 Python 中新增 VPC
vpc = aws.ec2.Vpc('my-vpc',
cidr_block='10.120.0.0/16',
enable_dns_hostnames=True,
opts=pulumi.ResourceOptions(
protect=True,
))
接著我們就能用 pulumi up
來執行我們的程式,建立第一個 VPC 了!
$ pulumi up
Previewing update (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/xxxxxx/aws-vpc-ts/dev/previews/38a0c5ff-9882-441c-9ad6-d2403f947aab
Type Name Plan
+ pulumi:pulumi:Stack aws-vpc-ts-dev create
+ └─ aws:ec2:Vpc my-vpc create
Resources:
+ 2 to create
Do you want to perform this update? yes
Updating (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/xxxxxx/aws-vpc-ts/dev/updates/5
Type Name Status
+ pulumi:pulumi:Stack aws-vpc-ts-dev created (1s)
+ └─ aws:ec2:Vpc my-vpc created (1s)
Resources:
+ 2 created
Duration: 4s
也可以在 Pulumi Cloud 中看到我們所建立的資源,而且按下 Status 上的 AWS 連結,可以開啟 AWS Console 的 VPC 頁面。
接著我們要來建立 2 個 Public Subnet。其實在 AWS 中只有 Subnet 的資源,並沒有什麼 Public Subnet、Private Subnet。
Public Subnet 是指在該 Subnet 內有個 Route 會將非 VPC 的流量直接網 Internet Gateway。只要任何有 public IP 的資源在這個子網路中,就可以直接被外部存取,也能隨意用該 public IP 存取 Internet。詳細的說明可以參考 AWS 的 Internet Gateway 文件。
在這邊我們會直接將建立 VPC 時,一併建立的 default route table 拿來當作所有 public subnet 的 route table。並建立一個 Internet Gateway 並與 default route table 關聯。
首先在剛剛建立的 VPC 中建立 Internet Gateway,在前一段程式碼中,我們新增的 aws.ec2.Vpc 指定到 myVpc
變數中,接著我們就能在這邊直接取得 myVpc
的 VPC ID,並指定給 InternetGateway 做為他的參數。
// 新增 Internet Gateway
const igw = new aws.ec2.InternetGateway("my-igw", {
vpcId: myVpc.id
});
接著我們 取得 Default Route Table。這邊要特別注意,以下程式碼看起來很像是 新增 一個 default route table,但其實做的事情是取得這個 default route table。至於為何會這樣設計?這就要牽扯到 Pulumi 的 AWS Package 其實是透過 pulumi terraform bridge 去使用 terraform 的 provider。而在 Terraform AWS Provider 中,關於 aws_default_route_table 的文件中就有關於這點的描述,有興趣的朋友可以自行閱讀。
// 取得 Default Route Table
const defaultRT = new aws.ec2.DefaultRouteTable("my-default-rt", {
defaultRouteTableId: myVpc.defaultRouteTableId,
});
取得 default route table 後,將 0.0.0.0/0
的流量導向 Internet Gateway,就完成這個 route table 的設定了!
這邊我就不將 Route 指定到任何變數了,因為這只是新增一個 Route 資源,但後面不會參考到它任何的資訊。
// 設定 0.0.0.0/0 -> IGW 的 Route 到 Default Route Table
new aws.ec2.Route("my-default-rt-default-route", {
routeTableId: defaultRT.id,
destinationCidrBlock: "0.0.0.0/0",
gatewayId: igw.id,
});
設定完了 Route Table 後,就可以來建立 Subnet 了!
我們直接 new
兩個 aws.ec2.Subnet,並把它們放進 publicSubnets 的物件中(這種寫法在 Python 可以當作是將它們放進 dict 中)。cidrBlock 與 availabilityZone 就先寫死,之後的文章再來改善。
// 建立 Public Subnets
const publicSubnets: Record<string, aws.ec2.Subnet> = {
'my-public-subnet-1': new aws.ec2.Subnet("my-public-subnet-1", {
vpcId: myVpc.id,
cidrBlock: '10.120.0.0/24',
availabilityZone: 'ap-east-1a',
}),
'my-public-subnet-2': new aws.ec2.Subnet("my-public-subnet-2", {
vpcId: myVpc.id,
cidrBlock: '10.120.1.0/24',
availabilityZone: 'ap-east-1b',
})
};
建立好兩個 publicSubnet 後,我們來將它們與 Default Route Table 關聯 (其實不用做這件事,畢竟它都叫 Default
了,預設沒做任何設定的 Route Table 就是它,這邊只是為了練習如何關聯 Route Table 而已)。
這邊就是 Pulumi 使用程式語言的優勢了,我們可以直接寫 for...of 迴圈,將每個 Subnet 與 Default Route Table 用 RouteTableAssociation 關聯在一起。
// 使用迴圈來設定 Route Table 關聯
for (const subnetName of Object.keys(publicSubnets)) {
new aws.ec2.RouteTableAssociation(`my-${subnetName}-rt-association`, {
routeTableId: defaultRT.id,
subnetId: publicSubnets[subnetName].id,
});
}
如果是熟悉 TypeScript 或 JavaScript 的朋友,也可以這樣寫:
// 使用 JS 的 functional programming 風格撰寫一樣的程式
Object.keys(publicSubnets).forEach(subnetName => {
new aws.ec2.RouteTableAssociation(`${subnetName}-rt-association`, {
routeTableId: defaultRT.id,
subnetId: publicSubnets[subnetName].id,
});
});
接著我們就使用 pulumi up
套用這些變更後,使用 pulumi staack
指令,列出 Stack 中所有的 Resource 吧!
可以從輸出看到,我們有以下資源:
其他還有 pulumi 自動建立的 Stack 與 aws provider。
Current stack is dev:
Owner: xxxxxx
Last updated: 11 seconds ago (2023-09-20 23:05:34.61341 +0800 CST)
Pulumi version used: v3.79.0
Current stack resources (10):
TYPE NAME
pulumi:pulumi:Stack aws-vpc-ts-dev
├─ aws:ec2/vpc:Vpc my-vpc
├─ aws:ec2/subnet:Subnet my-public-subnet-1
├─ aws:ec2/internetGateway:InternetGateway my-igw
├─ aws:ec2/subnet:Subnet my-public-subnet-2
├─ aws:ec2/defaultRouteTable:DefaultRouteTable my-default-rt
├─ aws:ec2/route:Route my-default-rt-default-route
├─ aws:ec2/routeTableAssociation:RouteTableAssociation my-public-subnet-1-rt-association
├─ aws:ec2/routeTableAssociation:RouteTableAssociation my-public-subnet-2-rt-association
└─ pulumi:providers:aws default_5_42_0
要建立 2 個 Private Subnet,並讓 Private Subnet 中的 IP 要可以出 Internet,就要透過 NAT Gateway 了。
這邊我們規劃新增 1 個 NAT Gateway,放在第一個 Public Subnet中、2 個 Subnet、兩張 Route Table 分別控制這兩個 Private Subent,可以透過 NAT Gateway 出外網。
建立 NAT Gateway
NAT Gateway 需要一個 Elastic IP address,在 AWS console 建立 NAT Gateway 預設都會一起建立。但我們使用 IaC 就得自己來。
// 建立一個 Elastic IP Address
const eip = new aws.ec2.Eip("my-nat-gateway-eip", {});
// 建立 NAT Gateway 在第一個 Public Subnet 中
const natGateway = new aws.ec2.NatGateway("my-nat-gateway", {
subnetId: Object.values(publicSubnets)[0].id,
allocationId: eip.id,
});
建立 Subnet,關聯至對應的 Route Table,並設定 0.0.0.0/0
至 NAT Gateway
這邊我們使用了與建立 Public Subnet 不一樣的方式,預先將兩個 Private Subnet 會用到的 CIDR 與 AvailabilityZone 變成一個陣列,這樣我們就可以直接用一個迴圈來建立 Private Subnet了。
// 建立 Private Subnets
const privateSubnetArgs = [
{cidr: '10.120.128.0/24', az: 'ap-east-1a'},
{cidr: '10.120.129.0/24', az: 'ap-east-1b'}
];
const privateSubnets: Record<string, aws.ec2.Subnet> = {};
for (const arg of privateSubnetArgs) {
// 建立 subnet
const subnet = new aws.ec2.Subnet(`my-private-subnet-${arg.az}`, {
vpcId: myVpc.id,
cidrBlock: arg.cidr,
availabilityZone: arg.az,
});
// 將 subnet 記錄到 privateSubnets dictionary 中
privateSubnets[`my-private-subnet-${arg.az}`] = subnet;
// 建立屬於這個 subnet 的 Route Table
const rt = new aws.ec2.RouteTable(`my-private-subnet-${arg.az}-rt`, {
vpcId: myVpc.id,
// 設定 route,0.0.0.0/0 到 natGateway
routes: [{cidrBlock: '0.0.0.0/0', natGatewayId: natGateway.id}]
});
// 設定 Route Table 與 Subnet 的關聯
new aws.ec2.RouteTableAssociation(`my-private-subnet-${arg.az}-rt-association`, {
routeTableId: rt.id,
subnetId: subnet.id,
});
}
最後我們再執行一次 pulumi up
,建立所有的資源。然後使用 pulumi stack
觀察所有的資源。
Current stack is dev:
Owner: xxxxxx
Last updated: 57 seconds ago (2023-09-20 23:24:57.020617 +0800 CST)
Pulumi version used: v3.79.0
Current stack resources (18):
TYPE NAME
pulumi:pulumi:Stack aws-vpc-ts-dev
├─ aws:ec2/vpc:Vpc my-vpc
├─ aws:ec2/subnet:Subnet my-private-subnet-ap-east-1b
├─ aws:ec2/defaultRouteTable:DefaultRouteTable my-default-rt
├─ aws:ec2/subnet:Subnet my-public-subnet-2
├─ aws:ec2/internetGateway:InternetGateway my-igw
├─ aws:ec2/subnet:Subnet my-private-subnet-ap-east-1a
├─ aws:ec2/subnet:Subnet my-public-subnet-1
├─ aws:ec2/routeTableAssociation:RouteTableAssociation my-public-subnet-2-rt-association
├─ aws:ec2/route:Route my-default-rt-default-route
├─ aws:ec2/eip:Eip my-nat-gateway-eip
├─ aws:ec2/routeTableAssociation:RouteTableAssociation my-public-subnet-1-rt-association
├─ aws:ec2/natGateway:NatGateway my-nat-gateway
├─ aws:ec2/routeTable:RouteTable my-private-subnet-ap-east-1b-rt
├─ aws:ec2/routeTable:RouteTable my-private-subnet-ap-east-1a-rt
├─ aws:ec2/routeTableAssociation:RouteTableAssociation my-private-subnet-ap-east-1b-rt-association
├─ aws:ec2/routeTableAssociation:RouteTableAssociation my-private-subnet-ap-east-1a-rt-association
└─ pulumi:providers:aws default_5_42_0
最後,我們可以透過 AWS VPC 介面中的 Resource map 來觀看這次的成果:
今天的 TypeScript、Python 範例程式在這:https://github.com/a60814billy/pulumi-in-30days/tree/main/day05
明天我們會基於今天的範例,繼續改善程式碼!