Quantcast
Channel: 小惡魔 – 電腦技術 – 工作筆記 – AppleBOY
Viewing all 325 articles
Browse latest View live

為什麼要學 GraphQL?

$
0
0

身為網站工程師,您不能不知道什麼是 GraphQL,這是一個前端跟後端溝通的 API Query 語法,大幅改善了前後端的合作模式,這篇會跟大家介紹為什麼麼要學 GraphQL,以及整理出三大 GraphQL 優勢,讓大家了解跟傳統 RESTful API 有什麼不同。當然不是叫開發者捨棄 RESTful API,而是根據專案的不同,來決定不同的技術 Stack。像是服務跟服務之前您說要用 GraphQL,肯定被打槍,而是要用更輕量的 RESTful API 或 gRPC。好了,底下來說明三點 GraphQL 的優勢。

教學影片

如果對於課程內容有興趣,可以參考底下課程。

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

1. 一次連線拿回前端所需資料

graphql

從上面的圖可以看到,要完成這頁面需要兩次 Request 才可以拿回全部資料,也就是需要兩次 connection。但是在 GraphQL 可以直接將 Query 語法寫在一起送到後端,後端全部處理完成後再一次回給前端

query {
  blog (
    id: 10
  ) {
    title
    post
    createdAt
    updatedAt
    user {
      id
      email
      username
    }
  }
  me {
    username
    email
  }
}

用上面的語法就可以直接拿到全部資料,大幅降低 connection 次數。

2. 根據不同畫面拿不同欄位資料

在 RESTful API 世界裡,後端會一次回傳所有資料,不會管前端需不需要這欄位,也就是前端沒有權力決定該拿什麼欄位,這樣會造成很多不必要的網路傳輸。一樣拿底下圖來說明

graphql

在右上角前端只需要使用者的個人頭像 + email 好了,但是後端 API 卻回給前端 10 個欄位,但是真正只需要兩個欄位,其他 8 個都是多餘的,這邊在 RESTful API 也可以根據不同畫面回不同的欄位資訊,卻造成後端很大的負擔。這時候用 GraphQL 解決了此問題,只要在 Query 語法內定義好要拿的資料即可

query {
  me {
    username
    email
    firstName
    lastName
  }
}

3. 即時 API 文件

大家應該都知道文件沒有一天是即時更新的,寫 RESTful API 要求後端也補上文件,簡直是難上加難,專案在趕的時候,誰還在管文件有沒有到最新,這邊就要推薦 GraphQL 了,因為只要程式碼一動,開發者透過 Client 工具就可以即時知道現在的 API 文件。

graphql

心得

以上三點是我認為 GraphQL 相較 RESTful API 的三大優勢,大家可以根據不同的專案屬性來決定是否要用 GraphQL,而只要是您在 Web 開發領域,就一定要知道 GraphQL 的這三大好處。


將 Postgres 資料轉換到 CSV 格式

$
0
0

postgres

時常用到 Postgres 轉換資料的功能,來即時協助 PM 了解目前使用者實際狀況,底下紀錄常用的指令。首先安裝 Postgres 環境,這邊其實就是用 Docker 方式來啟動一個全新的 Postgres DB。

  db:
    image: postgres:12
    restart: always
    volumes:
      - pg-data:/var/lib/postgresql/data
    logging:
      options:
        max-size: "100k"
        max-file: "3"
    environment:
      POSTGRES_USER: db
      POSTGRES_DB: db
      POSTGRES_PASSWORD: db

上面的 environment 參數可以自由調整,接著透過 docker-compose up -d 來啟動資料庫進行 App 串接。

登入 Postgres

假設 Docker 容器沒有 expos 5432 port 的話,基本上就是要登入到容器內操作,但是如果 expose 出來,那 Host 機器就是要裝 postgres client 套件,才有辦法登入,底下我們直接登入容器內,才不會有 clinet tool 跟 server 版本不一至問題。

docker-compose exec db /bin/bash

其中 db 就是在 docker-compose.yml 內的名稱,請自行更換。進入容器內後,再透過 psql 指令來登入 postgres 資料庫

psql -h 127.0.0.1 -d db -U db -W

將資料匯出成 CSV 格式

登入進 Postgres 後,透過簡單的指令就可以將單一資料表匯出成 csv 格式

\COPY some_table TO '/tmp/data.csv' DELIMITER ',' CSV HEADER;

如果只是要 table 內幾個欄位,可以改成底下

\COPY public.user(id, email) TO '/tmp/data.csv' DELIMITER ',' CSV HEADER;

或者是要透過 SQL 方式將資料整理過

\COPY (select id, email from public.user) TO '/data.csv' DELIMITER ',' CSV HEADER;

另外可以針對 UTC 時間轉台灣時間

select state, email, simulator, a.created_at \
  at time zone 'utc' at time zone 'Asia/Taipei' as created_at \
  from public.simulation a join public.user b on a.user_id = b.id  \
  order by a.created_at desc limit 5

時間轉換可以參考之前的一篇教學:『在 PostgreSQL 時區轉換及計算時間

拿出資料

上述資料會存放在容器內的 /data.csv,這時候可以透過 docker cp 指令將資料拿出來。透過 docker ps 找到 postgres 容器 ID,在執行下面指令

docker cp container_id:/tmp/data.csv .

之後就可以針對上面全部步驟,做自動化處理。

用 Postgres 計算員工上下班紀錄

$
0
0

postgres

這應該算是一個蠻簡單的情境,公司都需要去紀錄每位員工上下班紀錄,或者是紀錄每天刷卡補助餐點,在一定的時間內刷卡才會進行公司補助,非在約定的時間點刷卡則不補助,底下看看公司可能會想要的表格紀錄。在後台頁面會進行時間區域的選擇。

起始日期: 2020-06-01 結束日期: 2020-06-30 早上時間: 08:00 ~ 09:00 晚上時間: 18:00 ~ 19:00

建立表格來紀錄,其中 testPostgresSchema

CREATE TABLE "test"."workshift" (
    "id" int8 NOT NULL DEFAULT nextval('workshift_id_seq'::regclass),
    "company_id" int8,
    "employee_id" int8,
    "recorded_at" timestamp NOT NULL,
    "created_at" timestamp,
    "updated_at" timestamp,
    PRIMARY KEY ("id")
);

其中 recorded_at 是員工刷卡傳送上來的時間,本篇會介紹兩種情境的寫法,用來分別紀錄每個月各別員工刷卡次數,以及單月每天員工刷卡總次數。

員工刷卡次數

輸出的表格如下

Employee ID Name Breakfast Dinner
1234 Mr. Wang 1 0
4567 Mr. Lee 0 1

底下會用 postgres 內的 CASE WHEN 語法

SELECT
    employee_id,
    sum(
        CASE WHEN to_char(recorded_at, 'hh24:mi') >= '07:00'
            AND to_char(recorded_at, 'hh24:mi') <= '09:00' THEN
            1
        ELSE
            0
        END) AS breakfast_count,
    sum(
        CASE WHEN to_char(recorded_at, 'hh24:mi') >= '18:00'
            AND to_char(recorded_at, 'hh24:mi') <= '19:00' THEN
            1
        ELSE
            0
        END) AS dinner_count
FROM
    "public"."workshift"
WHERE
    workshift.company_id = 1
    AND recorded_at BETWEEN '2020-07-01T00:00:00Z'
    AND '2020-07-30T00:00:00Z'
GROUP BY
    employee_id
ORDER BY
    employee_id DESC
LIMIT 50

單月統計每天資料

另一種情境會是紀錄每天有多少刷卡紀錄,來計算補助金額。輸出的結果如下:

Day Breakfast Dinner
2020-07-01 1 4
2020-07-02 3 1

SQL 語法如下

SELECT
    to_char(recorded_at, 'YYYY-MM-DD') AS day_of_month,
    sum(
        CASE WHEN to_char(recorded_at, 'hh24:mi') >= '07:00'
            AND to_char(recorded_at, 'hh24:mi') <= '09:00' THEN
            1
        ELSE
            0
        END) AS breakfast_count,
    sum(
        CASE WHEN to_char(recorded_at, 'hh24:mi') >= '18:00'
            AND to_char(recorded_at, 'hh24:mi') <= '19:00' THEN
            1
        ELSE
            0
        END) AS dinner_count
FROM
    "public"."workshift"
WHERE
    workshift.company_id = 1
    AND recorded_at BETWEEN '2020-07-01T00:00:00Z'
    AND '2020-07-31T00:00:00Z'
GROUP BY
    day_of_month
ORDER BY
    day_of_month DESC
LIMIT 50

比較不一樣的地方是,透過 to_char 函式來取的每一天時間來計算所有員工刷卡次數來結算金額。搭配 GraphQL 語法搜尋會是

query {
  reports(
    category: Day
    timeRange: {
      startTime: "2020-07-01T00:00:00Z"
      endTime: "2020-07-10T00:00:00Z"
    }
    breakfastTime: {
      startTime: "07:00"
      endTime: "09:00"
    }
    lunchTime: {
      startTime: "12:00"
      endTime: "13:00"
    }
    dinnerTime: {
      startTime: "18:00"
      endTime: "19:00"
    }
  ) {
    totalCount
    nodes {
      date
      employee {
        uid
        name
      }
      breakfastCount
      lunchCount
      dinnerCount
    }
  }
}

由於是寫 RAW SQL,如果有使用 ORM 套件,要注意 SQL Injection 部分。歡迎大家提供更好的寫法或 DB 結構。

如何將前端網站打包成 Docker Image

$
0
0

cover

以現在開發網站流程,前後端分離已經不稀奇了。前端使用 React.jsVue.js,後端使用 Golang,是我現在擅長的合作模式。其實後端在開發上面不太需要將前端的開發流程放在自己的電腦上,也就是後端只需要專注開發後端,跟前端的溝通都會是透過 GraphQLSchema 當作討論。目前團隊各自維護專案的部署流程會是最好的方式,前端有兩種方式部署,一種是透過打包靜態檔案方式丟到遠端伺服器,另一種就是打包成 Docker Image,再連線到遠端伺服器更新,兩者都有人使用,本篇會教大家如何將前端網站打包成 Docker Image,用 Image 來部署會是最方便的。

教學影片

如果對於課程內容有興趣,可以參考底下課程。

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

前端打包

直接看 Reactjs 的 Deployment 章節,文件寫的非常清楚,透過簡單的指令就可以將前端網站編譯在 build 目錄內,開發者只要將 build 目錄打包丟上遠端伺服器即可。

npm ci
npm run build:staging

簡單兩個步驟就搞定了,接下來要將 build 目錄放進 Docker Image。

打包 Docker

首先前端的 Dockerfile 相當簡單,只要選 nginx 當做基底,再把相關的 html 檔案複製進去即可

FROM nginx:1.19

LABEL maintainer="Bo-Yi Wu <appleboy.tw@gmail.com>" \
  org.label-schema.name="web" \
  org.label-schema.vendor="Bo-Yi Wu" \
  org.label-schema.schema-version="1.0"

EXPOSE 8080

COPY ./config/nginx/nginx.conf /etc/nginx/conf.d/default.conf
COPY build /usr/share/nginx/html

CMD ["nginx", "-g", "daemon off;"]

可以在前端專案建立 docker 目錄,將上述內容存放成 Dockerfile。大家有無發現多了一行 nginx config 設定

COPY ./config/nginx/nginx.conf /etc/nginx/conf.d/default.conf

這目的是要將所有的 URL Routing 都直接轉給 React 或 Vue 去控管,不然只要重新整理網頁就會看到 404 not found

server {
  listen       80;
  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html =404;
  }
}

透過 try_files 可以解決掉 404 的問題。完成上述步驟後,就可以直接在電腦測試

docker build -t appleboy/app -f docker/Dockerfile.linux.amd64 .

串接部署

這邊就看團隊是用什麼工具部署,底下是 GitHub Action 部署方式,流程都是一樣的,只是用的工具不同,相信會一套,理論上另一種工具也要會

name: CI

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: build web
      uses: actions/setup-node@v1
      with:
        node-version: 12.x
    - run: npm ci
    - run: npm run build:staging

    - name: upload image to ECR
      uses: appleboy/docker-ecr-action@v0.0.2
      with:
        access_key: ${{ secrets.aws_access_key_id }}
        secret_key: ${{ secrets.aws_secret_access_key }}
        registry: ecr.ap-northeast-1.amazonaws.com
        cache_from: ecr.ap-northeast-1.amazonaws.com/frontend
        repo: frontend
        region: ap-northeast-1
        dockerfile: docker/Dockerfile.linux.amd64

    - name: pull and restart service
      uses: appleboy/ssh-action@master
      with:
        host: xxx.xxx.xxx.xxx
        username: deploy
        key: ${{ secrets.ssh_key }}
        port: 22
        proxy_host: xxx.xxx.xxx.xxx
        proxy_username: ubuntu
        proxy_port: 443
        proxy_key: ${{ secrets.proxy_ssh_key }}
        script: |
          cd /home/deploy/api/io && aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ecr.ap-northeast-1.amazonaws.com/frontend
          docker-compose pull
          docker-compose up -d web

步驟就是

  1. 編譯 build 目錄
  2. 打包 Docker Image 丟到 ECR
  3. 連線到遠端機器更新服務

心得

為了能夠讓網站可以跨不同的雲系統,統一使用 Docker 是最正確的方式,之後想要用 Kubernetes 或是用 docker-compose 都可以無痛轉移,這是趨勢,大家試著將服務都打包成 Docker 吧,方便自己也方便其他開發者。

在 Go 語言內管理 Concurrency 的三種方式

$
0
0

golang logo

相信大家踏入 Go 語言的世界,肯定是被強大的 Concurrency 所吸引,Go 語言用最簡單的關鍵字 go 就可以將任務丟到背景處理,但是開發者開怎麼有效率的控制 Concurrency,這是入門 Go 語言必學的項目,本篇會介紹三種方式來帶大家認識 Concurrency,而這三種方式分別對應到三個不同的名詞: WaitGroup, Channel, 及 Context,底下用簡單的範例帶大家了解。

教學影片

如果對於課程內容有興趣,可以參考底下課程。

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

WaitGroup

先來了解有什麼情境需要使用到 WaitGroup,假設您有兩台機器需要同時上傳最新的程式碼,兩台機器分別上傳完成後,才能執行最後的重啟步驟。就像是把一個 Job 同時拆成好幾份同時一起做,可以減少不少時間,但是最後需要等到全部做完,才能執行下一步,這時候就需要用到 WaitGroup 函式才能做到。底下看個簡單例子

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    i := 0
    wg.Add(3) //task count wait to do
    go func() {
        defer wg.Done() // finish task1
        fmt.Println("goroutine 1 done")
        i++
    }()
    go func() {
        defer wg.Done() // finish task2
        fmt.Println("goroutine 2 done")
        i++
    }()
    go func() {
        defer wg.Done() // finish task3
        fmt.Println("goroutine 3 done")
        i++
    }()
    wg.Wait() // wait for tasks to be done
    fmt.Println("all goroutine done")
    fmt.Println(i)
}

Channel

另外一種實際的案例就是,我們需要主動通知一個 Goroutine 進行停止的動作。舉例來說,當 App 啟動時,會在背景跑一些監控程式,而當整個 App 需要停止前,需要發個 Notification 給背景的監控程式,將其先停止,這時候就需要用到 Channel 來通知。底下來看個範例:

package main

import (
    "fmt"
    "time"
)

func main() {
    exit := make(chan bool)
    go func() {
        for {
            select {
            case <-exit:
                fmt.Println("Exit")
                return
            case <-time.After(2 * time.Second):
                fmt.Println("Monitoring")
            }
        }
    }()
    time.Sleep(5 * time.Second)
    fmt.Println("Notify Exit")
    exit <- true //keep main goroutine alive
    time.Sleep(5 * time.Second)
}

上面例子可以發現,背景用了一個 Goutine 及一個 Channel 來控制。可以想像當背景有無數個 Goroutine 的時候,我們就需要宣告多個 Channel 才能進行控制,也許 Goroutine 內又會產生 Goroutine,開發者這時候就會發現已經無法單純使用 Channel 來控制多個 Goroutine 了。這時候解決方式會是透過 context

Context

大家可以想像,今天有一個背景任務 A,A 任務又產生了 B 任務,B 任務又產生了 C 任務,也就是可以按照此模式一直產生下去,假設中途我們需要停止 A 任務,而 A 又必須告訴 B 及 C 要一起停止,這時候透過 context 方式是最快的了。

package main

import (
    "context"
    "fmt"
    "time"
)

func foo(ctx context.Context, name string) {
    go bar(ctx, name) // A calls B
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name, "A Exit")
            return
        case <-time.After(1 * time.Second):
            fmt.Println(name, "A do something")
        }
    }
}

func bar(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name, "B Exit")
            return
        case <-time.After(2 * time.Second):
            fmt.Println(name, "B do something")
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go foo(ctx, "FooBar")
    fmt.Println("client release connection, need to notify A, B exit")
    time.Sleep(5 * time.Second)
    cancel() //mock client exit, and pass the signal, ctx.Done() gets the signal  time.Sleep(3 * time.Second)
    time.Sleep(3 * time.Second)
}

大家可以把 context 想成是一個 controller,可以隨時控制不確定個數的 Goroutine,由上往下,只要宣告 context.WithCancel 後,再任意時間點都可以透過 cancel() 來停止整個背景服務。這邊實在案例會用在當 App 需要重新 restart 時,要先通知全部 goroutine 停止,正常停止後,才會重新啟動 App。

總結

根據不同的情境跟狀況來選擇不同的函式,底下做個總結

  • WaitGroup: 需要將單一個 Job 拆成多個子任務,等到全部完成後,才能進行下一步,這時候用 WaitGroup 最適合了
  • Channel+select: Channel 只能用在比較單純的 Goroutine 狀況下,如果要管理多個 Goroutine,建議還是走 context 會比較適合
  • Context: 如果你想一次控制全部的 Goroutine,相信用 context 會是最適合不過的,這也是現在 Go 用最兇的地方,當然 context 不只有這特性,詳細可以參考『用 10 分鐘了解 Go 語言 context package 使用場景及介紹

用 Go 語言實戰 Limit Concurrency 方法

$
0
0

golang logo

最近看到一篇文章討論的非常熱烈,就是『concurrency is still not easy』這篇文章甚至上了 Hack News,大家有興趣可以點進去看看,而本篇會用一個實際案例介紹為什麼作者會說寫 Concurrency 不是這麼容易。大家都知道在 Go 語言內,要寫 Concurrency 只要透過一個關鍵字 go 就可以輕易寫出,而多個 Goroutine 要溝通就是需要透過 Channel 方式,而網路上有一堆 Concurrency Pattern 提供給各位開發者,但是官方 Go 的標準庫內並沒有包含這些 Pattern,所以實作之後,說實在很難看出問題。文章內提到 gops 實作 Limit Concurrency 遇到系統整個 hang 住的問題?什麼是 Limit Concurrency,就是當系統有多個工作需要同時執行,但是需要限制 Concurrency 數量,避免整個資源都被吃光。底下來介紹文章內遇到的問題。

教學影片

如果對於課程內容有興趣,可以參考底下課程。

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

Limit Concurrency 問題

Gops 是一套 CLI 工具,可以列出系統上正在用 Go 跑的全部 Process。要怎樣複製問題呢?很簡單,先安裝好 gops 指令後,先執行 gops 會看到底下結果

$ gops
98   1    com.docker.vmnetd  go1.13.14 com.docker.vmnetd
2319 2275 gopls              go1.15    /Users/appleboy/go/bin/gopls
4083 452  gops               go1.15    /Users/appleboy/go/bin/gops

接著用底下程式碼繼續將 process 塞滿到 10 個,這時候在執行 gops 會發現系統完全不顯示了,也沒辦法結束。

package main

import (
    "fmt"
    "time"
)

func main() {

    timer1 := time.NewTicker(1 * time.Second)

    for v := range timer1.C {
        fmt.Println(v)
    }
}

這問題發生在 8/5 有網友提出了 PR 去限制 Concurrency,這寫法造成了上述出現的問題,導致 CLI 整個無法繼續運作,需要用 ctrl + c 才可以結束執行。底下是修改過後的程式碼:

// FindAll returns all the Go processes currently running on this host.
func FindAll() []P {
    const concurrencyProcesses = 10 // limit the maximum number of concurrent reading process tasks
    pss, err := ps.Processes()
    if err != nil {
        return nil
    }

    var wg sync.WaitGroup
    wg.Add(len(pss))
    found := make(chan P)
    limitCh := make(chan struct{}, concurrencyProcesses)

    for _, pr := range pss {
        limitCh <- struct{}{}
        pr := pr
        go func() {
            defer func() { <-limitCh }()
            defer wg.Done()

            path, version, agent, ok, err := isGo(pr)
            if err != nil {
                // TODO(jbd): Return a list of errors.
            }
            if !ok {
                return
            }
            found <- P{
                PID:          pr.Pid(),
                PPID:         pr.PPid(),
                Exec:         pr.Executable(),
                Path:         path,
                BuildVersion: version,
                Agent:        agent,
            }
        }()
    }
    go func() {
        wg.Wait()
        close(found)
    }()
    var results []P
    for p := range found {
        results = append(results, p)
    }
    return results
}

我將上面的例子簡化寫成單一 main 函式來執行,效果是一樣的:

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func main() {
    const concurrencyProcesses = 10 // limit the maximum number of concurrent reading process tasks
    const jobCount = 100

    var wg sync.WaitGroup
    wg.Add(jobCount)
    found := make(chan int)
    limitCh := make(chan struct{}, concurrencyProcesses)

    for i := 0; i < jobCount; i++ {
        limitCh <- struct{}{}
        go func(val int) {
            defer func() {
                wg.Done()
                <-limitCh
            }()
            waitTime := rand.Int31n(1000)
            fmt.Println("job:", val, "wait time:", waitTime, "millisecond")
            time.Sleep(time.Duration(waitTime) * time.Millisecond)
            found <- val
        }(i)
    }
    go func() {
        wg.Wait()
        close(found)
    }()
    var results []int
    for p := range found {
        fmt.Println("Finished job:", p)
        results = append(results, p)
    }

    fmt.Println("result:", results)
}

我把 ps.Processes() 的資料,換成 Job 數量來代表,來解釋為什麼麼這段程式碼造成了系統直接 hang 住不動。重點原因在 for 迴圈內的 limitCh <- struct{}{},先看到前面有設定了背景一次只能跑 10 個 Concurrency Processes

    for i := 0; i < jobCount; i++ {
        limitCh <- struct{}{}
        go func(val int) {
            ....
        }(i)
    }

這是一個標準的 Limit Concurrency 問題,在讀取第一個 Job 後,先將空 struct 丟入 limitCh 通道,這時候 limitCh 就是剩下 9 個可以繼續處理,接著持續一樣的動作,但是到第 11 個 Job 需要處理時,就會直接停在 limitCh <- struct{}{},在 for 迴圈後面的程式碼完全沒辦法執行,造成整個系統 deadlock,由此可知道,如果 Process 數量小於 10 的話,幾乎看不出來有任何問題,系統都可以正常運作 (大家可以把範例的 Job Count 換成 10)。下面會介紹兩種方式繞過此問題,大家可以參考看看

將 limitCh <- struct{}{} 丟到背景處理

相信很多開發者可能會想到,既然卡在 limitCh <- struct{}{},那就將此段程式碼也一樣丟到 goroutine 內處理就可以了。

    found := make(chan int)
    limitCh := make(chan struct{}, concurrencyProcesses)

    for i := 0; i < jobCount; i++ {
        go func() {
            limitCh <- struct{}{}
        }()
        go func(val int) {
            defer func() {
                <-limitCh
                wg.Done()
            }()
            waitTime := rand.Int31n(1000)
            fmt.Println("job:", val, "wait time:", waitTime, "millisecond")
            time.Sleep(time.Duration(waitTime) * time.Millisecond)
            found <- val
        }(i)
    }

很高興可以看到這方式解決掉系統 Hang 住的問題。但是你有沒有發現,程式碼沒辦法限制 Concurrency Processes,而是 100 個 Job 同時處理到結束。雖然這方式可以解決問題,但是回到問題的初衷,我們就是要寫 Limit Concurrency 啊。

使用 worker queue 寫法

這寫法算是蠻常見的,既然要限制背景能同時處理的數量,那相對的就是建立特定數量的 Worker,每個 Worker 內在讀取 Channle 內的資料出來。第一步驟建立 queue 通道,並將所有的內容都丟進 queue 內

    found := make(chan int)
    queue := make(chan int)

    go func(queue chan<- int) {
        for i := 0; i < jobCount; i++ {
            queue <- i
        }
        close(queue)
    }(queue)

這邊一樣是透過 goroutine 方式丟到背景,避免 block 整個 main 程式。接著建立特定的 Worker 數量來消化全部的 Job

    for i := 0; i < concurrencyProcesses; i++ {
        go func(queue <-chan int, found chan<- int) {
            for val := range queue {
                defer wg.Done()
                waitTime := rand.Int31n(1000)
                fmt.Println("job:", val, "wait time:", waitTime, "millisecond")
                time.Sleep(time.Duration(waitTime) * time.Millisecond)
                found <- val
            }
        }(queue, found)
    }

可以看到這邊的 for 迴圈就是以 concurrencyProcesses 為主了,裡面再用 goroutine 方式來讀取 Channel,直到全部的 Channel 都讀取完畢,整個 goroutine 就會結束。除了這解決方式之外,還是有其他方法可以實作,這邊就交由大家去發揮了。

心得

這篇文章為什麼這麼火紅的原因,我猜也因為是該 Repo 是 Google 官方自行開發,但是所有的 PR 都需要經過 Googler 嚴格 Review 過後才可以 Merge 進 Master 分支,但是像這種小細節,如果沒有真的實際測試,真的還蠻難發現問題。

用 Go 語言打造多台機器 Scale 架構

$
0
0

Go 語言 Select Multiple Channel 注意事項

$
0
0

golang logo

相信大家都知道 Select 可以用來處理多個 Channel,但是大家有沒有想過一個情境,如果是 for 搭配 select 時,肯定會用一個 Timer 或 context 來處理 Timeout 或手動 Cancel,假設如果跟其他 Channel 同時到達時,官方說法是 Select 會隨機選擇一個狀況來執行,如果並非選到我們所要的 case 那就會造成情境或流程上的錯誤,而本影片就是講解該如何解決此問題,請大家務必詳細了解業務的需求,來決定程式碼架構該如何寫。

影片介紹

00:00 簡介程式碼
01:03 NewTicker 跟 Channel 同時執行狀況
03:29 解決方案一以及限制 (多個 Channel 就不適用了)
05:06 多個一樣的 case 提供優先權
07:28 用 Kubernetes Client 案例來講解

其中 Kubernetes Client 範例來自這邊

如果對於課程內容有興趣,可以參考底下課程。

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

多個 Channle 同時讀寫

底下直接用範例解釋 (此範例來源自『go里面select-case和time.Ticker的使用注意事项』),也可以參考整個 FB 討論串

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 1024)
    go func(ch chan int) {
        for {
            if v, ok := <-ch; ok {
                fmt.Printf("val:%d\n", v)
            }
        }
    }(ch)

    tick := time.NewTicker(1 * time.Second)
    for i := 0; i < 30; i++ {
        select {
        // how to make sure all number in ch channel?
        case ch <- i:
        case <-tick.C:
            fmt.Printf("%d: case <-tick.C\n", i)
        }

        time.Sleep(200 * time.Millisecond)
    }
    close(ch)
    tick.Stop()
}

大家可以看到上面開始有個 goroutine 用來接收 ch channel 的內容,接著看看下面的 for 迴圈內有個 select 用來接受或寫入 Channel,但是這時候會發生一個問題,當 i = 5 時,是有機率會兩個 case 同時發生,這時候按照 Go 語言官方範例提到內容

A select blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.

是會隨機的方式選取一個,所以會發現有機率會少接收到 ch 值,所以底下有幾種方式可以解決此問題。也就是要確保 ch 可以接收到 0 ~ 29 數字。其中第一個做法就是將 ch <- i 加入到 tick.C 內

    for i := 0; i < 30; i++ {
        select {
        case ch <- i:
        case <-tick.C:
            fmt.Printf("%d: case <-tick.C\n", i)
            ch <- i
        }

        time.Sleep(200 * time.Millisecond)
    }

第二種作法就是透過 select default 方式不要讓程式 blocking

    for i := 0; i < 30; i++ {
        ch <- i
        select {
        case <-tick.C:
            fmt.Printf("%d: case <-tick.C\n", i)
        default:
        }

        time.Sleep(200 * time.Millisecond)
    }

上述這兩種方式都可以,只是真的要依照團隊業務邏輯來決定怎樣修改才是正確的。這概念已經有在去年 (2019) 的 Blog 講過,如果要再多了解 Select 語法,可以參考之前寫的文章『Go 語言使用 Select 四大用法


用 GitHub Actions, Drone CI 或 GitLab CI 部署 AWS Lambda

$
0
0

Screen Shot 2018-10-24 at 9.37.49 AM

最近剛好把 drone-lambda 新增了一些新的功能,也就是可以透過 CI/CD 的方式來更新 AWS Lambda 基本設定,像是 Memory Size, Handler, Timeout, Runtime 或 Role 等 …,趁這機會寫篇教學紀錄如何透過 GitHub Actions, Drone CIGitLab CI 部署 AWS Lambda。這三套部署方式都是透過 drone-lambda 包好的 Image 來進行。底下的程式碼都可以在這邊找到

GitHub Actions

此篇直接用 Go 語言的範例來進行線上部署,GitHub Actions 直接使用 lambda-action

name: deploy to lambda
on: [push]
jobs:

  deploy_zip:
    name: deploy lambda function from zip
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go-version: [1.15.x]
    steps:
      - name: checkout source code
        uses: actions/checkout@v1
      - name: Install Go
        uses: actions/setup-go@v1
        with:
          go-version: ${{ matrix.go-version }}
      - name: Build binary
        run: |
          cd example && GOOS=linux go build -v -a -o main main.go && zip deployment.zip main
      - name: deploy zip
        uses: appleboy/lambda-action@v0.0.8
        with:
          aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws_region: ${{ secrets.AWS_REGION }}
          function_name: gorush
          zip_file: example/deployment.zip
          debug: true

首先第一個步驟就是編譯 Binary 接著打包成 zip 檔案後,才可以進行部署,接著在 Plugin 寫上 function name 跟 zip 檔案路徑就可以直接更新到 AWS Lambda 了。

Drone CI

---
kind: pipeline
name: testing

platform:
  os: linux
  arch: amd64

steps:
- name: build
  image: golang:1.15
  commands:
  - apt-get update && apt-get -y install zip
  - cd example && GOOS=linux go build -v -a -o main main.go && zip deployment.zip main

- name: deploy-lambda
  image: appleboy/drone-lambda
  settings:
    pull: true
    aws_access_key_id:
      from_secret: AWS_ACCESS_KEY_ID
    aws_secret_access_key:
      from_secret: AWS_SECRET_ACCESS_KEY
    aws_region:
      from_secret: AWS_REGION
    function_name: gorush
    zip_file: example/deployment.zip
    debug: true

寫法跟 GitHub Actions 非常類似,因為在同一個 Piepline,所以可以在第一個步驟產生出來的 zip 檔案,也可以在第二個步驟部署。

GitLab CI

其實 GitLab CI 已經有寫一篇完整的教學,裡面用的是 Server less 框架來部署程式碼,所以開發者還需要看一下怎麼使用此框架,相對來說比較難上手,那底下來介紹用 drone-lambda 方式來進行部署。

variables:
  ARTIFACTS_DIR: artifacts
  GIT_DEPTH: 1

before_script:
  - mkdir -p ${CI_PROJECT_DIR}/${ARTIFACTS_DIR}

stages:
  - build
  - deploy

build:
  image: golang:1.15
  stage: build
  script:
    - apt-get update && apt-get -y install zip
    - cd example && GOOS=linux go build -v -a -o main main.go && zip deployment.zip main
    - mv deployment.zip ${CI_PROJECT_DIR}/${ARTIFACTS_DIR}/
  artifacts:
    paths:
      - ${ARTIFACTS_DIR}

deploy:
  image: appleboy/drone-lambda
  variables:
    FUNCTION_NAME: 'gorush'
    DEBUG: 'true'
    ZIP_FILE: '${CI_PROJECT_DIR}/${ARTIFACTS_DIR}/deployment.zip'
    GIT_STRATEGY: none
  stage: deploy
  artifacts:
    paths:
      - ${ARTIFACTS_DIR}
  script:
    - /bin/drone-lambda

在 GitLab CI 比較不同的是,每個步驟都是需要重新 git clone 整個專案,步驟結束,就會把整個容器砍掉,包含整個 Project Data,這邊就需要透過 artifacts 來共享資料。詳細資料可以參考艦長寫的這篇『CI/CD Pipeline 之 stage: build

心得

目前使用起來 GitHub Action 跟 Drone CI 行為是一致的,反倒是使用 GitLab CI 之後,需要多了解 artifacts 這塊,才可以完成整個串接。

善用 Go 語言效能測試工具來提升執行效率

$
0
0

golang logo

在 AI 訓練模型前,都需要經過大量的資料處理,而資料處理的速度在整個流程內扮演很重要的角色,寫出高效能的 Parser 能降低整體處理時間,那如何評估程式效能如何,以及如何快速找到效能瓶頸?本議程會帶大家了解 Go 語言內建的效能測試工具,透過 Benchmark 來找出程式效能瓶頸的地方,快速改善及優化,讓整個系統流程更順暢。也會順道分享 Go 在字串處理優化的一些小技巧。聽過此議程相信您對 Go 語言會有更深入的了解,如果你想寫出有效率的程式碼,本議程一定不能錯過。

投影片

這次很高興能到高雄 mopcon 給一場演講『善用 Go 語言效能測試工具來提升執行效率』。底下是這場投影片,等到年底就會有影片釋出。

底下紀錄會後一些朋友的意見跟問題?

為什麼要從 Python 到 Golang?

第一版 Python 由同事進行開發,這個版本也在公司內部運作了很久,也很少改版,而這次遇到效能上的問題,加上要搭配 AI,故我先拿 Golang 進行第一次的改版,方式還是使用 Regex,把整個邏輯換掉,也優化不少 Regex,效能提升不少。而至於為什麼要用 Go 而不是用 Python 原因是當下對於 Go 比較熟悉,也想嘗試看看用 Go 能提升多少效能,並非 Python 不好,考慮到團隊目前的技能樹,加上在自家 IT 環境內,用 Go 可以編譯出單一執行檔給同仁使用,相對 Python 來說是方便許多。在公司內部有些特定的環境是完全沒有網路了的,這時候用 Go 搭配 vendor 就可以無痛在該環境編譯,這點是 Go 非常強大的地方。

為什麼會想重寫 Parser?

後來用 Go 改寫的 Regex 版本,從原本的 9xx 秒降到 7 秒多,已經提升了不少,接下來要再往下繼續調整,估計也已經沒多少空間了,加上此版本對於更大的檔案量,1 GB 以上資料量,還是需要用掉不少系統資源,故我花了一週下班時間,重新改寫 Parser,最主要要驗證從 7 秒多可以降到幾秒呢?後來事實證明可以從 7 秒多降到 1 秒左右,整體來說提升了不少,也讓其他同仁在使用 Parser 的時候,從原本需要 400 台機器,降到不到 5 台。省下不少公司的資源,這些資源又可以去處理更多事情了。

用 Docker 每天自動化備份 MySQL, Postgres 或 MongoDB 並上傳到 AWS S3

$
0
0

由於備份 PostgreSQL 的指令 pg_dump 需要限定特定版本才可以備份,故自己製作用 Docker 容器方式來備份,此工具支援 MySQL, PostgreSQL 跟 MongoDB,只要一個 docker-compose yaml 檔案就可以進行線上的備份,並且上傳到 AWS S3,另外也可以設定每天晚上固定時間點進行時間備份,也就是平常所設定的 cron job。沒使用 AWS RDS,或自行管理機房的朋友們,就可以透過這小工具,進行每天半夜線上備份,避免資料被誤砍。底下教學程式碼都可以在這邊找到

影片教學

  • 00:00 備份資料庫工具介紹
  • 00:40 為什麼要寫這工具?
  • 01:55 架設 Minio S3 Storage 服務
  • 02:56 Minio UI 介面介紹 (建立 bucket)
  • 03:32 架設 PostgreSQL 12 服務
  • 04:00 備份方式參數介紹
  • 07:06 執行備份程式
  • 09:18 設定 Minio 內的 bucket life cycle (只保留七天內資料)
  • 10:25 設定每天自動備份並上傳到 Minio S3

如果對於課程內容有興趣,可以參考底下課程。

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

使用方式

本教學使用 Minio 來代替 AWS S3,底下用 docker-compose 來架設 Minio 及 PostgreSQL 12 版本

services:
  minio:
    image: minio/minio:edge
    restart: always
    volumes:
      - data1-1:/data1
    ports:
      - 9000:9000
    environment:
      MINIO_ACCESS_KEY: 1234567890
      MINIO_SECRET_KEY: 1234567890
    command: server /data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3

  postgres:
    image: postgres:12
    restart: always
    volumes:
      - pg-data:/var/lib/postgresql/data
    logging:
      options:
        max-size: "100k"
        max-file: "3"
    environment:
      POSTGRES_USER: db
      POSTGRES_DB: db
      POSTGRES_PASSWORD: db

接著挑選特定資料庫版本的 Docker Image

  backup_postgres:
    image: appleboy/docker-backup-database:postgres-12
    logging:
      options:
        max-size: "100k"
        max-file: "3"
    environment:
      STORAGE_DRIVER: s3
      STORAGE_ENDPOINT: minio:9000
      STORAGE_BUCKET: test
      STORAGE_REGION: ap-northeast-1
      STORAGE_PATH: backup_postgres
      STORAGE_SSL: "false"
      STORAGE_INSECURE_SKIP_VERIFY: "false"
      ACCESS_KEY_ID: 1234567890
      SECRET_ACCESS_KEY: 1234567890

      DATABASE_DRIVER: postgres
      DATABASE_HOST: postgres:5432
      DATABASE_USERNAME: db
      DATABASE_PASSWORD: db
      DATABASE_NAME: db
      DATABASE_OPTS:

其中 STORAGE_BUCKET 是 AWS S3 的 bucket 名稱,還有需要設定 STORAGE_PATH 這樣待會可以設定 bucket lifecycle,可以設定幾天後刪除舊的資料,接著設定 Minio S3 的 bucket lifecycle:

$ mc ilm import minio/test <<EOF
{
    "Rules": [
        {
            "Expiration": {
                "Days": 7
            },
            "ID": "backup_postgres",
            "Filter": {
                "Prefix": "backup_postgres/"
            },
            "Status": "Enabled"
        }
    ]
}
EOF

上面設定是一次性的備份,也就是手動使用 docker-compose up backup_postgres 就可以進行一次備份,當然可以固定每天晚上時間來備份

  backup_mysql:
    image: appleboy/docker-backup-database:mysql-8
    logging:
      options:
        max-size: "100k"
        max-file: "3"
    environment:
      STORAGE_DRIVER: s3
      STORAGE_ENDPOINT: minio:9000
      STORAGE_BUCKET: test
      STORAGE_REGION: ap-northeast-1
      STORAGE_PATH: backup_mysql
      STORAGE_SSL: "false"
      STORAGE_INSECURE_SKIP_VERIFY: "false"
      ACCESS_KEY_ID: 1234567890
      SECRET_ACCESS_KEY: 1234567890

      DATABASE_DRIVER: mysql
      DATABASE_HOST: mysql:3306
      DATABASE_USERNAME: root
      DATABASE_PASSWORD: db
      DATABASE_NAME: db
      DATABASE_OPTS:

      TIME_SCHEDULE: "@daily"
      TIME_LOCATION: Asia/Taipei

TIME_LOCATION 可以設定台灣時區,不然預設會是 UTC+0 時間。更多詳細的設定可以參考文件

心得

由於本身團隊可能沒有使用 AWS RDS 服務,故自己都需要寫程式自行備份,但是同事用的 DB 都不同,所以乾脆包成 Docker 容器方式讓同事可以方便設定。之後有新專案就可以直接套用,相當容易。

Go 1.16 推出 Embedding Files

$
0
0

golang logo

Go 語言官方維護團隊 rsc 之前在 GitHub Issue 上面提出要在 go command line 直接支援 Embedding Files,沒想到過沒幾個月,就直接實現出來了,並且預計在 2021 的 go 1.16 版本直接支援 embed 套件。有了這個功能,就可以將靜態檔案或專案設定檔直接包起來,這樣部署就更方便了。底下來看看官方怎麼使用。

影片教學

如果對於課程內容有興趣,可以參考底下課程。

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

embed package

直接看官方給的例子:

package main

import "embed"

func main() {
    //go:embed hello.txt
    var s string
    print(s)

    //go:embed hello.txt
    var b []byte
    print(string(b))

    //go:embed hello.txt
    var f embed.FS
    data, _ := f.ReadFile("hello.txt")
    print(string(data))

}

可以看到關鍵字: go:embed,透過註解就可以將靜態檔案直接使用在開發上面,另外也可以引用多個檔案或多個目錄:

package server

import "embed"

// content holds our static web server content.
//go:embed image/* template/*
//go:embed html/index.html
var content embed.FS

可以看到 go:embed 支援多個目錄,單一檔案或多個檔案都可以,假如沒有用到 embed.FS,請在 import 時加上 _,範例如下:

package main

import _ "embed"

func main() {
    //go:embed hello.txt
    var s string
    print(s)

    //go:embed hello.txt
    var b []byte
    print(string(b))
}

有了這個 Package 後,再也不需要第三方套件 Resource Embedding 了,底下來看看如何將 embed 套件整合進 Gin

整合 Gin Framework

先假設 Gin 需要包含靜態圖片及 Template,底下是目錄結構:

├── assets
│   ├── favicon.ico
│   └── images
│       └── example.png
├── go.mod
├── go.sum
├── main.go
└── templates
    ├── foo
    │   └── bar.tmpl
    └── index.tmpl

該如何將 Template 跟 assets 目錄直接打包進 Go 呢?直接看 main.go

package main

import (
    "embed"
    "html/template"
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    //go:embed assets/* templates/*
    var f embed.FS

    router := gin.Default()
    templ := template.Must(template.New("").ParseFS(f, "templates/*.tmpl", "templates/foo/*.tmpl"))
    router.SetHTMLTemplate(templ)

    // example: /public/assets/images/example.png
    router.StaticFS("/public", http.FS(f))

    router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.tmpl", gin.H{
            "title": "Main website",
        })
    })

    router.GET("/foo", func(c *gin.Context) {
        c.HTML(http.StatusOK, "bar.tmpl", gin.H{
            "title": "Foo website",
        })
    })

    router.GET("favicon.ico", func(c *gin.Context) {
        file, _ := f.ReadFile("assets/favicon.ico")
        c.Data(
            http.StatusOK,
            "image/x-icon",
            file,
        )
    })

    router.Run(":8080")
}

開發者可以很簡單用兩行就將靜態檔案直接包進來:

//go:embed assets/* templates/*
var f embed.FS

靜態檔案的 route 可以直接透過底下設定:

// example: /public/assets/images/example.png
router.StaticFS("/public", http.FS(f))

也可以透過 ReadFile 讀取單一檔案:

router.GET("favicon.ico", func(c *gin.Context) {
    file, _ := f.ReadFile("assets/favicon.ico")
    c.Data(
        http.StatusOK,
        "image/x-icon",
        file,
    )
})

程式範例可以直接在這邊找到

心得

Go 團隊真的蠻用心的,會比一些常用核心的功能納入官方維護,以保持後續的更新,有了這項功能,在 Go 的部署流程,直接可以略過靜態檔案加入 Docker 內了。未來專案有些保密檔案也可以透過此方式直接在 CI 流程內先換掉,再進行 go build 了。

用 Go 語言撰寫簡單的 Command Line 工具

$
0
0

golang logo

之前介紹了一個開源工具『用 Docker 每天自動化備份 MySQL, Postgres 或 MongoDB 並上傳到 AWS S3』,讓開發者可以快速透過 Docker 方式來備份資料庫,而本篇要介紹我如何用 Go 語言來撰寫 CLI 並且整合 Docker 來實現備份。此工具都是透過各大資料庫官方提供的 CLI 指令 (pg_dump, mysqldump … 等),故大家不用猜想是什麼神奇的技巧。底下來依序介紹整個目錄結構,及我如何實現。

影片介紹

  • 00:00 backup 資料庫工具簡介
  • 01:29 專案目錄分層介紹
  • 03:16 介紹 pkg/storage interface
  • 04:35 介紹 pkg/dbdump interface
  • 06:15 為什麼要用 urfave/cli v2 版本
  • 07:25 用 Drone 做自動化上傳多種不同 Docker Image
  • 08:40 go build + demo 使用 CLI
  • 09:33 介紹 cmd 目錄底下產生多個 CLI 目錄

如果對於課程內容有興趣,可以參考底下課程。

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

目錄結構

底下教學就拿 docker-backup-database 為範例,我從目錄結構開始講,剛入門的朋友肯定對於 Go 語言在目錄這塊定義的不是很清楚,但是其實這跟個人或團隊喜好也有點關係,底下看看目錄結構

├── LICENSE
├── Makefile
├── README.md
├── cmd
│   └── backup
│       ├── config.go
│       └── main.go
├── docker
│   ├── Dockerfile.mongo.3.6
│   ├── Dockerfile.mongo.4
│   ├── Dockerfile.mongo.4.2
│   ├── Dockerfile.mongo.4.4
│   ├── Dockerfile.mysql.5.6
│   ├── Dockerfile.mysql.5.7
│   ├── Dockerfile.mysql.8
│   ├── Dockerfile.postgres.10
│   ├── Dockerfile.postgres.11
│   ├── Dockerfile.postgres.12
│   ├── Dockerfile.postgres.13
│   └── Dockerfile.postgres.9
├── docker-compose.yml
├── go.mod
├── go.sum
└── pkg
    ├── config
    │   └── config.go
    ├── dbdump
    │   ├── dbdmp.go
    │   ├── mongo
    │   │   └── mongo.go
    │   ├── mysql
    │   │   └── mysql.go
    │   └── postgres
    │       └── postgres.go
    ├── helper
    │   └── cmd.go
    └── storage
        ├── core
        │   └── core.go
        ├── disk
        │   ├── disk.go
        │   └── disk_test.go
        ├── minio
        │   └── minio.go
        └── storage.go

其實目錄結構相當清楚,根目錄底下只會放跟部署或教學相關的資訊,像是 .drone.yml 用來做 CI/CD 幫忙自動化建立 Docker Image 並且上傳到 Docker Hub。而 docker-compose.yml 則是一份簡單的教學範例,讓想使用此工具的開發者可以快速建置出 Minio 或 Postgres 環境。而最後一個 Makefile 存放很多相關的指令,我本身不太喜歡打很長的指令,直接把用到的指令全都寫在 Makefile 內,這樣在寫 CI/CD 或同事及開發者想快速使用時,幾個指令就可以搞定了。盡量不要把指令在 CI/CD 流程中複雜化,這樣不好維護。而 docker 目錄會是此專案用到的所有 Dockerfile,就放在一起了,透過 Drone 直接平行化編譯 Image 並上傳。

cmd 目錄

├── cmd
│   └── backup
│       ├── config.go
│       └── main.go

開發者都可以發現在 GitHub 上面的 Go 開源專案,幾乎都會有一個 cmd 目錄,因為不想把 main.go 放在跟目錄下,然後又要命名一個還不錯的名稱,就需要建立在 cmd 目錄底下,這樣大家透過 go get 才可以正確下載到您要的命令,通常一個專案也許會有多個 CLI 工具,那就會在 cmd 底下建立多個目錄,每個目錄都會有 main.go 檔案,以現在這個範例為例,只會有一個 CLI,大家可以透過底下指令來下載 CLI。

go get github.com/appleboy/docker-backup-database/cmd/backup

在 CLI 套件選擇,我則是選擇了 urfave/cli,原因很簡單,此工具未來會應用在 CI/CD 流程上,所以會希望可以直接支援 GitHub Actions, Drone CI 或 GitLab CI,而 urfave/cli 讓開發者可以自行定義 ENV,原因是 GitHub Actions 只支援 INPUT_ 而 Drone CI 只支援 PLUGIN_,故 urfave/cli 讓我自由定義,只有這個原因才選這套件。

pkg 目錄

我會把專案用到的其他功能都一併建立在這邊,由這個目錄底下在做分類

└── pkg
    ├── config
    │   └── config.go
    ├── dbdump
    │   ├── dbdmp.go
    │   ├── mongo
    │   │   └── mongo.go
    │   ├── mysql
    │   │   └── mysql.go
    │   └── postgres
    │       └── postgres.go
    ├── helper
    │   └── cmd.go
    └── storage
        ├── core
        │   └── core.go
        ├── disk
        │   ├── disk.go
        │   └── disk_test.go
        ├── minio
        │   └── minio.go
        └── storage.go

可以看到我分了幾個目錄,config 用來存放 CLI 用到的環境變數,而 storage 用來定義上傳雲服務 AWS S3 或 Minio 的 Interface:

// Storage for s3 and disk
type Storage interface {
    // CreateBucket for create new folder
    CreateBucket(string, string) error
    // UploadFile for upload single file
    UploadFile(string, string, []byte, io.Reader) error
    // DeleteFile for delete single file
    DeleteFile(string, string) error
    // FilePath for store path + file name
    FilePath(string, string) string
    // GetFile for storage host + bucket + filename
    GetFileURL(string, string) string
    // DownloadFile downloads and saves the object as a file in the local filesystem.
    DownloadFile(string, string, string) error
    // BucketExist check object exist. bucket + filename
    BucketExists(string) (bool, error)
    // FileExist check object exist. bucket + filename
    FileExist(string, string) bool
    // GetContent for storage bucket + filename
    GetContent(string, string) ([]byte, error)
    // Copy Create or replace an object through server-side copying of an existing object.
    CopyFile(string, string, string, string) error
    // Client get storage client
    Client() interface{}
    // SignedURL get signed URL
    SignedURL(string, string, *core.SignedURLOptions) (string, error)
}

定義完成後,未來有其他的 Storage 需要支援,就可以直接在 storage 目錄底下建立新的目錄,接著就可以直接開發了。另外 dbdump 也是同樣原理,現在支援三種 Database 而已,未來可以接受擴充到任何資料庫型態。

// Backup database interface
type Backup interface {
    // Exec backup database
    Exec() error
}

可以看到 NewEngine

// NewEngine return storage interface
func NewEngine(cfg config.Config) (backup Backup, err error) {
    switch cfg.Database.Driver {
    case "postgres":
        return postgres.NewEngine(
            cfg.Database.Host,
            cfg.Database.Username,
            cfg.Database.Password,
            cfg.Database.Name,
            cfg.Storage.DumpName,
            cfg.Database.Opts,
        )
    case "mysql":
        return mysql.NewEngine(
            cfg.Database.Host,
            cfg.Database.Username,
            cfg.Database.Password,
            cfg.Database.Name,
            cfg.Storage.DumpName,
            cfg.Database.Opts,
        )
    case "mongo":
        return mongo.NewEngine(
            cfg.Database.Host,
            cfg.Database.Username,
            cfg.Database.Password,
            cfg.Database.Name,
            cfg.Storage.DumpName,
            cfg.Database.Opts,
        )
    }

    return nil, errors.New("We don't support Databaser Dirver: " + cfg.Database.Driver)
}

心得

雖然是一個不起眼的功能,但是還是花了一些時間把文件及結構寫清楚,對於之後在團隊導入或者是有新的 CLI 工具,都可以按照這格式進行,當然技術會一直改變,只會更好不會更差。希望這教學可以分享給想踏入 Go 語言,或者是想寫寫簡單的 CLI 工具的朋友們參考看看。

使用 GraphQL Gateway 串接多個 Data Schema

$
0
0

infra

不久之前寫過一篇『從 graphql-go 轉換到 gqlgen』,目前團隊舊有的專案還是繼續用 graphql-go 來撰寫,不過之後需求量越來越大,維護 graphql-go 就越來越困難,故有在想怎麼把 gqlgen 跟 graphql-go 相容在一起,那就是把這兩個套件想成不同的服務,再透過 Gateway 方式完成 single data graph。至於怎麼選擇 GraphQL Gateway 套件,最容易的方式就是使用 @apollo/gateway,但是由於個人比較偏好 Go 語言的解決方案,就稍微找看看有無人用 Go 實現了 Gateway,後來找到 nautilus/gateway,官方有提供文件以及教學 Blog 可以供開發者參考。底下會教大家使用 nautilus/gateway 將兩個不同的服務串接在一起。

線上影片

  • 00:00​ 為什麼有 GraphQL Gateway 需求?
  • 02:16​ 用兩個 Routing 來區分 graphql-go 跟 gqlgen
  • 03:00​ 用 jwt token check
  • 03:40​ 選擇 GraphQL Gateway 套件
  • 04:58​ main.go 撰寫機制介紹
  • 06:05​ 如何將 Token 往後面 Service 發送?
  • 06:58​ 看看完整的程式代碼
  • 07:56​ 最後心得感想

如果對於課程內容有興趣,可以參考底下課程。

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

整合 graphql-go + gqlgen

要把兩個不同的套件整合在一起,最簡單的方式就是分不同的 URL 區隔開來,兩邊都是透過 Bearer Token 來進行使用者身份確認。

        g := e.Group("/graphql")
        g.Use(auth.Check())
        {
            g.POST("", graphql.Handler())
            if config.Server.GraphiQL {
                g.GET("", graphql.Handler())
            }
        }
        q := root.Group("/query")
        q.Use(auth.Check())
        {
            q.POST("", gqlgen.SchemaHandler())
            q.GET("", gqlgen.SchemaHandler())
        }

透過 jwt 驗證及讀取使用者資料

// Check user bearer token
func Check() gin.HandlerFunc {
    return func(c *gin.Context) {
        if data, err := jwt.New().GetClaimsFromJWT(c); err != nil {
            c.Next()
        } else if id, ok := data["id"]; ok {
            var userID int64
            switch v := id.(type) {
            case int:
                userID = int64(v)
            case string:
                i, err := strconv.ParseInt(v, 10, 64)
                if err != nil {
                    log.Error().Err(err).Msg("can't convert user id to int64")
                }
                userID = i
            case float64:
                userID = int64(v)
            default:
                log.Info().Msgf("I don't know about user id type %T from token!", v)
            }

            user, err := model.GetUserByID(userID)
            if err != nil {
                log.Error().Err(err).Msg("can't get user data")
            }

            ctx := context.WithValue(
                c.Request.Context(),
                config.ContextKeyUser,
                user,
            )
            c.Request = c.Request.WithContext(ctx)
        }
    }
}

撰寫 graphql-gateway

使用 nautilus/gateway 可以簡單將 Schema 合併成單一 Data,不過此套件尚未支援 subscription

func main() {
    // default port
    port := "3001"
    server := "api:8080"
    if v, ok := os.LookupEnv("APP_PORT"); ok {
        port = v
    }

    if v, ok := os.LookupEnv("APP_SERVER"); ok {
        server = v
    }

    // introspect the apis
    schemas, err := graphql.IntrospectRemoteSchemas(
        "http://"+server+"/graphql",
        "http://"+server+"/query",
    )
    if err != nil {
        panic(err)
    }

    // create the gateway instance
    gw, err := gateway.New(schemas, gateway.WithMiddlewares(forwardUserID))
    if err != nil {
        panic(err)
    }

    // add the playground endpoint to the router
    http.HandleFunc("/graphql", withUserInfo(gw.PlaygroundHandler))

    // start the server
    fmt.Printf("🚀 Gateway is ready at http://localhost:%s/graphql\n", port)
    err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(1)
    }
}

由於之後要整合進 Docker 內,故透過 LookupEnv 來決定 Server 跟 Port。這樣可以將 /graphql/query 的 Schema 綁定在一起了。另外要解決的就是如何將 Authorization 傳到後面 GraphQL Server 進行認證。

// the first thing we need to define is a middleware for our handler
// that grabs the Authorization header and sets the context value for
// our user id
func withUserInfo(handler http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // look up the value of the Authorization header
        tokenValue := r.Header.Get("Authorization")
        // Allow CORS here By * or specific origin
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
        w.Header().Set("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
        // here is where you would perform some kind of validation on the token
        // but we're going to skip that for this example and just save it as the
        // id directly. PLEASE, DO NOT DO THIS IN PRODUCTION.

        // invoke the handler with the new context
        handler.ServeHTTP(w, r.WithContext(
            context.WithValue(r.Context(), "tokenValue", tokenValue),
        ))
    })
}

// the next thing we need to do is to modify the network requests to our services.
// To do this, we have to define a middleware that pulls the id of the user out
// of the context of the incoming request and sets it as the USER_ID header.
var forwardUserID = gateway.RequestMiddleware(func(r *http.Request) error {
    // the initial context of the request is set as the same context
    // provided by net/http

    // we are safe to extract the value we saved in context and set it as the outbound header
    if tokenValue := r.Context().Value("tokenValue"); tokenValue != nil {
        r.Header.Set("Authorization", tokenValue.(string))
    }

    // return the modified request
    return nil
})

其中上面的 Access-Control 用來解決 CORS 相關問題。前端用各自電腦開發時,就需要此部分。

心得

用 gqlgen 在開發上效率差很多,現在透過這方式,可以保留舊的 Schema 搭配新的 gqlgen 開發模式,未來也可以將共通的功能獨立拆成單一服務,再透過 gateway 方式將多個模組合併。

初探 Infrastructure as Code 工具 Terraform vs Pulumi

$
0
0

cover pulumi and terraform

想必大家對於 Infrastructure as Code 簡稱 (IaC) 並不陌生,而這個名詞在很早以前就很火熱,本篇最主要介紹為什麼我們要導入 IaC,以及該選擇哪些工具來管理雲平台 (AWS, GCP, Azure 等…)。觀看現在很火紅的 Terraform 及後起之秀 Pulumi 是大家可以作為選擇的參考,而底下會來歸納優缺點及技術比較,以及為什麼我最後會選擇 Pulumi。這兩套都是由 Go 語言所開發,現在選擇工具前,都要先考慮看看什麼語言寫的,以及整合進團隊自動化部署流程難易度。

教學影片

  • 00:00 Infrastructure as code 簡介 (簡稱 IaC)
  • 00:43 資料工程師 Roadmap
  • 01:35 為什麼需要 IaC
  • 02:26 IaC 帶來什麼樣的好處及優勢
  • 04:20 工具的選擇 Pulumi vs Terraform
  • 04:52 Terraform 跟 Pulumi 基本介紹
  • 06:58 Terraform 代碼展示 (HCL)
  • 07:51 Pulumi 代碼展示 (Go 語言)
  • 08:43 Terraform 可否用程式語言撰寫?
  • 11:06 為什麼要選擇 Pulumi
  • 13:19 自行開發整合工具 (不用安裝 CLI)
  • 15:17 用 Pulumi 開發資料庫 Migration 流程

如果對於課程內容有興趣,可以參考底下課程。

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

IaC 帶來的好處

在沒有這些工具之前,大家土法煉鋼的就是手動連進去機器,不然就是透過 Web UI 畫面進行 VPC 或 EC2 的建置,這些步驟都沒有經過任何方式紀錄下來,也沒辦法透過 Review 的方式來避免人為上的操作,故現在有了一些工具,把這些實際上的操作,轉換成程式碼或者是其他文件格式像是 JSON 或 Yaml 等 …,就可以解決蠻多之前無法避免的問題,底下是個人整理導入 IaC 所帶來的好處。

  1. 建置 CI/CD 自動化 (不用依賴 UI 操作)
  2. 版本控制 (大家可以一起 Review 避免錯誤)
  3. 重複使用 (減少建置時間)
  4. 環境一至性 (Testing, Staging, Production)
  5. 團隊成長 (分享學習資源)

個人覺得最後一點是最主要的考量,畢竟如果事情綁在一個人身上,最後沒人可以承接,會相當慘的,有時候技術就需要互相 Share,等到出狀況的時候,才可以互相 Cover。

Pulumi vs Terraform

IaC 工具真的太多了,此篇先拿 Pulumi 跟 Terraform 來管理雲平台做比較。Terraform 在 2014 年由 HashiCorp 公司推出,由 Go 語言所撰寫,並自定義了 HCL 語言 (HashiCorp configuration language),所以如果要入門 Terraform 的話,你首先需要先熟悉 HCL 語法,熟悉到一定程度後,就可以開始撰寫 HCL 來管理雲平台。底下看看 HCL 語法

data "aws_ami" "ubuntu_16_04_docker" {
  filter {
    name   = "name"
    values = ["app-base-docker-image-*"]
  }
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
  owners = ["161654634948"] # Canonical
}

resource "aws_instance" "foobar_api_01" {
  ami                    = data.aws_ami.ubuntu_16_04_docker.id
  instance_type          = "t2.small"
  vpc_security_group_ids = [aws_security_group.foobar_api.id]
  key_name               = aws_key_pair.foobar_deploy.key_name
  subnet_id              = aws_subnet.foobar_a.id
  iam_instance_profile   = aws_iam_instance_profile.ec2_role.id

  tags = {
    Name        = "foobar api 01"
    Project     = "foobar"
    Environment = var.environment["production"]
  }
}

另外來看一下後起之秀 Pulumi,其實 Pulumi 是站在 Terraform Provider 的肩膀上發展出來,也就是 Pulumi 寫了 Terraform Provider 的 Bridge,所以 Terrafrom Provider 提供了各種雲平台的 CRUD 之後,開發者就可以透過自己喜歡的語言來撰寫整體架構跟流程。最後讓我選擇 Pulumi 最大的原因還是在於開發者可以透過自己喜歡的語言來做到一樣的事情,新人不用重新熟悉 HCL 語法,加上如果在整體架構上有些額外的需求像是變數或邏輯上 Loop 比較複雜,在 HCL 上面會比較難實現,但是對於自己熟悉的語言 (GO, Python, JS … 等) 就可以很簡單的去實現出業務邏輯。

pulumi and terraform

Pulumi 提供 Automation API

Terraform 跟 Pulumi 要使用前,一定需要安裝各自的 CLI 工具,並透過 up 或 preview 來檢視需要修正的部分,自動化部署就需要把 CLI 安裝好才算完成。但是就在 2020 年末,Pulumi 直接推出 Automaton API,讓開發者可以直接將流程整合進去自行開發的軟體架構,不再依賴 CLI 工具,簡單舉個例子,假設需要開一個 RDS 服務,這時開發者會透過 Pulumi 撰寫格式,程式碼如下:

    _, err = rds.NewClusterInstance(ctx, "dbInstance", &rds.ClusterInstanceArgs{
      ClusterIdentifier:  cluster.ClusterIdentifier,
      InstanceClass:      rds.InstanceType_T3_Small,
      Engine:             rds.EngineTypeAuroraMysql,
      EngineVersion:      pulumi.String("5.7.mysql_aurora.2.03.2"),
      PubliclyAccessible: pulumi.Bool(true),
      DbSubnetGroupName:  subnetGroup.Name,
    })
    if err != nil {
      return err
    }

    ctx.Export("host", cluster.Endpoint)
    ctx.Export("dbName", dbName)
    ctx.Export("dbUser", dbUser)
    ctx.Export("dbPass", dbPass)

當拿到 DB 相關資訊後,接著要做一些 Migration,開發者無法在 pulumi up 同時完成 Migration 動作,而現在透過 Automation API,就可以輕鬆完成這件事情:

  // create our stack with an "inline" Pulumi program (deployFunc)
  stack := auto.UpsertStackInlineSource(ctx, stackName, projectName, deployFunc)
  // run the update to deploy our database
  res, err := stack.Up(ctx, stdoutStreamer)

  fmt.Println("Update succeeded!")

  // get the connection info
  host := res.Outputs["host"].Value
  dbName := res.Outputs["dbName"].Value
  dbUser := res.Outputs["dbUser"].Value
  dbPass := res.Outputs["dbPass"].Value

  // establish db connection
  db := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:3306)/%s", dbUser, dbPass, host, dbName))
  defer db.Close()

  // run our database "migration"
  fmt.Println("creating table...")
  db.Query(`
  CREATE TABLE IF NOT EXISTS hello_pulumi(
    id int(9) NOT NULL,
    color varchar(14) NOT NULL,
    PRIMARY KEY(id)
  );
 `)

開發者現在可以輕易打造自己的服務,或撰寫好用的 CLI 工具提供給其他開發者使用,而這些工具都是透過 Pulumi API 輕鬆完成,再也不需要 Pulumi CLI 就可以完成這些事情。


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

$
0
0

cover

上一篇作者提到了兩套 Infrastructure as Code 工具,分別是 TerraformPulumi,大家對於前者可能會是比較熟悉,那本篇用一個實際案例『建立 AWS S3 並上傳靜態網站』來跟大家分享如何從無開始一步一步使用 Pulumi。本教學使用的程式碼都可以在 GitHub 上面瀏覽及下載。教學會拆成七個章節:

  1. 建立 Pulumi 新專案
  2. 設定 AWS 環境
  3. 初始化 Pulumi 架構 (建立 S3 Bucket)
  4. 更新 AWS 架構 (S3 Hosting)
  5. 設定 Pulumi Stack 環境變數 (教學二)
  6. 建立第二個 Pulumi Stack 環境 (教學二)
  7. 刪除 Pulumi Stack 環境 (教學二)

教學影片

  • 00:00​ Pulumi 應用實作簡介
  • 01:30​ 章節一: 用 Pulumi 建立新專案
  • 02:39​ 用 Pulumi CLI 初始化專案
  • 04:47​ 介紹 Pulumi 產生的 AWS Go 目錄結構內容
  • 06:10​ 章節二: 設定 AWS 環境
  • 08:36​ 章節三: 建立 AWS S3 Bucket
  • 13:44​ 指定 S3 Bucket 名稱
  • 16:09​ 章節四: 將 S3 Bucket 變成 Web Host
  • 16:25​ 上傳 index.html 到 S3 Bucket 內
  • 18:22​ 設定 S3 Bucket 為 Web Host 讀取 index.html
  • 19:17​ 取得 AWS S3 的 Web URL
  • 20:55​ 修改 S3 Object 的 Permission (ACL)
  • 22:45​ 心得感想

用 Pulumi 建立新專案

步驟一: 安裝 Pulumi CLI 工具

首先你要在自己電腦安裝上 Pulumi CLI 工具,請參考官方網站,根據您的作業環境有不同的安裝方式,底下以 Mac 環境為主

brew install pulumi

透過 brew 即可安裝成功,那升級工具透過底下即可

brew upgrade pulumi

或者您沒有使用 brew,也可以透過 curl 安裝

curl -fsSL https://get.pulumi.com | sh

測試 CLI 指令

$ pulumi version
v2.20.0

有看到版本資訊就是安裝成功了

步驟二: 初始化專案

透過 pulumi new -h 可以看到說明

Usage:
  pulumi new [template|url] [flags]

Flags:
  -c, --config stringArray        Config to save
      --config-path               Config keys contain a path to a property in a map or list to set
  -d, --description string        The project description; if not specified, a prompt will request it
      --dir string                The location to place the generated project; if not specified, the current directory is used
  -f, --force                     Forces content to be generated even if it would change existing files
  -g, --generate-only             Generate the project only; do not create a stack, save config, or install dependencies
  -h, --help                      help for new
  -n, --name string               The project name; if not specified, a prompt will request it
  -o, --offline                   Use locally cached templates without making any network requests
      --secrets-provider string   The type of the provider that should be used to encrypt and decrypt secrets (possible choices: default, passphrase, awskms, azurekeyvault, gcpkms, hashivault) (default "default")
  -s, --stack string              The stack name; either an existing stack or stack to create; if not specified, a prompt will request it
  -y, --yes                       Skip prompts and proceed with default values

可以選擇的 Template 超多,那我們這次用 AWS 搭配 Go 語言的 Temaplate 當作範例

$ pulumi new aws-go --dir demo
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: (demo)
project description: (A minimal AWS Go Pulumi program)
Created project 'demo'

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-northeast-1
Saved config

Installing dependencies...

Finished installing dependencies

Your new project is ready to go! ✨

To perform an initial deployment, run 'cd demo', then, run 'pulumi up'

步驟三: 檢查專案目錄結構

└── demo
    ├── Pulumi.dev.yaml
    ├── Pulumi.yaml
    ├── go.mod
    ├── go.sum
    └── main.go

其中 main.go 就是主程式檔案

package main

import (
    "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", nil)
        if err != nil {
            return err
        }

        // Export the name of the bucket
        ctx.Export("bucketName", bucket.ID())
        return nil
    })
}

設定 AWS 環境

在使用 Pulumi 之前要先把 AWS 環境建立好

前置作業

請先將 AWS 環境設定完畢,請用 aws configure 完成 profile 設定

aws configure --profile demo

步驟一: 設定 AWS Region

可以參考 AWS 官方的 Available Regions,並且透過 Pulumi CLI 做調整

cd demo && pulumi config set aws:region ap-northeast-1

切換到 demo 目錄,並執行 pulumi config

步驟二: 設定 AWS Profile

如果你有很多環境需要設定,請使用 AWS Profile 作切換,不要用 default profile。其中 demo 為 profile 名稱

pulumi config set aws:profile demo

初始化 Pulumi 架構 (建立 S3 Bucket)

步驟一: 建立新的 S3 Bucket

        bucket, err := s3.NewBucket(ctx, "my-bucket", nil)
        if err != nil {
            return err
        }

步驟二: 執行 Pulumi CLI 預覽

透過底下指令可以直接預覽每個操作步驟所做的改變:

pulumi up

可以看到底下預覽:

Previewing update (dev)

View Live: https://app.pulumi.com/appleboy/demo/dev/previews/db6a9e4e-f391-4cc4-b50c-408319b3d8e2

     Type                 Name       Plan
 +   pulumi:pulumi:Stack  demo-dev   create
 +   └─ aws:s3:Bucket     my-bucket  create

Resources:
    + 2 to create

Do you want to perform this update?  [Use arrows to move, enter to select, type to filter]
  yes
> no
  details

選擇最後的 details:

Do you want to perform this update? details
+ pulumi:pulumi:Stack: (create)
    [urn=urn:pulumi:dev::demo::pulumi:pulumi:Stack::demo-dev]
    + aws:s3/bucket:Bucket: (create)
        [urn=urn:pulumi:dev::demo::aws:s3/bucket:Bucket::my-bucket]
        acl         : "private"
        bucket      : "my-bucket-e3d8115"
        forceDestroy: false

可以看到更詳細的建立步驟及權限,在此步驟可以詳細知道 Pulumi 會怎麼設定 AWS 架構,透過此預覽方式避免人為操作失誤。

步驟三: 執行部署

看完上面的預覽,我們最後就直接執行:

Do you want to perform this update? yes
Updating (dev)

View Live: https://app.pulumi.com/appleboy/demo/dev/updates/3

     Type                 Name       Status
 +   pulumi:pulumi:Stack  demo-dev   created
 +   └─ aws:s3:Bucket     my-bucket  created

Outputs:
    bucketName: "my-bucket-9dd3052"

Resources:
    + 2 created

Duration: 17s

透過上述 UI 也可以看到蠻多詳細的資訊

步驟四: 顯示更多 Bucket 詳細資訊

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

執行 pulumi up

Updating (dev)

View Live: https://app.pulumi.com/appleboy/demo/dev/updates/4

     Type                 Name      Status
     pulumi:pulumi:Stack  demo-dev

Outputs:
  + bucketID  : "my-bucket-9dd3052"
    bucketName: "my-bucket-9dd3052"

Resources:
    2 unchanged

Duration: 7s

步驟五: 更新 Bucket 名稱

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"),
        })
        if err != nil {
            return err
        }

        // Export the name of the bucket
        ctx.Export("bucketID", bucket.ID())
        ctx.Export("bucketName", bucket.Bucket)
        return nil
    })
}

透過 pulumi up

Previewing update (dev)

View Live: https://app.pulumi.com/appleboy/demo/dev/previews/7180c121-235c-40cc-9ae2-d0f68455296f

     Type                 Name       Plan        Info
     pulumi:pulumi:Stack  demo-dev
 +-  └─ aws:s3:Bucket     my-bucket  replace     [diff: ~bucket]

Outputs:
  ~ bucketID  : "my-bucket-9dd3052" => output<string>
  ~ bucketName: "my-bucket-9dd3052" => "foobar-1234"

Resources:
    +-1 to replace
    1 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/bucket:Bucket: (delete-replaced)
        [id=my-bucket-9dd3052]
        [urn=urn:pulumi:dev::demo::aws:s3/bucket:Bucket::my-bucket]
    +-aws:s3/bucket:Bucket: (replace)
        [id=my-bucket-9dd3052]
        [urn=urn:pulumi:dev::demo::aws:s3/bucket:Bucket::my-bucket]
      ~ bucket: "my-bucket-9dd3052" => "foobar-1234"
    ++aws:s3/bucket:Bucket: (create-replacement)
        [id=my-bucket-9dd3052]
        [urn=urn:pulumi:dev::demo::aws:s3/bucket:Bucket::my-bucket]
      ~ bucket: "my-bucket-9dd3052" => "foobar-1234"
    --outputs:--
  ~ bucketID  : "my-bucket-9dd3052" => output<string>
  ~ bucketName: "my-bucket-9dd3052" => "foobar-1234"

可以看到系統會砍掉舊的,在建立一個新的 bucket

更新 AWS 架構 (S3 Hosting)

上個步驟教大家如何建立 Infra 架構,那這單元教大家如何將使用 S3 當一個簡單的 Web Hosting。

  1. 將 index.html 放入 S3 內
  2. 設定 S3 當作 Web Hosting
  3. 測試 S3 Hosting

步驟一: 建立 index.html 放入 S3 內

建立 content/index.html 檔案,內容如下

<html>
  <body>
    <h1>Hello Pulumi S3 Bucket</h1>
  </body>
</html>

修改 main.go,將 index.html 加入到 S3 bucket 內

        index := path.Join("content", "index.html")
        _, err = s3.NewBucketObject(ctx, "index.html", &s3.BucketObjectArgs{
            Bucket: bucket.Bucket,
            Source: pulumi.NewFileAsset(index),
        })

        if err != nil {
            return err
        }

其中目錄結構如下

├── demo
│   ├── Pulumi.dev.yaml
│   ├── Pulumi.yaml
│   ├── content
│   │   └── index.html
│   ├── go.mod
│   ├── go.sum
│   └── main.go

部署到 S3 Bucket 內

$ pulumi up
Previewing update (dev)

View Live: https://app.pulumi.com/appleboy/demo/dev/previews/a0ac1b69-06b9-4109-800d-20618b36e5c8

     Type                    Name        Plan
     pulumi:pulumi:Stack     demo-dev
 +   └─ aws:s3:BucketObject  index.html  create

Resources:
    + 1 to create
    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: (create)
        [urn=urn:pulumi:dev::demo::aws:s3/bucketObject:BucketObject::index.html]
        acl         : "private"
        bucket      : "foobar-1234"
        forceDestroy: false
        key         : "index.html"
        source      : asset(file:77aab46) { content/index.html }

步驟二: 設定 S3 為 Web Hosting

修改 main.go

        bucket, err := s3.NewBucket(ctx, "my-bucket", &s3.BucketArgs{
            Bucket: pulumi.String("foobar-1234"),
            Website: s3.BucketWebsiteArgs{
                IndexDocument: pulumi.String("index.html"),
            },
        })

        index := path.Join("content", "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))),
        })

最後設定輸出 URL:

ctx.Export("bucketEndpoint", bucket.WebsiteEndpoint)

最後完整程式碼如下:

package main

import (
    "mime"
    "path"

    "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
        }

        index := path.Join("content", "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))),
        })

        if 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
    })
}

執行 pulumi up

Previewing update (dev)

View Live: https://app.pulumi.com/appleboy/demo/dev/previews/edbaefca-f723-4ac5-aabd-7cb638636612

     Type                    Name        Plan       Info
     pulumi:pulumi:Stack     demo-dev
 ~   ├─ aws:s3:Bucket        my-bucket   update     [diff: +website]
 ~   └─ aws:s3:BucketObject  index.html  update     [diff: ~acl,contentType]

Outputs:
  + bucketEndpoint: output<string>

Resources:
    ~ 2 to update
    1 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/bucket:Bucket: (update)
        [id=foobar-1234]
        [urn=urn:pulumi:dev::demo::aws:s3/bucket:Bucket::my-bucket]
      + website: {
          + indexDocument: "index.html"
        }
    --outputs:--
  + bucketEndpoint: output<string>
    ~ aws:s3/bucketObject:BucketObject: (update)
        [id=index.html]
        [urn=urn:pulumi:dev::demo::aws:s3/bucketObject:BucketObject::index.html]
      ~ acl        : "private" => "public-read"
      ~ contentType: "binary/octet-stream" => "text/html; charset=utf-8"
Do you want to perform this update? yes
Updating (dev)

View Live: https://app.pulumi.com/appleboy/demo/dev/updates/7

     Type                    Name        Status      Info
     pulumi:pulumi:Stack     demo-dev
 ~   ├─ aws:s3:Bucket        my-bucket   updated     [diff: +website]
 ~   └─ aws:s3:BucketObject  index.html  updated     [diff: ~acl,contentType]

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

Resources:
    ~ 2 updated
    1 unchanged

Duration: 13s

步驟三: 測試 URL

透過底下指令可以拿到 S3 的 URL:

pulumi stack output bucketEndpoint

透過 CURL 指令測試看看

$ curl -v $(pulumi stack output bucketEndpoint)
*   Trying 52.219.16.96...
* TCP_NODELAY set
* Connected to foobar-1234.s3-website-ap-northeast-1.amazonaws.com (52.219.16.96) port 80 (#0)
> GET / HTTP/1.1
> Host: foobar-1234.s3-website-ap-northeast-1.amazonaws.com
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< x-amz-id-2: 0NrZfFxZNOs+toz0/86FiASG+MyQE6f+KbKNi4wzcDtmn5mTnQoxupVybR464X8Oi6HDMjSU+i8=
< x-amz-request-id: A55BD2534EDC94A9
< Date: Thu, 11 Feb 2021 03:30:42 GMT
< Last-Modified: Thu, 11 Feb 2021 03:29:14 GMT
< ETag: "46e94ba24774d0c4a768f9461e6b9806"
< Content-Type: text/html; charset=utf-8
< Content-Length: 70
< Server: AmazonS3
<
<html>
  <body>
    <h1>Hello Pulumi S3 Bucket</h1>
  </body>
</html>
* Connection #0 to host foobar-1234.s3-website-ap-northeast-1.amazonaws.com left intact

心得

上述已經可以將靜態網站放在 AWS S3 上面了,下一篇會教大家底下三個章節

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

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

$
0
0

cover

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

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

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

設定 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 是一件令人舒服的事情。

搶救 Terraform State 檔案

$
0
0

recovery the terraform state file

近期其中一個專案使用 Terraform 來管理 AWS 雲平台,初期預計只有我一個人在使用 Terraform,所以就沒有將 Backend State 放在 AWS S3 進行備份管理,這個粗心大意讓我花了大半時間來搶救 State (.tfstate) 檔案,而搶救過程也是蠻順利的,只是需要花時間用 terraform import 指令將所有的 State 狀態全部轉回來一次,當然不是每個 Resource 都可以正常運作,還是需要搭配一些修正才能全部轉換。

結論: 請使用 terraform import 指令,這是最終解法。

教學影片

  • 00:00 為什麼需要搶救 Terraform state 檔案
  • 02:10 步驟一: 使用 Terraform refres 指令
  • 04:00 步驟二: 使用 Terraform import 指令
  • 09:15 步驟三: 將 State 備份到 S3 上面並且做版本控制

步驟一: 使用 terraform refresh

先用 terraform refresh 將所有 tf 檔案內的 data 框架內容讀取進來,像是底下格式:

data "aws_acm_certificate" "example_com" {
  domain = "*.example.com"
  types  = ["IMPORTED"]
}

或是

data "aws_ami" "ubuntu_16_04" {
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  filter {
    name   = "image-id"
    values = ["ami-0d97809b54a5f01ba"]
  }

  owners = ["099720109477"] # Canonical
}

這時候你可以看到專案底下多了 terraform.tfstate 檔案,記錄了這些檔案資料。

步驟二: 用 terraform import

這邊沒啥技巧,就是使用 terraform import 將剩下的 resource 慢慢匯入進來,此步驟需要花蠻多時間的,請大家慢慢操作跟使用,後續搭配 terraform plan 方式來看看有哪些資源還沒匯入,最重要的是一些機器的資源匯入,像是 EC2, RDS 等.. 這些是非常重要的部分。像是我這邊遇到,原本建立 RDS 的密碼都是透過 random_string

resource "random_string" "db_password" {
  special = false
  length  = 20
}

這邊你就需要把這邊整段拿掉,並且在 RDS 那邊把密碼欄位拿掉,否則你會發現密碼會被重新產生。只要是有產生動態密碼的地方,請務必小心,該拿掉的地方還是要拿掉,最後請務必把 terraform plan 檢查清楚,最後才下 terraform apply

步驟三: 上傳 state 到 S3

有了這次經驗,上述完成步驟二,整個更新 infra 也沒問題後,就需要把本機端的 state 上傳到 AWS S3 進行備份,並且做版本控制啊。打開 main.tf

terraform {
  backend "s3" {
    bucket = "xxxxx.backup"
    key    = "terraform/terraform.tfstate"
    region = "ap-southeast-1"
  }
}

存檔後,直接下 terraform init 就會將檔案直接轉到 AWS S3 上了。

兩台電腦透過 croc 工具來傳送檔案 (簡單, 加密, 快速)

$
0
0

croc

兩台電腦之間該如何傳送檔案,其實方法有超多種的,像是 FTP 或透過 SSH 方式來傳送檔案,但是這些方法步驟都有點複雜,FTP 需要架設 FTP 服務,SSH 要學習 SCP 指令,那有沒有更好的方式從單一電腦點對點傳送檔案到另一台呢?傳送過程需要快速又要安全,本篇介紹一套用 Go 語言寫的工具叫 croc,詳細的介紹可以參考看看作者的 Blog 介紹,此工具有底下功能及優勢。

影片教學

  • 00:00​ 兩台電腦該如何傳送檔案?
  • 01:40​ 介紹 croc 工具優勢跟特點
  • 03:24​ 如何使用 croc 工具
  • 05:34​ 自行產生 secret code 方式
  • 06:36​ croc relay server 介紹
  • 07:11​ 自行架設 relay server
  • 10:25​ 心得 (簡單, 快速, 安全)

工具特點及優勢

  1. 用 relay 方式讓任意兩台電腦傳送檔案
  2. 點對點加密 (使用 PAKE)
  3. 跨平台傳送檔案 (Windows, Linux, Mac)
  4. 一次可以傳送多個檔案或整個目錄
  5. 支援續傳
  6. 不需要自行架設服務或使用 port-forwarding 相關技術
  7. 優先使用 ipv6,而 ipv4 當作備援
  8. 可以使用 socks5 proxy

使用方式

使用方式如同底下這張圖所表示

croc

傳送端只需要執行 croc send file.txt 即可

$ croc send ~/Downloads/data.csv
Sending 'data.csv' (632.9 kB)
Code is: cabinet-rodeo-mayday
On the other computer run

croc cabinet-rodeo-mayday

上面可以看到會自動產生一個 secret code,接著在另外一台電腦執行底下指令

$ croc cabinet-rodeo-mayday
Accept 'data.csv' (632.9 kB)? (y/n) y

Receiving (<-111.243.108.9:51032)

當然你可以自訂 secret code

croc send --code appleboy ~/Downloads/data.csv

由於此工具是透過 relay server 方式來進行傳送,所以指令會預設連到官方所架設的服務器

// DEFAULT_RELAY is the default relay used (can be set using --relay)
var (
    DEFAULT_RELAY      = "croc.schollz.com"
    DEFAULT_RELAY6     = "croc6.schollz.com"
    DEFAULT_PORT       = "9009"
    DEFAULT_PASSPHRASE = "pass123"
)

假設你想要自己架設 relay server 呢?很簡單,這工具也讓開發者很快架設一台 relay server,只要執行底下

$ croc relay
[info]  2021/02/16 11:38:59 starting croc relay version v8.6.7-05640cd
[info]  2021/02/16 11:38:59 starting TCP server on 9010
[info]  2021/02/16 11:38:59 starting TCP server on 9012
[info]  2021/02/16 11:38:59 starting TCP server on 9009
[info]  2021/02/16 11:38:59 starting TCP server on 9013
[info]  2021/02/16 11:38:59 starting TCP server on 9011

可以指定單一 port:

$ croc relay --ports 3001
[info]  2021/02/16 11:39:22 starting croc relay version v8.6.7-05640cd
[info]  2021/02/16 11:39:22 starting TCP server on 3001

接著在傳送檔案也要跟著換掉 relay server

$ croc --relay 127.0.0.1:3001 send ~/Downloads/data.csv
Sending 'data.csv' (632.9 kB)
Code is: saddle-origin-horizon
On the other computer run

croc --relay 127.0.0.1:3001 saddle-origin-horizon

可以看到需要加上 --relay 127.0.0.1:3001 就可以完成了,所以很簡單的架設 relay server,這樣官方服務掛了,你也可以在任意一台電腦裝上 relay server 了。

心得

croc 工具強調的就是: 簡單 + 安全 + 快速,三大優勢,讓大家可以更容易點對點傳送檔案,加上 CLI 工具在任何平台都可以下載 (Windows, Mac, 及 Linux),只需要一個指令就可以裝好此工具,跟其他朋友傳送檔案。未來會再多介紹一些好用工具給大家。

即時效能分析工具 Pyroscope

$
0
0

當網站上線後,流量增加或短暫功能故障,都會造成使用者體驗相當不好,而這時該怎麼快速找到效能的瓶頸呢?通常 CPU 衝到 100% 時,有時候也蠻難複製及找出關鍵問題點。本篇會介紹一套工具叫 pyroscope,讓開發者可以快速找到效能瓶頸的程式碼。之前也寫了相關的效能瓶頸文章,可以參考看看『Go 語言用 pprof 找出程式碼效能瓶頸』或『善用 Go 語言效能測試工具來提升執行效率』,上述兩篇都是針對 Go 語言的效能分析文章,而 pyroscope 目前可以支援在 Python, RubyGo 的環境。底下筆者會針對 Go 環境做介紹。

什麼是 Pyroscope?

Pyroscope 是一套開源的效能即時監控平台,簡單的 Server 及 Agent 架構,讓開發者可以輕鬆監控代碼效能,不管你要找 10 秒或幾分鐘內的效能數據,都可以快速的即時呈現,開發者也不用在意裝了此監控會造成任何效能上的負擔。Pyroscope 背後的儲存採用 Badger 這套 Key-Value 資料庫,效能上是非常好的。目前只有支援 3 種語言 (Python, Ruby 及 Go) 未來會預計支援 NodeJS。假設您還沒導入任何效能分析工具或平台,那 Pyroscope 會是您最好的選擇。

Pyroscope 架構

如果你有打算找效能分析工具平台,Pyroscope 提供了三大優勢,讓開發者可以放心使用

  1. 低 CPU 使用率,不會影響既有平台
  2. 可儲存好幾年的資料,並且用 10 秒這麼細的顆粒度看資料
  3. 壓縮儲存資料,減少浪費硬碟空間

架構只有分 Server 跟 Agent 而已,可以參考底下架構圖,除了 Go 語言之外,Python 跟 Ruby App 都是透過 pyroscope 指令啟動相關 app 來監控系統效能。底下架構圖來自官方網站

啟動 Pyroscope 服務

啟動方式有兩種,第一是直接用 docker 指令啟動

docker run -it -p 4040:4040 pyroscope/pyroscope:latest server

另一種可以用 docker-compose 啟動

---
services:
  pyroscope:
    image: "pyroscope/pyroscope:latest"
    ports:
      - "4040:4040"
    command:
      - "server"

在 Go 裡面安裝 agent

本篇用 Go 語言當作範例,先 import package

import "github.com/pyroscope-io/pyroscope/pkg/agent/profiler"

接著在 main.go 寫入底下程式碼即可:

profiler.Start(profiler.Config{
    ApplicationName: "simple.golang.app",
    ServerAddress:   "http://pyroscope:4040",
})

其中 http://pyroscope 可以換成自訂的 hostname 即可,接著打開上述網址就可以看到效能監控的畫面了

透過畫面可以快速找到是 SQL 或哪個函式執行很久

心得

這套工具相當方便,在 Go 語言雖然可以用 pprof 快速找到問題,但是難免還是需要手動的一些地方才可以查出效能瓶頸,有了這套平台,就可以將全部 App 都進行監控,當使用者有任何問題,就可以快速透過 Pyroscope 查看看哪邊程式碼出了問題。

Viewing all 325 articles
Browse latest View live