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

Let’s Encrypt 將在 2018 年一月支援 Wildcard Certificates

$
0
0
Letsencrypt Let’s Encrypt 宣布在 2018 年一月全面支援 Wildcard Certificates,目的就是讓全世界網站都支援 HTTPS 協定。自從 2015 年 12 月宣布免費支援申請 HTTPS 憑證,從原本的 40% 跳升到 58%,Let’s Encrypt 到現在總共支援了 47 million 網域。

升級 API

2018 年 1 月 Let’s Encrypt 將支援 IETF-standardized ACME v2 版本,到時候就可以自動申請 Wildcard Certificates。Go 語言autocert packgeCaddy 都要一併修正支援 v2 API 版本。未來只要申請一次,就可以使用在全部 sub domain (像是 *.example.com)。

結論

Let’s Encrypt 讓 Web 開發者都可以享用到 HTTPS 的優勢,當然也造成不少問題,之前寫了幾篇關於 Let’s Encrypt 可以參考。實在無法想像現在還有沒支援 HTTPS 的網站,之前台北市政府外包案出包『智慧支付 App 忘記切換HTTPS加密傳輸』,還是被一位外國人發現的。

Drone 發佈 0.8.0-rc.1 版本

$
0
0
drone-logo_512 Drone 作者在昨天晚上發佈了 0.8.0-rc.1,此版本有兩個重大變更,第一是 Server 跟 Angent 之間溝通方式轉成 GRPC,另一個變更則是將原本單一執行擋 drone 拆成兩個,也就是之後會變成 drone-serverdrone-agent,拆成兩個好處是,通常 Server 端只會有一台,但是隨著專案越來越多,團隊越來越龐大,Agent 肯定不只有一台機器,所以把 Agent 拆出來可以讓維運人員架設新機器時更方便。

執行畫面

此版本的 UI 也有不同的改變,但是還是以簡單為主,也支援手機端瀏覽,首先看到在單一 Build 的狀態,現在可以顯示每一個步驟的執行時間 Screen Shot 2017-07-21 at 2.16.30 PM 點選任意一個步驟後,可以看到該步驟詳細紀錄,右邊則會顯示步驟列表 Screen Shot 2017-07-21 at 2.18.15 PM

Secret 設定頁面

不需要透過 Command Line 也可以將 Secret (像是 Docker 帳號密碼等) 透過此頁面設定,不過這邊有個缺陷,不能指定 Image,在 Command line 可以設定 Secret 綁定在特定 Docker image 身上。 Screen Shot 2017-07-21 at 2.17.20 PM

Registry 設定頁面

如果在公司內部有架設 Docker Registry 的話,可以透過此頁面將帳號密碼設定 Screen Shot 2017-07-21 at 2.17.30 PM

Project 設定頁面

此頁面可以設定專案狀態,包含執行幾分鐘後就直接停止等。 Screen Shot 2017-07-21 at 2.17.37 PM

結論

此版的 UI 畫面實在是太讚了,尤其是執行步驟畫面,可以看到每個步驟執行時間,早上跟作者聊一下,說下週六我要拿 Drone 現在最新版來教大家,他回說那他會保證這週到下週的修改不會影響到我上課。底下是上課時間跟內容,歡迎大家報名參加『用一天打造團隊自動化測試及部署』。
  • 時間: 2017/07/29 09:30 ~ 17:30
  • 地點: CLBC 大安館 (台北市大安區區復興南路一段283號4樓)
  • 價格: 3990 元

2017 COSCUP 研討會: Gitea + Drone 介紹

$
0
0
gitea-lg 今年很高興可以到 COSCUP 分享『Gitea + Drone 介紹』,我是在第二天的最後一場來做分享,最後還被大會進來趕人,講超過時間了。這次是我第一次到台大社科院,太陽真的好大,兩天下來流的汗水,大概已經是一年份的了。由於今年 COSCUP 不供應午餐,在第一天中午到科技站出口,左轉第一個店面就坐下來吃麵,店面不大,賣傳統小吃,我點了麻醬麵大碗 55 元,燙青菜 35 元,真的很大碗,不知道是不是因為在學校附近的關係,所以特別大碗,我心裡想說,這裡不是台北嗎? P_20170805_123130_HDR.jpg P_20170805_123141_HDR.jpg

講課內容

第二天講課,現場有 Demo 如何在十分鐘內快速安裝 Gitea + Drone,會議上大致介紹 Gitea 專案由來,以及提到最終我們希望是由 Gitea 來 host Gitea 這開源專案,當然目前還缺少幾項重大功能,可以參考此列表。底下是這次分享的投影片

問與答

現在有兩位朋友提問,我就在這邊寫下 Q&A

問: Gitea 可以用在正式環境嗎?

我這邊的想法是,如果團隊不至於很大,相信以 Gitea 都可以撐得住,我個人拿來放 Android 的 source code 或者是嵌入式系統專案的程式碼,含 Kernel 是幾 G 的大小,目前都還撐得住,對於一般的 Web 服務,我想代碼容量應該是更小才是。目前我知道有幾間新創團隊有開始使用 Gitea,用起來還算不錯,至少幫忙完成架設後,再也沒找過我了。相信穩定性及效能上基本上來說還算夠。

問: Gitea 整合 Jenkins 有無問題?

在課堂上有講到在今年初我拿 gogs plugin 來搭配 Gitea,你可以看此 plugin 的教學是需要手動在每個專案的 Webhook 頁面設定上 Jenkins Hook,透過 Hook 去觸發 Jenkins 的任務,我個人覺得不是很友善,畢竟每次開新專案都需要手動設定一次。我發現今年七月,Jenkins 團隊有寫了 Gitea Plugin,所以只需要到 Jenkins 後台將外掛啟動就好,不過本人今天測試 Gitea + Jenkins,發現還是無法自動註冊 WebHook URL,所以不確定是哪邊有問題,如果大家有時間的話,可以幫忙測試看看。

影片

今年 COSCUP 大會有現場播+錄影,沒來的朋友們,可以直接看底下影片,我是在最後半小時。

後記

這次演講有帶到一些些 Drone,講的沒有很深入,也僅此於 Demo,如果對於 Drone 想深入了解,可以參加今年 9/04 – 9/06 台灣第一屆 DevOpsDays Taipei 2017,當天會分享更深入的 Drone 流程,另外透過此投票,可以選擇你要聽的場次,我猜是越高票,就會到越大的會議廳。

用 Go 語言打造微服務架構

$
0
0
68747470733a2f2f7261772e6769746875622e636f6d2f676f6c616e672d73616d706c65732f676f706865722d766563746f722f6d61737465722f676f706865722e706e67 今年在 ModernWeb 講『用 Go 語言打造微服務架構』,蠻開心看到底下很多 Go 開發者,希望未來能有更多公司導入 Go 語言,底下是會議大綱:
  • Microservices vs. Monolithic 差異
  • 微服務核心架構 (Go 工具專案)
  • Go 語言核心高並發
  • 為什麼選用 Go 語言
  • 微服務代價跟準備

有像 AWS 雲端,為什麼要自己搭建微服務?

我在現場介紹了很多用 Go 語言搭建的工具跟服務,讓開發者可以不依靠任何雲端產品來架構微服務系統,但是相信大家一頭霧水,為什麼需要這些服務工具,不是已經有 AWS 或 GCP 了嗎?這邊我的回答是,很多客戶都有自己的機房,如果服務都擺到雲端,客戶的資料都是機密,不可能全部的客戶都想要把服務搭建但現有雲端平台,所以我會準備一套完全不需要雲端的方案,方便讓客戶可以在自己的機房搭建。

如何拆分微服務

會後有朋友問到,現在全部的功能都寫在一起,怎麼知道哪些功能可以拆出來當作微服務。在會議上我提到了底下幾點給大家參考:
  • 依照業務區分
  • 自動化部署
  • 高度容錯
  • 快速置換
  • 獨立開發
  • 易擴充
而我自己在團隊內拆分的標準是:如果很多專案都需要共用一個功能或需求,那就是可以拆出來,舉例來說,每個專案都需要後端伺服器串接 Google FCM 或 Apple 來發送手機訊息,這部分就可以直接拆成一個微服務,專門發送手機訊息。只要寫一次,就可以讓多個專案同時支援,當然服務跟服務之間的溝通方式要事先定好。

導入微服務代價

  • 系統複雜度提升
  • 系統資料一致性
  • 維運工作複雜化
導入微服務並不是只有優點,也是有很多代價的,除非有強大的 DevOps 團隊,否則當服務越來越多,系統複雜度提高,維運工作只會越來越複雜的。

微服務事前準備

  • 快速建置 (Develop)
  • 監控機制 (Monitor)
  • 快速部署 (Deploy)
準備微服務前,請先有內部系統監控機制,以及 CI/CD 的串接,讓開發者可以專心開發,不用擔心部署問題,另外由於微服務肯定會越來越多,所以一定要監控每個服務,達到機器管理機器,而不是增加人力去管理越多台服務,而在導入初期肯定無法完全做到,還是需要一些人為操作,但最終還是要全部自動化。

後記

微服務架構並不適合每個團隊,請依照團隊目前狀況以及業務需求,再來拆微服務,而不是聽到別人說微服務很潮,就開始建議主管或者是導入。最後附上投影片:

用 upx 壓縮 Go 語言執行擋

$
0
0
Go-brown-side.sh 剛開始學 Go 語言的時候,跟學習其他語言一樣,寫了底下一個簡單的 Hello World 檔案
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello World!")
}
這是一個最簡單的程式碼,接著透過 go build 的方式編譯出執行檔,這時候我們看看檔案大小:
$ du -sh hello*
1.8M    hello
4.0K    hello.go
一個 4K 大小的 Hello World,竟然要 1.8MB (使用 Go 1.9 版本),雖然說現在硬碟很便宜,網路傳輸也很快,但是對於大量 Deploy 到多台機器時,還是需要考量檔案大小,當然是越小越好,這時候我們可以使用 Go 語言的 shared 方式編譯出共同的 .a 檔案,將此檔案丟到全部機器,這樣再去編譯主執行擋,可以發現 Size 變成很小,應該不會超過 20K 吧。但是此種方式比較少人使用,大部分還直接將主程式編譯出來,這時來介紹另外一個工具用來減少 Binary 大小,叫做 UPX
UPX – the Ultimate Packer for eXecutables
當然 upx 不只是可以壓縮 Go 的編譯檔案,其他編譯檔案也可以壓縮喔,底下是透過 upx 使用兩種不同的編譯方式來減少執行檔大小
$ du -sh hello*
1.8M    hello
# upx -o hello_1 hello
636K    hello_1
# upx --brute -o hello_2 hello
492K    hello_2
4.0K    hello.go
用了兩種方式來壓縮大小,越小的檔案,處理的時間越久,壓縮比例至少超過 70%,效果相當不錯,用 Hello World 沒啥感覺,實際拿個 Produciton 的案子來試試看,就拿 Gorush 來壓縮看看
$ du -sh *
15M     gorush
# upx --brute -o gorush_1 gorush
3.4M    gorush_1
# upx -o gorush_2 gorush
4.7M    gorush_2
壓縮比例也差不多 70 %,但是使用 --brute 處理時間蠻久的,如果要加速部署,用一般模式即可。別因為些微差異,就花費多餘時間在壓縮上。

為什麼我用 Drone 取代 Jenkins 及 GitLab CI

$
0
0
Logo-DevOpsDays 終於有機會正式跟大家介紹為什麼我會捨棄 JenkinsGitLab CI,取而代之的是用 Go 語言寫的 Drone。今年很高興錄取台灣第一屆 DevOps Day 講師,在今年主題是『用 Drone 打造輕量級容器持續交付平台』,主要推廣這套 Drone CI/CD 工具,會議內容圍繞在 Jenkins, GitLab CI 跟 Drone 的比較。也提到為什麼我不用 Jenkins 及 GitLab CI 的幾個原因。底下整理議程大綱。
  • 為什麼選擇 Drone
  • Drone 基礎簡介
  • Drone 架構擴展
  • Drone 安裝方式
  • Drone 管理介面
  • Drone 測試部署
  • Drone 自訂套件
在講為什麼不用 Jenkins 或 GitLab CI 之前我們來看看大家都用什麼工具來串 CI/CD 流程 Screen Shot 2017-09-07 at 10.50.39 AM

為什麼不用 Jenkins

有六個原因大家可以想看看是否有踩到身為工程師的痛點
  1. 專案設定複雜 (連 DevOps 老手都這麼覺得)
  2. 流程版本控制 (同事改個設定檔,流程就爆掉)
  3. 無法擴充套件 (你會 Java 嗎?團隊內有人會嗎?)
  4. 後續維護? (同事離職或請假該怎麼辦)
  5. 學習困難? (新人完全不會啊)
  6. 團隊成長? (團隊內只有特定同事才會?)

為什麼不用 GitLab CI

GitLab CI 已經改善了很多 Jenkins 遇到的問題,但是還有兩點是我看到的缺陷:
  1. 只支援 GitLab 版本控制 (如果你用 GitHub 該怎麼辦)
  2. 無法擴充 Yaml 檔案寫法
大家可以想想第二點,假設你今天要部署 10 台伺服器,該如何將檔案同時丟到 10 台?當然在 GitLab CI 可以做到,但是可以比較看看 Drone 透過 drone-scp,而 GitLab CI 則是要自己自幹 Shell Script。所以可想而知,如果可以擴充 Yaml 寫法,就可以輕易簡化 Yaml 設定,讓流程更清楚。

導入 CI/CD 的瓶頸

在之前做過一份統計,大家對於導入 CI/CD 的瓶頸在哪邊,底下是統計圖 Screen Shot 2017-09-07 at 10.50.56 AM 可以看到前三名分別是:
  1. 工具設定複雜
  2. 團隊無法成長
  3. 新人學習困難
如果您的團隊有以上困擾,歡迎使用 Drone,底下是我錄製的 Udemy 線上課程。

Drone 線上課程

如果你對 Drone 有興趣,且想改善上面我提到的問題,歡迎訂購,課程訂價是 2600 元,不過在 DevOps Day 開賣,現在特價 1600 元。Coupoon 優惠碼: DEVOPSTAIPEI

購買網址

底下是我的投影片,歡迎大家參考:

部署 Go 語言 App 到 Now.sh

$
0
0
Go-brown-side.sh 本篇要教大家如何部署 Go 語言的 App 到 now.sh 服務。now 服務是讓開發者可以透過 JavaScript 或用 Docker 方式直接部署到 now 雲端機器,也就是 now 所提供的服務可以在自己電腦透過 package.jsonDockerfile 來部署 app。原先剛出來時候,只有支援 node.js 部署,後來才增加 Docker。透過 Docker 就可以來部署各種不同語言的專案。

事前準備

請先下載 now-cli 工具,直接到 Release 頁面找到相對應的執行檔。下載後丟到 /usr/local/bin 或新增到 $PATH 變數底下。

Go 語言

請直接在專案內建立 server.go
package main

import (
    "flag"
    "time"

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

func rootHandler(context *gin.Context) {
    currentTime := time.Now()
    currentTime.Format("20060102150405")
    context.JSON(200, gin.H{
        "current_time": currentTime,
        "text":         "Hello World",
    })
}

// GetMainEngine is default router engine using gin framework.
func GetMainEngine() *gin.Engine {
    r := gin.New()

    r.GET("/", rootHandler)

    return r
}

// RunHTTPServer List 8000 default port.
func RunHTTPServer() error {
    port := flag.String("port", "8000", "The port for the mock server to listen to")

    // Parse all flag
    flag.Parse()

    err := GetMainEngine().Run(":" + *port)

    return err
}

func main() {
    RunHTTPServer()
}
存檔後,請直接透過 go run 進行測試。

撰寫 Dockerfile

接著要寫 Dockerfile 來進行編譯及測試
FROM golang:1.9-alpine3.6

MAINTAINER Bo-Yi Wu <appleboy.tw@gmail.com>

ADD . /go/src/github.com/appleboy/go-hello

RUN go install github.com/appleboy/go-hello

EXPOSE 8000
CMD ["/go/bin/go-hello"]
請修改 github.com/appleboy/go-hello 路徑,接著透過底下指令來編譯 Docker,注意 EXPOSE 8000 代表需要將 docker 內的 8000 對外。
$ docker build -t appleboy/go-hello -f Dockerfile .
編譯成功後,使用 docker run 執行
$ docker run -p 8080:8000 appleboy/go-hello
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> main.rootHandler (1 handlers)
[GIN-debug] Listening and serving HTTP on :8000
你可以發現成功執行了 app。

部署到 Now.sh

在第一個步驟安裝好 now-cli 指令,請直接在專案目錄內下 now,就可以發現系統會自動上傳該專案目錄底下所有檔案。有些檔案是不需要上傳的話,請使用 .dockerignore 來自動忽略,node.js 則是透過 .npmignore。如果沒有發現 .dockerignore.npmignore 則是檢查 .gitignore 檔案。

Demo 網站

Now.sh 缺陷

部署慢

不確定是不是在台灣的關係,在家裡部署到 now.sh,檔案大小不到 5MB 要部署 30 分鐘以上。蠻久的。

免費方案

免費方案單一上傳檔案不能超過 1MB,這對於寫 Go 語言的,不能先編譯出 binary 然後再進行 Docker 包裝來說相當不方便。變成需要將檔案全部丟到 now.sh 再進行編譯,部署體驗就會變得不是很好。

無法指定 Dockerfile 名稱

現在只支援專案目錄底下的 Dockerfile,假設今天把檔名換成 Dockerfile.now 這樣是無法讀取的,command 也沒有參數讓開發者修正。

.dockerignore 無作用

如果在 .dockerignore 寫入
*
!bin/
!config/
可以發現加上 --debug 模式後,config 內的檔案沒有上傳到 now.sh,造成編譯失敗。

結論

如果想要透過 now.sh 來 demo,其實蠻方便的,要是上 production,我建議還是不要冒險。免費方案目前只支援三個 instance,每個月流量只能有 1G。大家可以參考試用看看。

用 Kubernetes 將 Drone CI/CD 架設在 AWS

$
0
0
Screen Shot 2017-09-24 at 12.53.39 AM Drone 是我今年主推的 CI/CD 自架服務,詳細可以參考這篇文章,目前在公司內部團隊使用了一年以上,服務相當穩定。Drone 本身可以透過 docker-compose 方式快速在機器上架設完成,但是由於 Kubernetes 的盛行,大家也希望能透過 Kubernetes 來安裝 Drone 服務。本篇會教大家如何在 AWS 上透過 Kubernetes 安裝完成。Drone 預設使用 SQLite 當作資料庫,檔案會直接存放在 /var/lib/drone 路徑底下,但是容器內不支援寫入,所以必須要要額外掛上空間讓 Drone 可以寫入資料。此篇會以 GitHub 認證 + SQLite 來教學。

事前準備

  1. 在 AWS 上面 建立 kubernets
  2. 在 AWS 上面建立 EBS 空間存放 SQLite
  3. 申請 GitHub OAut
在安裝 Drone 之前,有三件事情務必先完成,第一是要先有 K8S 環境,本篇不會教大家如何架設出 K8S 環境。但是你可以透過官方提供的方式來測試:
  1. 使用 Minikube (快速在本機端架設出 K8S Cluster)
  2. Katacoda
  3. Play with Kubernetes (單次免費四小時)
第二,你需要先在 AWS 上面建立一個 1G 的 EBS 空間,空間大小由你決定,你可以透過 AWS CLI 或直接到 AWS Console 頁面建立,底下是建立 EBS 的指令
$ aws ec2 create-volume \
  --availability-zone=ap-southeast-1a \
  --size=1 --volume-type=gp2
注意 availability-zone 區域要跟 K8S 同樣,大小先設定 1G。完成後會看到底下訊息:
{
    "AvailabilityZone": "ap-southeast-1a",
    "Encrypted": false,
    "VolumeType": "gp2",
    "VolumeId": "vol-04741f74eb0f6b891",
    "State": "creating",
    "Iops": 100,
    "SnapshotId": "",
    "CreateTime": "2017-09-23T15:42:24.319Z",
    "Size": 1
}
之後會用到 VolumeId。假如沒有做此步驟,您會發現 Drone 伺服器是無法啟動成功。最後是請到 GitHub 帳號內建立一個全新 OAuth App,並取得 CLientSecret 代碼。

安裝 Drone

所有的 Yaml 檔案都可以直接在 appleoy/drone-on-kubernetes,找到。首先打開 drone-server-deployment.yaml,找到 volumeID 取代成上述建立好的結果。
  awsElasticBlockStore:
    fsType: ext4
    # NOTE: This needs to be pointed at a volume in the same AZ.
    # You need not format it beforehand, but it must already exist.
    # CHANGEME: Substitute your own EBS volume ID here.
-   volumeID: vol-xxxxxxxxxxxxxxxxx
+   volumeID: vol-01f13b969e9dabff7
再來設定 server 跟 agent 溝通用的 Secret,打開 drone-secret.yaml
data:
-  server.secret: ZHJvbmUtdGVzdC1kZW1v
+  server.secret: ZHJvbmUtdGVzdC1zZWNyZXQ=
透過 base64 指令換掉上面的代碼。假設密碼設定 drone-test-secret,請執行底下指令
$ echo -n "drone-test-secret" | base64
ZHJvbmUtdGVzdC1zZWNyZXQ=
在 GitHub 上面建立新的 Application,並且拿到 Client ID 跟 Secret Key,修改 drone-configmap.yaml 檔案
server.host: drone.example.com
server.remote.github.client: xxxxx
server.remote.github.secret: xxxxx
接著陸續執行底下指令,新增 Drone NameSpace 並且將 server 及 agent 服務啟動
$ kubectl create -f drone-namespace.yaml
$ kubectl create -f drone-secret.yaml
$ kubectl create -f drone-configmap.yaml
$ kubectl create -f drone-server-deployment.yaml
$ kubectl create -f drone-server-service.yaml
$ kubectl create -f drone-agent-deployment.yaml
完成後,k8s 會自動建立 ELB,透過 kubectl 可以看到 ELB 名稱
$ kubectl --namespace=drone get service -o wide
執行後看到底下結果:
NAME            CLUSTER-IP      EXTERNAL-IP
drone-service   100.68.89.117   xxxxxxxxx.ap-southeast-1.elb.amazonaws.com
拿到 ELB 網域後,可以直接更新 GitHub 的 application 資料 Screen Shot 2017-09-23 at 3.40.28 PM 最後打開瀏覽器,填入上述的網址,會發現直接轉到 GitHub OAuth 認證,點選確認後,Drone 會開始讀取您的個人資料,這樣就代表成功了。

懶人包安裝

上述步驟是不是有點複雜,要執行的 kubectl 指令非常多,所以我提供了另一種安裝方式,就是透過 [install-drone.sh],執行這 Shell Script 檔案前,你必須先做兩件事情。1. 申請 EBS 空間,完成後請修改 drone-server-deployment.yaml 內的 volumeID。2. 申請 [GitHub OAuth Application][42],完成後請修改 drone-configmap.yaml 內的 GitHub 設定。最後執行底下指令
$ ./install-drone.sh

擴展 agent 服務

假設一個 agent 已經不符合團隊需求,在 k8s 內只要一個指令就可以自動水平擴展 agent:
$ kubectl scale deploy/drone-agent \
  --replicas=2 --namespace=drone
其中 replicas 可以改成你要的數字。

如何清除 Drone 服務

我們已經將 Drone 資料庫備份在 EBS,所以隨時都可以移除 Drone 服務,透過簡單的指令就可以清除掉 Drone 所有容器,只要下清除 Namespace 即可
$ kubectl delete -f drone-namespace.yaml

歡迎追蹤 drone-on-kubernetes

後記

這算是我的第一篇 Kubernetes 心得文,就先拿 Drone 服務來測試看看。Drone 作者也很用心寫了一個接口讓 K8S 可以監控 agent 容器,假如您不是使用 SQLite 而是用 MySQL 資料庫,就需要修改 YAML 設定檔。對於本篇文章有不懂的地方,歡迎大家留言。我也錄製了 15 分鐘影片放在 Udemy Drone 課程,課程到月底特價 1600 元。優惠代碼: DEVOPSTAIPEI

Drone 搭配 Kubernetes 升級應用程式版本

$
0
0
Screen Shot 2017-10-10 at 9.22.48 PM 本篇要教大家如何透過 Drone 搭配 Kubernetes 自動化升級 App container 版本。為什麼我只說升級 App 版本,而不是升級或調整 K8S Deployment 架構呢 (kubectl apply)?原因是本篇會圍繞在 honestbee 撰寫的 drone 外掛: drone-kubernetes,此外掛是透過 Shell Script 方式搭配 kubectl 指令來完成升級 App 版本,可以看到程式原始碼並無用到 kubectl apply 方式來升級,也並非用 Go 語言搭配 k8s API 所撰寫,所以無法使用 YAML 方式來進行 Deployment 的升級。本篇講解的範例都可以在 drone-nodejs-example 內找到。底下指令就是外掛用來搭配 Drone 參數所使用。
$ kubectl set image \
  deployment/nginx-deployment \
  nginx=nginx:1.9.1

事前準備

由於此外掛只有提供升級 App 版本,而不會自動幫忙建立 K8S 環境,所以在設定 Drone 之前,我們必須完成兩件事情。
  1. 建立 K8S Service 帳號
  2. 建立 K8S Deployment 環境

建立 K8S Service Account

首先要先建立一個 Service 帳號給 Drone 服務使用,也方便設定權限。
apiVersion: v1
kind: Namespace
metadata:
  name: demo

---

apiVersion: v1
kind: ServiceAccount
metadata:
  name: drone-deploy
  namespace: demo

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: drone-deploy
  namespace: demo
rules:
  - apiGroups: ["extensions"]
    resources: ["deployments"]
    verbs: ["get","list","patch","update"]

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: drone-deploy
  namespace: demo
subjects:
  - kind: ServiceAccount
    name: drone-deploy
    namespace: demo
roleRef:
  kind: Role
  name: drone-deploy
  apiGroup: rbac.authorization.k8s.io
先建立 demo namespace,再建立 drone-deploy 帳號,完成後就可以拿到此帳號 Token 跟 CA,可以透過底下指令來確認是否有 drone-deploy 帳號
$ kubectl -n demo get serviceAccounts
NAME           SECRETS   AGE
default        1         1h
drone-deploy   1         1h
接著顯示 drone-deploy 帳戶的詳細資訊
$ kubectl -n demo get serviceAccounts/drone-deploy -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: 2017-10-10T13:09:55Z
  name: drone-deploy
  namespace: demo
  resourceVersion: "917006"
  selfLink: /api/v1/namespaces/demo/serviceaccounts/drone-deploy
  uid: 4f445728-adbc-11e7-b130-06d06b7f944c
secrets:
- name: drone-deploy-token-2xzqw
可以看到 drone-deploy-token-2xzqw 此 secret name,接著從這名稱取得 ca.certtoken:
$ kubectl -n demo get \
  secret/drone-deploy-token-2xzqw \
  -o yaml | egrep 'ca.crt:|token:'
由於 token 是透過 base64 encode 輸出的。那也是要用 base64 decode 解出
# linux:
$ echo token | base64 -d && echo''
# macOS:
$ echo token | base64 -D && echo''

建立 K8S Deployment

在這邊我們需要先建立第一次 K8S 環境。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: k8s-node-demo
  namespace: demo
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: k8s-node-demo
    spec:
      containers:
      - image: appleboy/k8s-node-demo
        name: k8s-node-demo
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 3
          periodSeconds: 3

---

apiVersion: v1
kind: Service
metadata:
  name: k8s-node-demo
  namespace: demo
  labels:
    app: k8s-node-demo
spec:
  selector:
    app: k8s-node-demo
  # if your cluster supports it, uncomment the following to automatically create
  # an external load-balanced IP for the frontend service.
  type: LoadBalancer
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
先定義好 replicas 數量,及建立好 AWS Load Balancer。完成後,基本上你可以看到第一版本成功上線
$ kubectl get service
NAME            CLUSTER-IP      EXTERNAL-IP        PORT(S)        AGE
k8s-node-demo   100.67.24.253   a4fba848aadbc...   80:30664/TCP   11h

設定 Drone + Kubernetes

要完成 k8s 部署,必須要先將專案打包成 Image 並且上 Tag 丟到自家的 Private Registry。
publish:
  image: plugins/docker
  repo: appleboy/k8s-node-demo
  dockerfile: Dockerfile
  secrets:
    - source: demo_username
      target: docker_username
    - source: demo_password
      target: docker_password
  tags: [ '${DRONE_TAG}' ]
  when:
    event: tag
請先到 Drone 專案後台的 Secrets 設定 demo_usernamedemo_password 內容。並採用 GitHub 流程透過 Git Tag 方式進行。接著設定 K8S Deploy:
deploy:
  image: quay.io/honestbee/drone-kubernetes
  namespace: demo
  deployment: k8s-node-demo
  repo: appleboy/k8s-node-demo
  container: k8s-node-demo
  tag: ${DRONE_TAG}
  secrets:
    - source: k8s_server
      target: plugin_kubernetes_server
    - source: k8s_cert
      target: plugin_kubernetes_cert
    - source: k8s_token
      target: plugin_kubernetes_token
  when:
    event: tag
請注意 k8s_certk8s_token,你需要拿上面的 ca.certtoken 內容新增到 Drone Secrets 設定頁面。 Screen Shot 2017-10-10 at 11.28.09 PM 完成後,下個 Tag 來試試看是否能部署成功。最後附上專案範例位置:

drone-nodejs-example

後記

Honestbee 寫的 drone-kubernetes 外掛,無法直接讀取 K8S 設定檔,這是缺點之一,這樣就還需要另一個 Repo 存放全部 K8S 環境。雖然不是完全自動化的過程 (含建立 k8s 環境),但是至少也有 80 分啦。對於本篇文章有不懂的地方,歡迎大家留言。我也錄製了 15 分鐘影片放在 Udemy Drone 課程,課程特價 1600 元。優惠代碼: KUBERNETES

在 Go 語言使用 Viper 管理設定檔

$
0
0
viper 在每個語言內一定都會有管理設定檔的相關套件,像是在 Node.jsdotenv 套件,而在 Go 語言內呢?相信大家一定都會推 Hugo 作者寫的 Viper,Viper 可以支援讀取 JSON, TOML, YAML, HCL 等格式的設定檔案,也可以讀取環境變數,另外也可以直接跟取遠端設定檔整合(像是 etcdConsul),本篇會介紹如何使用 Viper。

情境需求

當專案是使用 Yaml 或 JSON 存放設定檔時,在不同的部署環境都需要不同的設定檔。這時候就需要設定 App 可以指定不同設定檔路徑,指令如下
$ app -c config.yaml
這樣測試同事拿到執行檔時,就可以透過 -c 參數來讀取個人設定檔。有個問題,假設設定檔需要動態修改,每次測完就改動一次有點麻煩,所以 App 必須要支援環境變數,像是如下:
$ APP_PORT=8088 app -c config.yaml
假如沒有帶入 -c 參數,App 要能讀取系統預設環境設定檔案,像是 ($HOME/.app/config.yaml)。下面來教大家如何透過 Viper 做到上述環境。

建立預設檔案

在 Go 語言內可以先用變數方式將 Yaml 直接寫在程式碼內:
var defaultConf = []byte(`
app:
  port: 3000
`)
接著設定 Viper 讀取 Yaml 檔案型態。
viper.SetConfigType("yaml")

讀取指定檔案

透過 Go 語言的 flag 套件可以輕易實作出命令列 -c 參數
flag.StringVar(&configFile, "c", "", "Configuration file path.")
接著就可以直接讀取 Yaml 檔案
if configFile != "" {
    content, err := ioutil.ReadFile(confPath)

    if err != nil {
        return conf, err
    }

    viper.ReadConfig(bytes.NewBuffer(content))
}
可以看到透過 viper.ReadConfig 可以把 Yaml 內容丟進去,之後就可以透過 viper.GetInt("app.port") 來存取資料。

讀取動態目錄

Viper 有個功能就是可以直接幫忙找尋相關目錄內的設定檔案。先假設底下路徑是您希望 App 可以自動幫你讀取:
  1. /etc/app/ (Linux 常用的 /etc/ 目錄)
  2. $HOME/.app (家目錄底下的 .app 目錄)
  3. . (執行當下目錄)
首先設定 Viper 要去找 config 開頭的設定檔案
viper.SetConfigName("config")
上面設定好,就會直接找 config.yaml 檔案,如果設定 app 則是找 app.yaml。接著指定設定檔所在目錄
viper.AddConfigPath("/etc/app/")
viper.AddConfigPath("$HOME/.app")
viper.AddConfigPath(".")
最後透過 ReadInConfig 來自動搜尋並且讀取檔案。
if err := viper.ReadInConfig(); err == nil {
    fmt.Println("Using config file:", viper.ConfigFileUsed())
}

從環境變數讀取

如果專案需要跑在容器環境,這樣此功能對部署來說非常重要,也就是我只需要將 Go 語言的執行檔包進去 Docker 就好,而不需要將 Yaml 設定檔一起包入,或是透過 Volume 方式掛起來。這樣至少減少了一個步驟。首先設定 Viper 自動讀取環境變數:
// read in environment variables that match
viper.AutomaticEnv()
接著設定環境變數 Prefix,避免跟其他專案衝突
// will be uppercased automatically
viper.SetEnvPrefix("test")
最後設定環境變數的分隔符號從 . 換成 _
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
以上面的例子來說,你可以透過 TEST_APP_PORT 來指定不同的 port
$ TEST_APP_PORT=3001 app -c config.yaml

實作範例

我們可以把上面的說明整理成範例,讀取流程會如下
  1. 讀取實體路徑
  2. 讀取預設路徑
  3. 讀取預設設定
假如 App 讀取特定路徑設定檔 (-c 參數),那就不會執行 2, 3 步驟,步驟 1 省略的話,App 就會自動先找預設路徑,如果預設路徑找不到就會執行步驟 3。程式碼範例如下:
viper.SetConfigType("yaml")
viper.AutomaticEnv()         // read in environment variables that match
viper.SetEnvPrefix("gorush") // will be uppercased automatically
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

if confPath != "" {
    content, err := ioutil.ReadFile(confPath)

    if err != nil {
        return conf, err
    }

    viper.ReadConfig(bytes.NewBuffer(content))
} else {
    // Search config in home directory with name ".gorush" (without extension).
    viper.AddConfigPath("/etc/gorush/")
    viper.AddConfigPath("$HOME/.gorush")
    viper.AddConfigPath(".")
    viper.SetConfigName("config")

    // If a config file is found, read it in.
    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Using config file:", viper.ConfigFileUsed())
    } else {
        // load default config
        viper.ReadConfig(bytes.NewBuffer(defaultConf))
    }
}

結論

我個人用 Viper 最大的原因就是可以透過環境變數修改 App 的預設參數,另外編譯 Docker 容器時也不需要將設定檔丟入。在 Kubernetes 架構內可以透過 config map 方式來動態改變 App 行為。如果要搭配命令列,可以使用 cobra 結合 Viper。

Gorush 輕量級手機訊息發送服務

$
0
0
68747470733a2f2f7261772e6769746875622e636f6d2f676f6c616e672d73616d706c65732f676f706865722d766563746f722f6d61737465722f676f706865722e706e67 今年第一次參加濁水溪以南最大研討會 Mopcon,給了一場議程叫『用 Go 語言打造輕量級 Push Notification 服務』,身為南部人一定要參加 Mopcon,剛好透過此議程順便發佈新版 Gorush,其實今年投稿 Mopcon 最主要是回家鄉宣傳 Google 所推出的 Go 語言,藉由實際案例來跟大家分享如何入門 Go 語言,以及用 Go 語言最大好的好處有哪些。底下是此議程大綱:
  • 為什麼建立 Gorush 專案
  • 如何用 Go 語言實作
  • Drone 自動化測試及部署
  • Kubernetes 上跑 Gorush

什麼是 Gorush

Gorush 是一套輕量級的 Push Notification 服務,此服務只做一件事情,就是發送訊息給 Google Andriod 或 Apple iOS 手機,啟動時預設跑在 8088 port,可以透過 Docker 或直接用 Command line 執行,對於 App 開發者,可以直接下載執行檔,在自己電腦端發送訊息給手機測試。藉由這次投稿順便發佈了新版本。底下來說明新版本多了哪些功能。

支援 RPC 協定

在此之前 Gorush 只有支援 REST API 方式,也就是透過 JSON 方式來通知伺服器來發送訊息,新版了多了 RPC 方式,這邊我是使用 gRPC 來實作,Gorush 預設是不啟動 gRPC 服務,你必須要透過參數方式才可以啟動,詳細可以參考此文件,底下是 Go 語言客戶端範例:
package main

import (
    "log"

    pb "github.com/appleboy/gorush/rpc/proto"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
)

const (
    address = "localhost:9000"
)

func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGorushClient(conn)

    r, err := c.Send(context.Background(), &pb.NotificationRequest{
        Platform: 2,
        Tokens:   []string{"1234567890"},
        Message:  "test message",
    })
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Success: %t\n", r.Success)
    log.Printf("Count: %d\n", r.Counts)
}

支援 ARM64 Docker 映像檔

目前已經支援 ARM64 的 Docker 版本,所以可以在 ARM64 板子內用 Docker 來執行,可以直接在 Docker Hub 找到相對應的標籤

支援全域變數

Gorush 本身支援 Yaml 設定檔,但是每次想要改設定,都要重新修改檔案,這不是很方便,所以我透過 Viper 套件來讓 Gorush 同時支援 Yaml 設定,或 Global 變數,也就是以後都可以透過變數方式來動態調整,有了這方式就可以讓 Docker 透過環境變數來設定。底下是範例讓開發者動態調整 HTTP 服務 Port。請注意所有變數的前置符號為 GORUSH_
$ GORUSH_CORE_PORT=8089 gorush

支援 Kubernetes

此版增加了 Kubernetes 設定方式,有了上述的全域變數支援,這時候設定 Kubernetes 就更方便了,請直接參考 k8s 目錄,詳細安裝步驟請參考此說明,底下是透過 ENV 動態設定 Gorush
env:
- name: GORUSH_STAT_ENGINE
  valueFrom:
    configMapKeyRef:
      name: gorush-config
      key: stat.engine
- name: GORUSH_STAT_REDIS_ADDR
  valueFrom:
    configMapKeyRef:
      name: gorush-config
      key: stat.redis.host

iOS 支援動態發送到開發或正式環境

在此之前發送訊息到 iOS 手機,都必須在啟動伺服器前將 iOS 環境設定好,現在可以動態調整 JSON 參數。
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 1,
      "message": "Hello World iOS!"
    }
  ]
}
可以加上 developmentproduction 布林參數,底下是將訊息傳給 iOS 開發伺服器
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 1,
      "development": true,
      "message": "Hello World iOS!"
    }
  ]
}

投影片

底下是議程投影片,有興趣的參考看看 最後有講到如何部署及測試 Go 語言,這邊講了一下 Drone 這套自動化測試工具。如果大家有興趣可以參考我在 Udemy 開設的課程,目前特價 1600 元。Drone 幫忙開發者自動化測試,部署到 Docker Hub 或編譯出執行檔,這些在 Drone 裡面都可以透過 YAML 來設定,開發者只需要專注於寫程式就可以了。

Go 語言實現 gRPC Health 驗證

$
0
0
grpc_square_reverse_4x 本篇教大家如何每隔一段時間驗證 gRPC 服務是否存活,如果想了解什麼是 gRPC 可以參考 這篇『REST 的另一個選擇:gRPC』,這邊就不多介紹 gRPC 了,未來將會是容器的時代, 那該如何檢查容器 Container 是否存活。如果是用 Kubernetes 呢?該如何來撰寫 gRPC 接口搭配 livenessProbe 設定。底下是在 Dockerfile 內可以設定 HEALTHCHECK 來 達到檢查容器是否存活。詳細說明可以參考此連結
HEALTHCHECK --interval=5m --timeout=3s \
  CMD curl -f http://localhost/ || exit 1

建立 Health Check proto 接口

打開您的 *.proto 檔案,並且寫入
message HealthCheckRequest {
  string service = 1;
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
  }
  ServingStatus status = 1;
}

service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}
存檔後重新產生 Go 程式碼: 檔案存放在 rpc/proto 目錄
$ protoc -I rpc/proto rpc/proto/gorush.proto --go_out=plugins=grpc:rpc/proto
或者在 Makefile 內驗證 proto 檔案是否變動才執行:
rpc/proto/gorush.pb.go: rpc/proto/gorush.proto
    protoc -I rpc/proto rpc/proto/gorush.proto \
    --go_out=plugins=grpc:rpc/proto

程式碼連結

建立 Health Interface

如果還有其他接口需要驗證,這就必須建立一個 Health Interface 讓你的服務可以驗證多種 protocol, 建立 health.go
package rpc

import (
    "context"
)

// Health defines a health-check connection.
type Health interface {
    // Check returns if server is healthy or not
    Check(c context.Context) (bool, error)
}

程式碼連結

建立 gRPC 服務

首先要定義一個 Server 結構來實現 Check 接口
type Server struct {
    mu sync.Mutex
    // statusMap stores the serving status of the services this Server monitors.
    statusMap map[string]proto.HealthCheckResponse_ServingStatus
}

// NewServer returns a new Server.
func NewServer() *Server {
    return &Server{
        statusMap: make(map[string]proto.HealthCheckResponse_ServingStatus),
    }
}
這邊可以看到,gRPC 的狀態可以從 proto 產生的 Go 檔案拿到,打開 *.pb.go,可以找到如下
type HealthCheckResponse_ServingStatus int32

const (
    HealthCheckResponse_UNKNOWN     HealthCheckResponse_ServingStatus = 0
    HealthCheckResponse_SERVING     HealthCheckResponse_ServingStatus = 1
    HealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2
)
接著來實現 Check 接口
// Check implements `service Health`.
func (s *Server) Check(ctx context.Context, in *proto.HealthCheckRequest) (*proto.HealthCheckResponse, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    if in.Service == "" {
        // check the server overall health status.
        return &proto.HealthCheckResponse{
            Status: proto.HealthCheckResponse_SERVING,
        }, nil
    }
    if status, ok := s.statusMap[in.Service]; ok {
        return &proto.HealthCheckResponse{
            Status: status,
        }, nil
    }
    return nil, status.Error(codes.NotFound, "unknown service")
}
上面可以看到透過帶入 proto.HealthCheckRequest 得到 gRPC 的回覆,這邊通常都是帶空值, gRPC 會自動回 1,最後在啟動 gRPC 服務前把 Health Service 註冊上去
    s := grpc.NewServer()
    srv := NewServer()
    proto.RegisterHealthServer(s, srv)
    // Register reflection service on gRPC server.
    reflection.Register(s)
這樣大致上完成了 gRPC 伺服器端實作

程式碼連結

建立 Client 套件

一樣可以透過 proto 產生的程式碼來撰寫 Client 驗證,建立 client.go 裡面寫入
package rpc

import (
    "context"

    "github.com/appleboy/gorush/rpc/proto"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
)

// generate protobuffs
//   protoc --go_out=plugins=grpc,import_path=proto:. *.proto

type healthClient struct {
    client proto.HealthClient
    conn   *grpc.ClientConn
}

// NewGrpcHealthClient returns a new grpc Client.
func NewGrpcHealthClient(conn *grpc.ClientConn) Health {
    client := new(healthClient)
    client.client = proto.NewHealthClient(conn)
    client.conn = conn
    return client
}

func (c *healthClient) Close() error {
    return c.conn.Close()
}

func (c *healthClient) Check(ctx context.Context) (bool, error) {
    var res *proto.HealthCheckResponse
    var err error
    req := new(proto.HealthCheckRequest)

    res, err = c.client.Check(ctx, req)
    if err == nil {
        if res.GetStatus() == proto.HealthCheckResponse_SERVING {
            return true, nil
        }
        return false, nil
    }
    switch grpc.Code(err) {
    case
        codes.Aborted,
        codes.DataLoss,
        codes.DeadlineExceeded,
        codes.Internal,
        codes.Unavailable:
        // non-fatal errors
    default:
        return false, err
    }

    return false, err
}

程式碼連結

驗證 gRPC 服務是否存活

上述 Client 寫好後,其他開發者可以直接 import 此 package,就可以直接使用。再建立 一個檔案取名叫 check.go
package main

import (
    "context"
    "log"
    "time"

    "github.com/go-training/grpc-health-check/rpc"

    "google.golang.org/grpc"
)

const (
    address = "localhost:9000"
)

func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()

    client := rpc.NewGrpcHealthClient(conn)

    for {
        ok, err := client.Check(context.Background())
        if !ok || err != nil {
            log.Printf("can't connect grpc server: %v, code: %v\n", err, grpc.Code(err))
        } else {
            log.Println("connect the grpc server successfully")
        }

        <-time.After(time.Second)
    }
}

程式碼連結

結論

所有程式碼都可以在這邊找到,假設團隊的 gRPC 服務跟 Web 服務器 綁在同一個 Go 程式的話,可以透過撰寫 /healthz 來同時處理 gRPC 及 Http 服務的驗證。在 Kubernetes 內就可以透過設定 livenessProbe 來驗證 Container 是否存活。
livenessProbe:
  httpGet:
    path: /healthz
    port: 3000
  initialDelaySeconds: 3
  periodSeconds: 3

從 Nginx 換到 Caddy

$
0
0
The_Caddy_web_server_logo.svg 終於下定決心將 Nginx 換到 Caddy 這套用 Go 語言所撰寫的開源套件,大家一定會有所疑問『為什麼要換掉 Nginx 而改用 Caddy』,原因其實很簡單,你現在看的 Blog 安裝在 Linode 機器上面,之前跑的是 Nginx 搭配 letsencrypt,但是必須要寫一個 Scripts 來自動更新 letsencrypt 憑證,這機制最後不太運作,加上這一年來,每三個月就會有人丟我說『你的 Blog 憑證過期了』,所以就在這時間點,花點時間把 Nginx 設定調整到 Caddy,轉換的時間不會花超過一小時喔。

轉換 WordPress

目前自己只有三個服務,其中一項就是現在的 Blog 是用 WordPress 架出來的,先來看看原先 Nginx 設定
server {
  # don't forget to tell on which port this server listens
  listen 80;

  # listen on the www host
  server_name blog.wu-boy.com;

  location /.well-known/acme-challenge/ {
    alias /var/www/dehydrated/;
  }

  # and redirect to the non-www host (declared below)
  return 301 https://blog.wu-boy.com$request_uri;
}

server {
  listen 0.0.0.0:443 ssl http2;

  location /.well-known/acme-challenge/ {
    alias /var/www/dehydrated/;
  }

  ssl_certificate /etc/dehydrated/certs/blog.wu-boy.com/fullchain.pem;
  ssl_certificate_key /etc/dehydrated/certs/blog.wu-boy.com/privkey.pem;

  # The host name to respond to
  server_name blog.wu-boy.com;

  # Path for static files
  root /home/www/blog/www;
  index index.html index.htm index.php;
  access_log /home/www/blog/log/access.log;
  error_log /home/www/blog/log/error.log;

  # Specify a charset
  charset utf-8;
  # disable autoindex
  autoindex off;

  # Deliver 404 instead of 403 "Forbidden"
  error_page 403 /403.html;
  # Custom 404 page
  error_page 404 /404.html;

  # stop image Hotlinking
  # ref: http://nginx.org/en/docs/http/ngx_http_referer_module.html
  location ~ .(gif|png|jpe?g)$ {
     valid_referers none blocked server_names;
     if ($invalid_referer) {
        return 403;
    }
  }

  location = / {
    index index.html index.html index.php;
  }

  include h5bp/basic.conf;
  include h5bp/module/wordpress.conf;
看到都頭昏眼花了。這還沒有附上 wordpress.conf 的設定呢。接著我們看一下 Caddy 的設定
blog.wu-boy.com {
  root /home/www/blog/www
  gzip
  fastcgi / unix:/var/run/php5-fpm.sock php
  rewrite {
    if {path} not_match ^\/wp-admin
    to {path} {path}/ /index.php?{query}
  }
}
有沒有差異很大,Caddy 強調的就是簡單,而且會自動更新網站憑證。

轉換 CodeIgniter

CodeIgniterLaravel 架構一樣,所以設定方式也差不多
codeigniter.org.tw {
  root /home/www/ci/www
  gzip
  fastcgi / unix:/var/run/php5-fpm.sock php
  rewrite {
    if {path} not_match ^\/(forum|user_guide|userguide3)
    to {path} {path}/ /index.php?{query}
  }
}

透過 Proxy 設定其他服務

如果你有啟動其他服務,可以透過 proxy 方式來設定
xxxx.wu-boy.com {
  proxy / localhost:8081 {
    websocket
    transparent
  }
}

感想

原本一直想找機會來處理憑證無法三個月自動更新,但是沒什麼時間處理,後來花短短半小時時間就把 Caddy 處理完成,並且設定在 Ubuntu 內開機自動啟動。也許大家可以嘗試看看 Caddy,如果最後真的效能瓶頸在 Caddy,也可以隨時抽換掉。想看更多範例,可以直接參考此範例連結

Drone Secret 安全性管理

$
0
0
drone-logo_512 Drone 是一套以 Docker 容器技術為主的 CI/CD 開源專案,本篇來聊聊 Drone 如何管理專案內的 Secret 資料,首先先來定義什麼是 Secret,舉個簡單例子,如果你是透過 Drone 來完成基本打包+上傳到遠端伺服器,那一定會需要用到兩個 Plugin,就是 drone-scpdrone-ssh,而使用這兩個 plugin 需要有一組 Password 或是一把金鑰 (Public Key Authentication),在 Drone 裡面可以透過後台 UI 介面將密碼或者是金鑰內容儲存在 Secret 設定頁面。預覽圖如下: Screen Shot 2017-11-20 at 9.10.10 AM

安全性?

如果透過 Drone 後台新增的 Secret 資料都不是很安全,因為每個 Steps 都可以直接存取 Secret 資料。
pipeline:
  test1:
    image: mhart/alpine-node:9.1.0
    group: testing
    secrets: [ test46 ]
    commands:
      - echo "node.js"
      - echo $TEST46

  test2:
    image: appleboy/golang-testing
    group: testing
    secrets: [ test46, readme ]
    commands:
      - echo "golang"
      - echo $TEST46
      - echo $README
從上面可以得知,透過 appleboy/golang-testingmhart/alpine-node:9.1.0 都可以存取 test46 變數,這樣哪裡不安全?答案是,假設今天服務的是開源專案,這樣別人是不是可以發個 PR,內容新增一個步驟,將變數內容直接印出來即可。當然你可以把 Drone 的頁面關閉,只有管理者可以存取,但是這樣就失去開源專案的意義,因為貢獻者總該需要看到哪裡編譯錯誤,或者是測試失敗的地方。

透過 drone cli 管理

要解決安全性問題,必須要將 Secret 變數綁定只有特定 Image 才可以存取。而要做到此功能只能透過 drone cli 工具才可以完成。該如何使用 drone secret 指令呢?其實不會很難,drone cli 可以做的比 Web UI 還強大。所以關於 Secret 部分,我幾乎都是用 cli 來管理
$ drone secret -h
NAME:
   drone secret - manage secrets

USAGE:
   drone secret command [command options] [arguments...]

COMMANDS:
     add     adds a secret
     rm      remove a secret
     update  update a secret
     info    display secret info
     ls      list secrets

OPTIONS:
   --help, -h  show help
先假設 ssh-password 變數需要綁定在 appleboy/drone-ssh 映像檔上面,該如何下指令:
$ drone secret add \
  --name ssh-password \
  --value 1234567890 \
  --image appleboy/drone-ssh \
  --repository go-training/drone-workshop
上述例子可以用在存密碼欄位,如果是想存檔案類型呢?也就是把金鑰 public.pem 給存進變數。這邊可以透過 @檔案路徑 的方式來存取該檔案,並且直接寫入到 Drone 資料庫。注意只要是 @ 開頭,後面就必須接實體檔案路徑。
$ drone secret add \
  --name ssh-key \
  --value @/etc/server.pem \
  --image appleboy/drone-ssh \
  --repository go-training/drone-workshop

心得

將 Drone 安裝在公司內部,又想要防止團隊成員直接拿到 Pem 資料,就必須透過 drone cli 工具來達成此功能,否則當同事可以輕易拿到這把 Key 時,就可以隨時登入機器惡搞。如果你拿 Drone 管理開源專案,更是要這麼做了。上述教學,我已經錄製成影片檔放在 Udemy 線上課程,如果已經購買的朋友們,可以直接看線上教學。

用 Go 語言減少 node_modules 容量來加速部署

$
0
0
Go-brown-side.sh 之前寫過一篇『減少 node_modules 大小來加速部署 Node.js 專案』文章,透過 Yarn 指令可以移除不必要的模組,剩下的模組佔據整個專案大部分容量,那該如何針對留下的模組來瘦身呢?這週看到 Node.js 大神 TJ 又發了一個 Go 語言專案叫做 node-prune,此專案用來移除在 node_modules 內不必要的檔案,那哪些才是不必要的檔案呢?

預設刪除列表

底下是 node-prune 預設刪除的檔案列表
var DefaultFiles = []string{
    "Makefile",
    "Gulpfile.js",
    "Gruntfile.js",
    ".DS_Store",
    ".tern-project",
    ".gitattributes",
    ".editorconfig",
    ".eslintrc",
    ".jshintrc",
    ".flowconfig",
    ".documentup.json",
    ".yarn-metadata.json",
    ".travis.yml",
    "LICENSE.txt",
    "LICENSE",
    "AUTHORS",
    "CONTRIBUTORS",
    ".yarn-integrity",
}
預設刪除目錄
var DefaultDirectories = []string{
    "__tests__",
    "test",
    "tests",
    "powered-test",
    "docs",
    "doc",
    ".idea",
    ".vscode",
    "website",
    "images",
    "assets",
    "example",
    "examples",
    "coverage",
    ".nyc_output",
}
預設刪除的副檔名
var DefaultExtensions = []string{
    ".md",
    ".ts",
    ".jst",
    ".coffee",
    ".tgz",
    ".swp",
}
作者也非常好心,開了 WithDir, WithExtensions … 等接口讓開發者可以動態調整名單。其實這專案不只可以用在 Node.js 專案,也可以用在 PHP 或者是 Go 專案上。

產生執行檔

對於非 Go 的開發者來說,要使用此套件還需要額外安裝 Go 語言環境才可以正常編譯出 Binary 檔案,也看到有人提問無法搞定 Go 語言環境,所以我直接 Fork 此專案,再搭配 Drone CI 方式輕易產生各種環境的 Binary 檔案,現在只有產生 Windows, Linux 及 MacOS。大家可以從底下連結直接下載來試試看。下載完成,放到 /usr/local/bin 底下即可。使用方式很簡單,到專案底下直接執行 node-prune
$ node-prune

         files total 16,674
       files removed 4,452
        size removed 18 MB
            duration 1.547s

下載連結


GitHub Flow 及 Git Flow 流程使用時機

$
0
0
Screen Shot 2017-12-20 at 11.45.04 AM 在 Facebook 上面看到這篇『git flow 實戰經驗談』,想說來寫一下對於團隊內該導入 GitHub Flow 還是 Git Flow,寫下自己的想法給大家參考看看。當你加入團隊,第一件事情就是嘗試了解目前團隊是走哪一種 Git 流程,但是在團隊內可能使用 GitHub 流程或者是傳統 Git 流程,在開始進入開發流程時,請務必先了解團隊整個 Release 流程。後者流程在筆者幾年前有發表一篇『branch model 分支模組基本介紹』,如果大家有興趣可以先看看,而我自己在團隊內使用這兩種流程,嘗試過幾個團隊,得到底下結論: 底下來探討為什麼我會有這些想法。首先先來看看公司團隊內部如果是走 Git 流程會有哪些缺陷。

學習困難

Screen Shot 2017-12-19 at 11.10.58 AM 很多開發者對於 branch 分支不是很了解,也常常下錯指令,首先在 Git Flow 內,需要先了解什麼時候在 develop 開分支,什麼時候該對 release 開分支。另外什麼時候該將 commit 拉到 develop,什麼時候該拉到 release 內。這些種種環境都圍繞在團隊內部,如果你不知道該將目前的分支 merge 到正確的地方,這會是團隊開發速度的瓶頸,尤其是在處理釋出下一版功能情況下。多個分支只會造成大家對於軟體流程的困擾,也讓剛加入團隊的新人需要一段時間去適應,團隊內也需要一位開發者去輔導大家,那何不導入簡易的 GitHub Flow 來改善這問題呢?

管理不易

多個分支造成大家不小心拉了不對的 commit 進到任何分支,這邊要如何避免此狀況,那就只能用 Protected 分支方式,讓團隊全部成員都需要發 Pull Request 狀態下才可以將修改的內容合併到正確分支,但是這也不是一個很好的解法,今天假設要釋出軟體 v1.0.0 版本時,會將 develop 的全部 commit 都合併到 release 分支,這邊有兩種做法,一種是不打 Tag,也就是 CI/CD 服務是監聽 release 分支,只要 release 有變動,CI/CD 服務就開始部署到 Production,另一種是在 Release 分支上下 Tag,透過 CI/CD 來監聽 Tag 事件,這兩種我都有看過團隊使用。但是前者的缺陷是,如果沒走 Tag 的話,你怎麼知道現在 Production 機器上面是哪一個版本,以及該如何知道此版跟上一版本的差異在哪邊。而後者雖然解決了版本差異的問題,但是 Tag 基本上不該限制只能在單一特定分支。

如何解決

Screen Shot 2017-12-20 at 11.40.21 AM 為了解決上述兩大問題,我建議在公司團隊內使用 GitHub Flow 來減少流程步驟,讓工程師可以更專心在開發上面,而不是花更多時間在 Git 分支操作上面。GitHub Flow 只需要記住主分支 master 其他分支都是從主分支在開出來,所以新人很容易理解,不管是解 Issue 還是開發新功能,都是以 master 分支為基底來建立新的分支,開發團隊也只需要懂到這邊就可以了。接下來 Deploy 到 Production 則是透過 Tag 方式來解決。由開發團隊主管來下 Tag,這樣可以避免團隊內部成員不小心合併分支造成 Deploy 到正式環境的錯誤狀況。另外大家會遇到上線後,如何緊急上 Patch 並且發佈下一個版本,底下是最簡單的操作步驟。
# 抓取遠端所有 tag
$ git fetch -t origin

# 從上一版本建立 branch (0.2.4 代表上一個版本)
$ git checkout -b patch-1 origin/0.2.4

# 把修正個 commit 抓到 patch-1 branch
$ git cherry-pick commit_id

# 打上新的 Tag 觸發 Deploy 流程
$ git tag 0.2.5
$ git push origin 0.2.5

# 將 patch 也同步到 master 分支
$ git checkout master
$ git cherry-pick commit_id
$ git push origin master
有沒有覺得跟 Git Flow 流程差異很多,大家只需要記住兩件事情,第一是專案內只會有 master 分支需要受到保護。第二是部署流程一律走 Tag 事件,這樣可以避免工程師不小心 Merge commit 造成提前部署,因為平常開發過程,不會有人隨便下 Git Tag,所以只要跟團隊同步好,Git Tag 都由團隊特定人士才可以執行即可。底下附上團隊內的流程: Screen Shot 2017-12-20 at 11.36.30 AM

開源專案

為什麼開源專案需要走 Github Flow + Git Flow 呢?原因其實很簡單,假設現在要釋出 1.0.0 版本,那肯定會從 master 上單一節點去下 tag 標記為 1.0.0,這沒問題,這版本釋出之後,CI/CD 服務會自動依照 Tag 事件來自動化部署軟體到 GitHub Release 頁面。但是軟體哪有沒 bug 的,一但發現 Bug,這時候想發 Pull Request 回饋給開源專案,會發現只能針對 master 發 Pull Request,該專案團隊這時候就需要在下完 Tag 同時建立 release/v1.0 分支,方便其他人在發 PR 時,在 review 完成後合併到 master 內,接著團隊會告知這 PR 需要被放到 release/v1.0 內方便釋出下一個版本 v1.0.1,所以我才會下這個結論,一個好的開源專案是需要兩個 Flow 同時使用。而在開源專案上的好處是,你不用擔心別人不會 Git 流程或指令。基本上不會用 Git 的開發者,也不會發 Pull Request 了。

結論

兩者流程各有優缺點,在選擇流程時,請務必考量 CI/CD 的串接方式,以及團隊成員的狀況來決定流程,而這篇最主要提出我在公司團隊內,以及在開源專案上看到的流程。最終都是找到一個最佳流程來讓專案執行的更順利,希望此篇能對要導入 Git 服務的朋友們有點幫助。歡迎大家隨時留言討論。

問與答

整理朋友提出來的一些疑問,歡迎大家參考看看。

Q1: 選擇 GitHub flow 都是因為怕自動部署造成錯誤。如果在部署到 production 前,都先部署到測試環境,還會有這樣的問題嗎?

其實我最主要不是怕自動部署造成錯誤,反而是我帶人的時候,不管是不是資深的工程師都有這問題,不熟悉 Git 整個流程,需要依賴 SourceTree 這工具,然而這工具真的害死一堆剛入門的朋友,不好好學 command,一開始就碰 SourceTree,你根本不知道 SourceTree 在背景做了哪些事情。至於部署流程,這牽扯到跟 CI/CD 相關,我個人覺得只要權限設定對,把可以 release 產品的開發者都設定好,理論上不會出什麼錯誤才是。而我用 Tag 的原因是方便記錄版本差異。當然先部署到 Staging 上面測試,這是必經流程,沒人可以在還沒測試過的狀態下,部署到正式環境。

在本機端導入 Drone CLI 做專案測試

$
0
0
drone-logo_512 Drone 是一套用 Go 語言所撰寫的 CI/CD 開源專案,透過 .drone.yml 檔案方式讓開發者可以自行撰寫測試及部署流程。大家一定會認為要先架設好 Drone 伺服器,才能透過 Git Push 方式來達到自動化測試及部署專案。現在跟大家介紹,如果你的團隊尚未架設 Drone 服務,但是又想要使用 Drone 透過 Yaml 方式所帶來的好處,很簡單,你只需要透過 Drone CLI 工具就可以完成,不需要架設任何一台 Drone 服務,只要學會 Yaml 方式如何撰寫,就可以透過 drone exec 指令來完成。好處是寫完 .drone.yml 檔案,未來圖隊如果正式架設了 Drone 服務,就可以無痛升級,沒有的話,也可以透過 CLI 工具在公司專案內單獨使用,這比寫 docker-compose.yml 方式還要快很多。本篇會介紹使用 drone exec 的小技巧。

安裝 drone cli

請直到官方下載頁面下載相對應檔案,完成後請放到 /usr/local/bin 底下,目前支援 Windows, Linnx 及 MacOS。如果開發環境有 Go 語言,可以直接透過底下指令安裝
$ go get -u github.com/drone/drone-cli/drone
或是透過 tarbal 方式安裝
curl -L https://github.com/drone/drone-cli/releases/download/v0.8.0/drone_linux_amd64.tar.gz | tar zx
sudo install -t /usr/local/bin drone

撰寫 Yaml 檔案

用編輯器打開專案,並且初始化 .drone.yml 檔案
pipeline:
  backend:
    image: golang
    commands:
      - echo "backend testing"

  frontend:
    image: golang
    commands:
      - echo "frontend testing"
在命令列直接下 drone exec 畫面如下
[backend:L0:0s] + echo "backend testing"
[backend:L1:0s] backend testing
[frontend:L0:0s] + echo "frontend testing"
[frontend:L1:0s] frontend testing
可以發現今天就算沒有 drone server 團隊依然可以透過 drone exec 來完成測試。

使用 secret

在 drone 測試會需要使用 secret 來保存類似像 AWS API Key 隱秘資訊,但是這只能在 Drone server 上面跑才會自動帶入 secret。
pipeline:
  backend:
    image: golang
    secrets: [ test ]
    commands:
      - echo "backend testing"
      - echo $TEST

  frontend:
    image: golang
    commands:
      - echo "frontend testing"
執行 drone exec 後會發現結果如下
$ drone exec
[backend:L0:0s] + echo "backend testing"
[backend:L1:0s] backend testing
[backend:L2:0s] + echo $TEST
[backend:L3:0s]
[frontend:L0:0s] + echo "frontend testing"
[frontend:L1:0s] frontend testing
可以得知 $TEST 輸出是沒有任何資料,但是如果在 Drone server 上面跑是有資料的。那該如何在個人電腦也拿到此資料呢?其實很簡單,透過環境變數即可
$ TEST=appleboy drone exec
[backend:L0:0s] + echo "backend testing"
[backend:L1:0s] backend testing
[backend:L2:0s] + echo $TEST
[backend:L3:0s] appleboy
[frontend:L0:0s] + echo "frontend testing"
[frontend:L1:0s] frontend testing
這樣我們就可以正確拿到 secret 資料了。

忽略特定步驟

已經導入 drone 的團隊,一定會把很多部署的步驟都放在 .drone.yml 檔案內,但是在本機端只想跑前後端測試,後面的像是 Notification,或者是 SCP 及 SSH 步驟都需要忽略,這樣可以單純只跑測試,這時候該透過什麼方式才可以避免呢?很簡單只要在 when 條件子句加上 local: false 即可。假設原本 Yaml 寫法如下:
pipeline:
  backend:
    image: golang
    commands:
      - echo "backend testing"

  frontend:
    image: golang
    commands:
      - echo "frontend testing"

  deploy:
    image: golang
    commands:
      - echo "deploy"
這次我們想忽略掉 deploy 步驟,請改寫如下
pipeline:
  backend:
    image: golang
    commands:
      - echo "backend testing"

  frontend:
    image: golang
    commands:
      - echo "frontend testing"

  deploy:
    image: golang
    commands:
      - echo "deploy"
    when:
      local: false
再執行 drone exec,大家可以發現,最後一個步驟 deploy 就被忽略不執行了,這在本機端測試非常有用,也不會影響到 drone server 上的執行。大家可以參考此 Yaml 檔案範例,大量使用了 local: false 方式。

心得

我把本篇教學也錄製好放到 Udemy 免費讓大家觀看,在 Drone Command Line 介紹章節內,如果大家有興趣想學其他部分,也歡迎購買線上課程。本篇很適合想入門 Drone 但是又還沒導入 Drone 的團隊。可以透過 drone exec 方式來測試看看 drone 的優勢及好處,並且可以取代 docker-compose 無法做到的平行處理喔。

Udemy 課程特價 $1600 只到 2017/12/31 日喔

Caddy 搭配 Harbor 自架私有 Docker Registry

$
0
0
docker Harbor 是由 VMWare 公司用 Go 語言所開發的開源軟體,它可以讓團隊存放各種不同的私有 Docker 映像檔,假如團隊內沒考慮 AWS 的 ECR 或者是 Google 提供的 GCR 方案,建議您可以參考看看 Harbor,而 Harbor 提供了簡易的 UI 介面,包含權限控管,及跨區域的自動同步功能,比起自己從官網把 Docker Registry 架起來,功能多上不少。本篇不會教大家如何架設 Harbor,有興趣的可以直接參考官方文件,此篇會紀錄如何透過 Caddy 將憑證用在 Harbor 內部。

問題?

我本來把 Harbor 架設在 AWS EC2 上面,而剛開始是採用 http 並非使用 https,這在搭配 Kubernetes 會有個問題,因為假設使用 http 的話,Docker 預設是不吃 http 的,所以必須要在 k8s 每一個 Node 機器內補上下面設定
# open /etc/docker/daemon.json
{
  "debug" : true,
  "insecure-registries" : [
    "harbor.xxxx.com"
  ]
}
如果在個人電腦上面 (Mac) 則是需要到底下 Docker 設定頁面補上 register 資訊 Screen Shot 2018-01-02 at 11.20.27 PM 如果今天在 k8s 內需要自動擴展一台新的 EC2 機器,你會發現在這台 EC2 機器內是抓不到任何 Image 檔案,所以必須要讓 Harbor 支援 https 才能解決掉此問題。

解決方式

在 Harbor 內可以參考此份文件將憑證檔案放到 Docker 內部。假設今天沒有憑證,其實可以透過 Caddy 方式來拿到憑證放到 Dokcer 內部。第一步先找到 Caddy 存放路徑,一般來說是放在 ~/.caddy/ 目錄,接著透過 link 方式放到 /data 目錄 (/data 是 Harbor 預設放在 Host 的目錄)
ln -sf ~/.caddy/acme/acme-v01.api.letsencrypt.org/sites/your_domain.com/harbor.wu-boy.com.key /data/cert/server.key
ln -sf ~/.caddy/acme/acme-v01.api.letsencrypt.org/sites/your_domain.com/harbor.wu-boy.com.cert /data/cert/server.cert
接著打開 harbor.cfgui_url_protocol 設定為 https
ui_url_protocol = https
重新啟動 harbor
$ ./prepare
$ docker-compose down
$ docker-compose up -d

設定 Caddy

這邊不確定是不是 Harbor 的 bug,理論上如果在 Harbor 內跑 http,只要把 Caddy 設定好 proxy 理論上要可以通,但是實際上就是不行,必須要在 harbor 跑 https 然後 Caddy 也跑 https 才行
your_domain.com {
  log stdout
  proxy / https://your_domain.com:8089 {
    websocket
    transparent
  }
}
其中 8089 就是對應到 harbor 容器內的 443 port。這樣還不夠,你必須要在 /etc/hosts 底下補上
127.0.0.1 your_domain.com
這樣才可以正確讓 Caddy + Harbor 正式跑起來,並且三個月自動更換憑證。

心得

沒有時間去研究 Harbor 底層為什麼會出現這問題,有時間的話再來研究看看。可能是在 harbor 包的 Nginx 容器設定有些問題。

DigitalOcean 2018 年調整價格

$
0
0
DO_Logo_Vertical_Blue-6321464d 很高興看到 DigitalOcean 在 2018 年推出新的 VPS 價錢方案,可以從下面這張圖看出來,記憶體幾乎都調整為兩倍方案,這已經完全追上 Linode 現在的價格了,另外 DigitalOcean 還額外推出每個月 $15 美金方案,還可以動態選擇要高 CPU (1~3) 還是高記憶體 (1G ~ 3G) 由玩家自由搭配,這方案真的是太棒了。

DigitalOcean 新價格

由下圖可以看到連 SSD 容量也都提升了 1.x ~ 2 倍,這數字已經超越 Linode 給的容量了。未來 DigitalOcean 也將會推出以秒計費,而不是以小時計費。 Snip20180117_1 對照看看 Linode 現在的價格 Snip20180117_5

心得

這次看起來 Do 打算跟 Linode 平行對齊,並且把 SSD 容量給提高超越了 Linode,推出新的 $15/mo 方案讓玩家可以選擇高記憶體或高 CPU 方案,來等等看 Linode 看到這方案後會怎麼調整自家方案。看到 Do 推出新方案,現在用 Linode 的原因只剩下地點可以選日本之外,其他部分好像都沒有比 Do 好了。

Drone CI/CD 系統簡介

$
0
0
Screen Shot 2018-01-18 at 10.21.43 AM 很高興到 GCPUG.TW 分享『Drone CI/CD 系統簡介』,會議介紹了 Drone 系統架構,這套是由 Go 語言所開發,前兩年我參加了 Drone 開源專案的開發,也貢獻了數個 Drone Plugin,去年我正式開始宣傳 Drone 的好處及優勢,以及為什麼要從 JenkinsGitLab CI 轉換到 Drone,會議大綱如下,很感謝 QNAP 提供現場直播及錄影。
  • Why I don’t choose Jenkins or GitLab CI?
  • What is Drone CI?
  • Drone Infrastructure
  • How to install Drone in five minutes?
  • Integrate your project
  • Create your Drone plugin
  • Try drone CLI without drone server

GCPUG 聚會影片

下面是在 GCPUG 分享的影片

DevOps 線上讀書會影片

下面是在 DevOps 線上讀書會分享的影片,此內容包含了線上操作

投影片

Viewing all 325 articles
Browse latest View live