iT邦幫忙

2023 iThome 鐵人賽

DAY 5
0
Cloud Native

30 天學習 Pulumi:用各種程式語言控制雲端資源系列 第 5

[Day 05] 使用 Pulumi 建立一個標準的 AWS VPC

  • 分享至 

  • xImage
  •  

接下來幾天,將會介紹如何開始使用 Pulumi 做 IaC。

首先,來學習如何建立 AWS 的基礎網路環境,建立 VPC、Subnet 應該是 IaC 常做的任務,接著幾天我們會慢慢將我們建立 AWS 網路架構的 IaC 程式變得更加通用、並可在未來重用。

今天的範例中,我們會建立以下資源:

  • 建立一個 VPC 在 ap-east-1 (香港)
  • 在 VPC 中建立 Subnet
    • 2 個 Public Subnet 在不同的可用區
    • 2 個 Private Subnet 在不同的可用區
  • 設定 NAT Gateway,並讓 2 個 Private Subnet 可以透過 NAT Gateway 到 Internet

開新專案

首先,我們要建立一個 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`

小提醒:

  1. 如果輸入想要更改預設的可用區域,可以直接打開 Pulumi.dev.yaml,修改裡面的 aws:region 的內容即可。
  2. Pulumi aws package 預設會使用 AWS CLI 的 default profile 的 credentials 對 AWS 做操作,如果想要修改使用的 profile,可以透過 pulumi config set aws:profile <other-profile-name> 設定。之後我們講到 config 的時候,會再介紹這個指令。
  3. 如果要使用 Python,可以透過 aws-python 專案範本來建立專案。

建立 VPC

接著就可以打開 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 類別。而所有雲端資源的建構子都長的一樣,接受三個參數,分別是 nameResourceArgumentsPulumiOptions

  • 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 頁面。

https://ithelp.ithome.com.tw/upload/images/20230920/20162822rl9UHVCSGq.png

https://ithelp.ithome.com.tw/upload/images/20230920/20162822WXXZZj7m55.png

繼續建立更多的資源 (Internet Gateway, Default Route Table, Route)

接著我們要來建立 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,
});

建立 Public Subnet

設定完了 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 吧!

可以從輸出看到,我們有以下資源:

  • VPC
  • 2 個 Subnet
  • InternetGateway
  • DefaultRouteTable
  • Route
  • 2 個 RouteTableAssociation

其他還有 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

建立 Private Subnet

要建立 2 個 Private Subnet,並讓 Private Subnet 中的 IP 要可以出 Internet,就要透過 NAT Gateway 了。

這邊我們規劃新增 1 個 NAT Gateway,放在第一個 Public Subnet中、2 個 Subnet、兩張 Route Table 分別控制這兩個 Private Subent,可以透過 NAT Gateway 出外網。

  1. 建立 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,
});
  1. 建立 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 來觀看這次的成果:

https://ithelp.ithome.com.tw/upload/images/20230920/20162822x709KQsZaX.png


今天的 TypeScript、Python 範例程式在這:https://github.com/a60814billy/pulumi-in-30days/tree/main/day05

明天我們會基於今天的範例,繼續改善程式碼!


上一篇
[Day 04] Pulumi 專案結構、Stack 操作
下一篇
[Day 06] 善用程式語言優勢撰寫 Pulumi IaC
系列文
30 天學習 Pulumi:用各種程式語言控制雲端資源30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言