Kosko – 用 JavaScript 管理 Kubernetes

敝社從 2016 年就開始 Kubernetes,應該能算是相當早期的使用者了,也因此我們累積了一堆的 Kubernetes YAML 設定檔,從某個時間開始 staging 和 production 環境的設定檔更開始分裂,自此以來一直無法合併。因此這次的目標就是:

  • 整合各環境的設定
  • 能夠重複利用
  • 能夠驗證設定是否正確

現有工具

一開始我先從 awesome-kubernetes 尋找現有的設定管理工具,以下列出一些我覺得還不錯的工具以及它們的優缺點。

kustomize

  • 👍 屬於 sig-cli 的專案,應該能夠保障更新活躍且不會突然棄坑。
  • 👍 學習成本低,使用熟悉的 YAML。
  • 👍 用 Overlay 的概念讓不同環境的參數去 patch 基礎設定檔。
  • 👎 沒有驗證設定的功能。

ksonnet

  • 👍 彈性極高,能透過變數及函數共享設定。
  • 👍 支援 Helm
  • 👍 能夠驗證設定。
  • 👎 使用比較少見的 jsonnet,需要另外花時間學習,而且資源也比較少。
  • 😢 已停止維護

kapitan

  • 👍 能夠管理 secrets。
  • 👍 能夠產生文件、Terraform 設定以及一些 scripts。
  • 👍 用 Inventory 的概念管理不同環境和共享設定。
  • 👎 使用 jsonnet,理由同上。
  • 👎 使用 jinja2 做為 template engine,我不太能夠理解既然都用上 jsonnet 的話那為何還需要用 templates?

造輪子

因為現有工具對我來說都有些不足的地方,所以我最後決定根據 ksonnet 的概念,並稍微調整一些部份讓我用起來更順手一些:

  • 改用 JavaScript,因為資源豐富而且大家都會用。
  • 不支援 Helm,因為我們沒在用。
  • 只負責產生和驗證 YAML,完全不和 Kubernetes cluster 接觸。

相較於 ksonnet 來說砍了很多功能,所以實際上實作並沒有花太多時間,麻煩的是把現有的上百個 YAML 檔轉換成 JavaScript、整合 staging 和 production 環境並實際在 Kubernetes 上測試,大約花了 5 週才完成所有工作,最後的結果非常可觀。

kosko

安裝

1
npm install kosko -g

初始化

1
2
3
kosko init example
cd example
npm install

產生 YAML

1
2
3
4
5
# 輸出到 console
kosko generate

# Apply 到 Kubernetes cluster 上
kosko generate | kubectl apply -f -

驗證 YAML

其實在執行 kosko generate 時也會順帶驗證,這個指令只是用來方便在 CI 上跑測試時不會把設定輸出到 log。

1
kosko validate

轉移現有的 YAML

1
2
3
4
5
# 單一檔案
kosko migrate -f nginx-deployment.yml

# 資料夾
kosko migrate -f nginx

資料夾結構

預設的資料夾結構參考 ksonnetcomponents 資料夾用來放 manifests,environments 則是各環境的參數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── components
│ ├── nginx.js
│ └── postgres.js
├── environments
│ ├── staging
│ │ ├── index.js
│ │ ├── nginx.js
│ │ └── postgres.js
│ └── production
│ ├── index.js
│ ├── nginx.js
│ └── postgres.js
├── kosko.toml
└── templates

但實際使用時發現這種結構在 components 過多時使用起來必須要在 componentsenvironments 兩個資料夾來回,不太方便,所以最後加上了自訂路徑的功能。

1
2
[paths.environment]
component = "components/#{component}/#{environment}"

上述的設定改變了 component environments 的檔案路徑,變成了下列的結構:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── components
│ ├── nginx
│ │ ├── index.js
│ │ ├── staging.js
│ │ └── production.js
│ └── postgres
│ ├── index.js
│ ├── staging.js
│ └── production.js
├── environments
│ ├── staging.js
│ └── production.js
├── kosko.toml
└── templates

kubernetes-models-ts

為了能夠驗證設定是否符合 schema,我根據 Kubernetes 的 OpenAPI specification 產生了相對應的 TypeScript。不僅能夠在編譯時找出一些基本的型別錯誤,即使沒有使用 TypeScript 也能透過 JSON schema 驗證設定。

下面列出一些開發時遇到的問題:

JSON 沒有 undefined

JSON 實際上是沒有 undefined 型別的,雖然 JSON.stringify 會直接忽略,但是 js-yaml 卻不會,所以我必須在 toJSON() 函數裡刪除所有 undefined 的欄位。

int-or-string

在 Kubernetes 裡有一種特殊型別叫做 int-or-string,雖然在 JSON schema 是 string,但在 TypeScript 必須轉為 string | number,不然編譯器常會報錯。舉例來說,Service 中的 targetPort 就是常見的情況,它同時可以是 port number (int) 或 named port (string)。

1
2
3
4
5
new Service({
spec: {
ports: [{ port: 80, targetPort: 80 }]
}
});

編輯器支援

最後炫耀一下,在支援 TypeScript 的編輯器裡寫設定有多爽 😎

結語

一開始其實是打算用 ksonnet 的,但是必須要另外學 jsonnet 很麻煩。開始造輪子大約一個月後發現 ksonnet 竟然停止維護了,不禁感嘆幸好當初選擇了自己造輪子?

其他使用 Kubernetes 的大大們可能也會遇到設定管理的問題,不知道各位是怎麼解決的?是使用官方的 kustomize?還是也自己開發工具?又是如何管理 secrets 呢?如果可以的話,希望能互相交流。

在 Monorepo 裡用 TypeScript

最近在開發公司內部使用的工具時,心血來潮想用 Lerna 來管理 monorepo,但是又想用 TypeScript,結果碰到了一些編譯上的問題,例如套件之間互相依賴時,TypeScript 不知道依賴關係而無法了解編譯順序,導致整個 monorepo 無法編譯成功。

之後發現了 Quramy/lerna-yarn-workspaces-example 這個範例,裡頭用了 Yarn、Lerna 和 TypeScript,在這之中 Yarn 並不是必須的可以忽視,重點是在 TypeScript 3.0 推出的 Project References,這個功能讓 TypeScript 能夠知道各個模組之間的依賴關係,因此自動解決了編譯順序的問題。

要使用 Project References 的話必須在 tsconfig.json 加上下列選項。

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"composite": true,
"declaration": true,
"rootDir": "src",
"outDir": "dist"
}
}
  • composite - 讓 TypeScript 能夠快速找到被引用專案的位置。
  • declaration - 編譯定義檔 (.d.ts)。
  • rootDir - 設定專案的根目錄,預設是 tsconfig.json 的所屬資料夾。
  • outDir - 編譯的輸出路徑。

並在要引用的地方加上 references

1
2
3
4
5
{
"references": [
{ "path": "../x-core" }
]
}

然後在執行 tsc 時加上 -b 選項以及要編譯的專案路徑,就能順利編譯。

1
tsc -b packages/x-core packages/x-cli

因為專案比較多,所以我另外寫了一個 script 自動找出所有需要編譯的專案路徑。

build.jsSource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"use strict";

const spawn = require("cross-spawn");
const globby = require("globby");
const { dirname } = require("path");

const TSC = "tsc";

const pkgs = globby.sync("packages/*/tsconfig.json").map(dirname);
const args = ["-b", ...pkgs, ...process.argv.slice(2)];

console.log(TSC, ...args);

spawn.sync(TSC, args, {
stdio: "inherit"
});

執行 tsc 時加上 --watch 就能夠監看檔案變化並自動重新編譯。

1
tsc -b packages/x-core packages/x-cli --watch

執行 tsc 時加上 --clean 則是能夠自動根據 outDir 設定清除編譯後的檔案。

1
tsc -b packages/x-core packages/x-cli --clean

可以在 tommy351/kosko 看到實際運用的範例。

用 Elixir 和 hackney 做 proxy

前幾個月改版時,我決定用 Elixir 來實作 OAuth server + Proxy,這是一門結合了 Erlang VM 和 Ruby 語法的程式語言,可以很容易運用 Erlang 的特性做出低延遲、高並發且高容錯度的系統,又不用學習 Erlang 比較特殊的 Prolog 式語法(但是你可能還是多少要懂 Erlang 語法,因為很多時候你會直接運用 Erlang library)。

Erlang 的這些強大特性拿來做 OAuth server + Proxy 似乎有些大材小用,不過因為我爽,所以就決定用 Elixir 來寫了。

Read More

使用 Ansible 管理 Google Compute Engine

最近忙著佈署新的測試伺服器,而 Google Cloud Platform 剛好有提供 $300 兩個月的免費試用,且在台灣又有設點,所以我就決定拿 Google Compute Engine 來建置測試伺服器了。

Dynamic inventory

在開始之前,先稍微解釋一下何謂 Ansible 的 inventory,inventory 即代表伺服器,在 Ansible 中,可把伺服器列在 inventory file 中,藉此來分類伺服器,例如:

1
2
3
4
5
6
[webservers]
1.2.3.4 ansible_ssh_user=john
5.6.7.8 ansible_ssh_user=john

[dbservers]
9.10.11.12 ansible_ssh_user=mary

然而伺服器一多,管理 inventory file 就顯得有些麻煩,這時可以利用 dynamic inventory,只要把 inventory file 指定為可執行檔,Ansible 就能從執行檔的輸出中取得 inventory 資料。

Ansible 官方提供了各種主流主機商的 dynamic inventory,可以直接取用,而 GCE 當然也沒有缺席:http://docs.ansible.com/ansible/guide_gce.html

Read More

Algolia DocSearch

DocSearchAlgolia 最近提供的一個免費搜尋引擎服務,只要登錄網站,他們就會爬好整個網站,使用者只要把 JavaScript 腳本貼到適當的位置上即可使用,和 Swiftype 差不多,只是搜尋結果更好,而且速度更快。

這篇文章可能看起來很像業配文,不過我真的沒收這家公司的錢啦 XD。

Read More