初探 Pulumi 上傳靜態網站到 AWS S3 (二)

cover

上一篇『初探 Pulumi 上傳靜態網站到 AWS S3 (一)』主要介紹 Pulumi 基本使用方式,而本篇會延續上一篇教學把剩下的章節教完,底下是本篇會涵蓋的章節內容:

  1. 設定 Pulumi Stack 環境變數
  2. 建立第二個 Pulumi Stack 環境
  3. 刪除 Pulumi Stack 環境

讓開發者可以自由新增各種不同環境,像是 Testing 或 Develop 環境,以及該如何動態帶入不同環境的變數內容,最後可以透過單一指令將全部資源刪除。

教學影片

  • 00:00​ Pulumi 教學內容簡介 (Stack 介紹)
  • 01:04​ 設定 Pulumi Stack 環境變數
  • 05:04​ 用 Go 語言讀取多個檔案上傳到 AWS S3
  • 08:04​ 用 Pulumi 建立多個開發環境
  • 09:00​ 用 pulumi config 設定環境參數
  • 12:35​ 如何清除單一 Pulumi Stack 環境
  • 15:30​ Pulumi 管理 AWS 心得

設定 Pulumi Stack 環境變數

大家可以看到,現在所有 main.go 的程式碼,都是直接 hardcode 的,那怎麼透過一些環境變數來動態改變設定呢?這時候可以透過 pulumi config 指令來調整喔,底下來看看怎麼實作,假設我們要讀取的 index.html 放在其他目錄底下,該怎麼動態調整?

步驟一: 撰寫讀取 Config 函式

func getEnv(ctx *pulumi.Context, key string, fallback ...string) string {
    if value, ok := ctx.GetConfig(key); ok {
        return value
    }

    if len(fallback) > 0 {
        return fallback[0]
    }

    return ""
}

pulumi 的 context 內有一個讀取環境變數函式叫 GetConfig,接著我們在設計一個 fallback 當作 default 回傳值。底下設定一個變數 s3:siteDir

pulumi config set s3:siteDir production

打開 Pulumi.dev.yaml 可以看到

config:
  aws:profile: demo
  aws:region: ap-northeast-1
  s3:siteDir: production

接著將程式碼改成如下:

        site := getEnv(ctx, "s3:siteDir", "content")
        index := path.Join(site, "index.html")
        _, err = s3.NewBucketObject(ctx, "index.html", &s3.BucketObjectArgs{
            Bucket:      bucket.Bucket,
            Source:      pulumi.NewFileAsset(index),
            Acl:         pulumi.String("public-read"),
            ContentType: pulumi.String(mime.TypeByExtension(path.Ext(index))),
        })

步驟二: 更新 Infrastructure

$ pulumi up
Previewing update (dev)

View Live: https://app.pulumi.com/appleboy/demo/dev/previews/d76d2f9b-16c8-4bfd-820d-d5368d29f592

     Type                    Name        Plan       Info
     pulumi:pulumi:Stack     demo-dev
 ~   └─ aws:s3:BucketObject  index.html  update     [diff: ~source]

Resources:
    ~ 1 to update
    2 unchanged

Do you want to perform this update? details
  pulumi:pulumi:Stack: (same)
    [urn=urn:pulumi:dev::demo::pulumi:pulumi:Stack::demo-dev]
    ~ aws:s3/bucketObject:BucketObject: (update)
        [id=index.html]
        [urn=urn:pulumi:dev::demo::aws:s3/bucketObject:BucketObject::index.html]
      - source: asset(file:77aab46) { content/index.html }
      + source: asset(file:01c09f4) { production/index.html }

可以看到 source 會被換成 production/index.html

步驟三: 讀取更多檔案

整個 Web 專案肯定不止一個檔案,所以再來改一下原本的讀取檔案列表流程

        site := getEnv(ctx, "s3:siteDir", "content")
        files, err := ioutil.ReadDir(site)
        if err != nil {
            return err
        }

        for _, item := range files {
            name := item.Name()
            if _, err = s3.NewBucketObject(ctx, name, &s3.BucketObjectArgs{
                Bucket:      bucket.Bucket,
                Source:      pulumi.NewFileAsset(filepath.Join(site, name)),
                Acl:         pulumi.String("public-read"),
                ContentType: pulumi.String(mime.TypeByExtension(path.Ext(filepath.Join(site, name)))),
            }); err != nil {
                return err
            }
        }

執行部署

     Type                    Name        Status      Info
     pulumi:pulumi:Stack     demo-dev
 +   ├─ aws:s3:BucketObject  about.html  created
 ~   └─ aws:s3:BucketObject  index.html  updated     [diff: ~source]

Outputs:
    bucketEndpoint: "foobar-1234.s3-website-ap-northeast-1.amazonaws.com"
    bucketID      : "foobar-1234"
    bucketName    : "foobar-1234"

Resources:
    + 1 created
    ~ 1 updated
    2 changes. 2 unchanged

Duration: 9s

完整程式碼如下:

package main

import (
    "io/ioutil"
    "mime"
    "path"
    "path/filepath"

    "github.com/pulumi/pulumi-aws/sdk/v3/go/aws/s3"
    "github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)

func main() {
    pulumi.Run(func(ctx *pulumi.Context) error {
        // Create an AWS resource (S3 Bucket)
        bucket, err := s3.NewBucket(ctx, "my-bucket", &s3.BucketArgs{
            Bucket: pulumi.String("foobar-1234"),
            Website: s3.BucketWebsiteArgs{
                IndexDocument: pulumi.String("index.html"),
            },
        })
        if err != nil {
            return err
        }

        site := getEnv(ctx, "s3:siteDir", "content")
        files, err := ioutil.ReadDir(site)
        if err != nil {
            return err
        }

        for _, item := range files {
            name := item.Name()
            if _, err = s3.NewBucketObject(ctx, name, &s3.BucketObjectArgs{
                Bucket:      bucket.Bucket,
                Source:      pulumi.NewFileAsset(filepath.Join(site, name)),
                Acl:         pulumi.String("public-read"),
                ContentType: pulumi.String(mime.TypeByExtension(path.Ext(filepath.Join(site, name)))),
            }); err != nil {
                return err
            }
        }

        // Export the name of the bucket
        ctx.Export("bucketID", bucket.ID())
        ctx.Export("bucketName", bucket.Bucket)
        ctx.Export("bucketEndpoint", bucket.WebsiteEndpoint)

        return nil
    })
}

func getEnv(ctx *pulumi.Context, key string, fallback ...string) string {
    if value, ok := ctx.GetConfig(key); ok {
        return value
    }

    if len(fallback) > 0 {
        return fallback[0]
    }

    return ""
}

建立第二個 Pulumi Stack 環境

在 Pulumi 可以很簡單的建立多種環境,像是 Testing 或 Production,只要將動態變數抽出來設定成 config 即可。底下來看看怎麼建立全先的環境,這步驟在 Pulumi 叫做 Stack。前面已經建立一個 dev 環境,現在我們要建立一個全新環境來部署 Testing 或 Production 該如何做呢?

步驟一: 建立全新 Stack 環境

透過 pulumi stack 可以建立全新環境

$ pulumi stack ls
NAME  LAST UPDATE   RESOURCE COUNT  URL
dev*  1 minute ago  5               https://app.pulumi.com/appleboy/demo/dev

建立 stack

$ pulumi stack init prod
Created stack 'prod'
$ pulumi stack ls
NAME   LAST UPDATE   RESOURCE COUNT  URL
dev    1 minute ago  5               https://app.pulumi.com/appleboy/demo/dev
prod*  n/a           n/a             https://app.pulumi.com/appleboy/demo/prod

設定參數

pulumi config set s3:siteDir www
pulumi config set aws:profile demo
pulumi config set aws:region ap-northeast-1

步驟二: 建立 www 內容

建立 content/www 目錄,一樣放上 index.htm + about.html

<html>
  <body>
    <h1>Hello Pulumi S3 Bucket From New Stack</h1>
  </body>
</html>

about.html

<html>
  <body>
    <h1>About us From New Stack</h1>
  </body>
</html>

步驟三: 部署 New Stack

先看看 Preview 結果

$ pulumi up
Previewing update (prod)

View Live: https://app.pulumi.com/appleboy/demo/prod/previews/3b85a340-0e71-455e-9b96-48dc38538d18

     Type                    Name        Plan
 +   pulumi:pulumi:Stack     demo-prod   create
 +   ├─ aws:s3:Bucket        my-bucket   create
 +   ├─ aws:s3:BucketObject  index.html  create
 +   └─ aws:s3:BucketObject  about.html  create

Resources:
    + 4 to create

Do you want to perform this update? details
+ pulumi:pulumi:Stack: (create)
    [urn=urn:pulumi:prod::demo::pulumi:pulumi:Stack::demo-prod]
    + aws:s3/bucket:Bucket: (create)
        [urn=urn:pulumi:prod::demo::aws:s3/bucket:Bucket::my-bucket]
        acl         : "private"
        bucket      : "my-bucket-ba8088c"
        forceDestroy: false
        website     : {
            indexDocument: "index.html"
        }
    + aws:s3/bucketObject:BucketObject: (create)
        [urn=urn:pulumi:prod::demo::aws:s3/bucketObject:BucketObject::index.html]
        acl         : "public-read"
        bucket      : "my-bucket-ba8088c"
        contentType : "text/html; charset=utf-8"
        forceDestroy: false
        key         : "index.html"
        source      : asset(file:460188b) { www/index.html }
    + aws:s3/bucketObject:BucketObject: (create)
        [urn=urn:pulumi:prod::demo::aws:s3/bucketObject:BucketObject::about.html]
        acl         : "public-read"
        bucket      : "my-bucket-ba8088c"
        contentType : "text/html; charset=utf-8"
        forceDestroy: false
        key         : "about.html"
        source      : asset(file:376c42a) { www/about.html }

如果看起來沒問題,就可以直接執行了

Updating (prod)

View Live: https://app.pulumi.com/appleboy/demo/prod/updates/1

     Type                    Name        Status
 +   pulumi:pulumi:Stack     demo-prod   created
 +   ├─ aws:s3:Bucket        my-bucket   created
 +   ├─ aws:s3:BucketObject  about.html  created
 +   └─ aws:s3:BucketObject  index.html  created

Outputs:
    bucketEndpoint: "my-bucket-a7044ab.s3-website-ap-northeast-1.amazonaws.com"
    bucketID      : "my-bucket-a7044ab"
    bucketName    : "my-bucket-a7044ab"

Resources:
    + 4 created

Duration: 18s

最後用 curl 執行看看

$ curl -v $(pulumi stack output bucketEndpoint)
*   Trying 52.219.8.20...
* TCP_NODELAY set
* Connected to my-bucket-a7044ab.s3-website-ap-northeast-1.amazonaws.com (52.219.8.20) port 80 (#0)
> GET / HTTP/1.1
> Host: my-bucket-a7044ab.s3-website-ap-northeast-1.amazonaws.com
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< x-amz-id-2: oGxc+rLPi3kLOZslMsOmJqPY/WGeMoxX9sXJDRj4wlJlGVq+7pMx3ers71jxnDiDkeM9JRrd+T8=
< x-amz-request-id: 528235DDFF40F365
< Date: Thu, 11 Feb 2021 04:49:21 GMT
< Last-Modified: Thu, 11 Feb 2021 04:48:41 GMT
< ETag: "ae41d1b3f0aeef6a490e1b2edc74d2b5"
< Content-Type: text/html; charset=utf-8
< Content-Length: 85
< Server: AmazonS3
<
<html>
  <body>
    <h1>Hello Pulumi S3 Bucket From New Stack</h1>
  </body>
</html>
* Connection #0 to host my-bucket-a7044ab.s3-website-ap-northeast-1.amazonaws.com left intact
* Closing connection 0

刪除 Pulumi Stack 環境

最後步驟就是要學習怎麼一鍵刪除整個 Infrastructure 環境。現在我們已經建立兩個 Stack 環境,該怎麼移除?

步驟一: 刪除所有資源

pulumi destroy 指令可以刪除全部資源

Previewing destroy (prod)

View Live: https://app.pulumi.com/appleboy/demo/prod/previews/92f9c4a4-f4a9-464d-be27-5040aff295ae

     Type                    Name        Plan
 -   pulumi:pulumi:Stack     demo-prod   delete
 -   ├─ aws:s3:BucketObject  about.html  delete
 -   ├─ aws:s3:BucketObject  index.html  delete
 -   └─ aws:s3:Bucket        my-bucket   delete

Outputs:
  - bucketEndpoint: "my-bucket-a7044ab.s3-website-ap-northeast-1.amazonaws.com"
  - bucketID      : "my-bucket-a7044ab"
  - bucketName    : "my-bucket-a7044ab"

Resources:
    - 4 to delete

Do you want to perform this destroy? details
- aws:s3/bucketObject:BucketObject: (delete)
    [id=about.html]
    [urn=urn:pulumi:prod::demo::aws:s3/bucketObject:BucketObject::about.html]
- aws:s3/bucketObject:BucketObject: (delete)
    [id=index.html]
    [urn=urn:pulumi:prod::demo::aws:s3/bucketObject:BucketObject::index.html]
- aws:s3/bucket:Bucket: (delete)
    [id=my-bucket-a7044ab]
    [urn=urn:pulumi:prod::demo::aws:s3/bucket:Bucket::my-bucket]
- pulumi:pulumi:Stack: (delete)
    [urn=urn:pulumi:prod::demo::pulumi:pulumi:Stack::demo-prod]
    --outputs:--
  - bucketEndpoint: "my-bucket-a7044ab.s3-website-ap-northeast-1.amazonaws.com"
  - bucketID      : "my-bucket-a7044ab"
  - bucketName    : "my-bucket-a7044ab"

選擇 yse 移除所以資源

Destroying (prod)

View Live: https://app.pulumi.com/appleboy/demo/prod/updates/2

     Type                    Name        Status
 -   pulumi:pulumi:Stack     demo-prod   deleted
 -   ├─ aws:s3:BucketObject  index.html  deleted
 -   ├─ aws:s3:BucketObject  about.html  deleted
 -   └─ aws:s3:Bucket        my-bucket   deleted

Outputs:
  - bucketEndpoint: "my-bucket-a7044ab.s3-website-ap-northeast-1.amazonaws.com"
  - bucketID      : "my-bucket-a7044ab"
  - bucketName    : "my-bucket-a7044ab"

Resources:
    - 4 deleted

Duration: 7s

步驟二: 移除 Stack 設定

上面步驟只是把所有資源移除,但是你還是保留了所以 stack history 操作,請看

$ pulumi stack history
Version: 2
UpdateKind: destroy
Status: succeeded
Message: chore(pulumi): 設定 Pulumi Stack 環境變數
+0-4~0 0 Updated 1 minute ago took 8s
    exec.kind: cli
    git.author: Bo-Yi Wu
    git.author.email: xxxxxxxx@gmail.com
    git.committer: Bo-Yi Wu
    git.committer.email: xxxxxxxx@gmail.com
    git.dirty: true
    git.head: 9d9f8182abefb0e90656ca45065bc07a8a3431f4
    git.headName: refs/heads/main
    vcs.kind: github.com
    vcs.owner: go-training
    vcs.repo: infrastructure-as-code-workshop

Version: 1
UpdateKind: update
Status: succeeded
Message: chore(pulumi): 設定 Pulumi Stack 環境變數
+4-0~0 0 Updated 8 minutes ago took 18s
    exec.kind: cli
    git.author: Bo-Yi Wu
    git.author.email: xxxxxxxx@gmail.com
    git.committer: Bo-Yi Wu
    git.committer.email: xxxxxxxx@gmail.com
    git.dirty: true
    git.head: 437e94e130ee3d31eb80075dd237cc17d09255d1
    git.headName: refs/heads/main
    vcs.kind: github.com
    vcs.owner: go-training
    vcs.repo: infrastructure-as-code-workshop

要整個完整移除,請務必要執行底下指令

pulumi stack rm

最後的確認

$ pulumi stack rm
This will permanently remove the 'prod' stack!
Please confirm that this is what you'd like to do by typing ("prod"):

移除其他的 Stack

按照上面的步驟重新移除其他的 Stack,先使用底下指令列出還有哪些 Stack:

$ pulumi stack ls
NAME  LAST UPDATE     RESOURCE COUNT  URL
dev   24 minutes ago  5               https://app.pulumi.com/appleboy/demo/dev

選擇 Stack

pulumi stack select dev

接著重複上面一跟二步驟即可

心得

本篇跟上一篇教學剛好涵蓋了整個 Pulumi 的基本使用方式,如果你還在選擇要用 Terraform 還是 Pulumi,甚至 AWS 所推出的 CDK,很推薦你嘗試看看 Pulumi,未來會介紹更多 Pulumi 進階的使用方式,或者像是部署 Kubernetes .. 等,能使用自己喜歡的語言寫 Infra 是一件令人舒服的事情。