一般來說, container通常會設計成只專注在它單一的任務上, 也就是通常不會把一個http server跟db server跑在同一個container內, Kubernetes 的Pod的設計, 讓我們可以在同一個Pod內放多個containers, 因此可以延伸出init container, sidecar container來輔助原本的container, 中間可以透過分享Volume或是直接透過loopback網路來共享資料, 但還是會有情境是, 你會希望可以在某個container空間內執行某個程式, docker的話, 你可以用 docker exec ,那在Kubernetes呢?

Kubernetes 也是可以用kubectl 達成同樣的目的, 像是:

 kubectl exec mycontainer ls

其實跟docker exec 是類似的, 如果是要執行shell進去執行其他的維護:

kubectl exec mycontainer -it /bin/sh

但如果是, 你要從其他的pod去執行其他的pod裡面的指令呢? 像是用Cron job定期去執行某特定pod裡面的程式?

我們也是可以透過呼叫Kubernetes API來達成這目的的, 如同下面這個範例:

func runCommand(clientset *kubernetes.Clientset, pod string, ns string, cmd ...string) (string, error) {
	req := clientset.CoreV1().RESTClient().Post().Resource("pods").Name(pod).Namespace(ns).SubResource("exec")
	option := &v1.PodExecOptions{
		Command: cmd,
		Stdin:   false,
		Stdout:  true,
		Stderr:  true,
		TTY:     false,
	}

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	req.VersionedParams(option, scheme.ParameterCodec)

	exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
	if err != nil {
		return "", err
	}

	sopt := remotecommand.StreamOptions{
		Stdout: &stdout,
		Stderr: &stderr,
	}

	err = exec.Stream(sopt)

	if err != nil {
		return "", err
	}

	if stderr.String() != "" {
		return stdout.String(), errors.New(stderr.String())
	}

	return stdout.String(), nil
}

由於Kubernetes API還沒包裝exec這個資源, 所以要用.Resource("pods").Name(pod).Namespace(ns).SubResource("exec")去取用, stdin, stdout, stderr都還是可以串接回來的

但如果你直接在你的pod內執行這段的話, 其實不會成功的, 因為你沒有權限可以去做, 你必須先設定好一個Role如下:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: executor
rules:
  - verbs:
      - get
      - list
    apiGroups:
      - ''
    resources:
      - pods
      - pods/log
  - verbs:
      - create
    apiGroups:
      - ''
    resources:
      - pods/exec

我的情境是會用list pods找出pod的名稱, 在用這個名稱去找到特定的pod執行, 所以會需要前半段get和list的權限, 後半段對pods/execcreate的權限才是執行這段程式真正需要的

要達成這件事, 只需要利用turf.js就好, turf.js提供了一大堆處理地理資訊的相關工具, 只拿來算距離, 還真算有點小用 :P

以下是範例:

const { point } = require('@turf/helpers');
const distance = require('@turf/distance').default;

const from = point([120.9758828, 24.8043379]);
const to = point([120.92846560331556, 24.846169955749275]);

const result = distance(from, to, { units: 'kilometers' });

這邊point回傳的是GeoJson的Feature, 由於GeoJson是經度在前緯度在後, 如果你去Google map複製座標來的話, 會剛好相反, 這邊需要注意, 以上就可以算出這兩點到底距離幾公里了

turf.js可以做的相當多, 算距離只是其中之一, 如果你要找出像是包含所有點的最小矩形, 或是合併多個多邊形(像是合併行政區域), 都可拿來使用

WSL 當作開發環境固然方便好用的, 但也吃蠻大空間的, 最近在開發的東西, 需要跑一個postgresql, 裝不少資料, 吃掉我蠻多硬碟空間的, 偏偏我SSD就只有小小的512G (好吧, 的確寒酸到不像開發者的電腦), 一下子就吃滿滿了, 所以就必須要把這個給搬到我另一個比較大的磁碟救急(說是救急的意思是, 預期它會吃上1TB, 不過這又是會碰到一個問題, 之後再解了)

WSL新開的映像檔都是放在C:(畢竟不是謎片,不會自動住到D槽…咦?!), 要把它搬家的話, 需要先把它export出來, export的方法很簡單:

wsl --shutdown
wsl --export Ubuntu-20.04 d:\ubuntuback.tar

先shutdown是想保險一點, 所以也先把相關的視窗(像是Terminal, VSCode)都關一關, 如果映像檔很大, 這預期要做非常久, 像我這個有200G以上, 放下去, 基本上我就去看電視不管它了, 當然也要確保一下你目標硬碟夠大, 這邊Ubuntu-20.04是我要備份的目標, 如果不知道名字是啥可以用wsl -l查詢

export完之後, 接著就用:

wsl --import Ubuntu20dev e:\wsl\dev D:\ubuntuback.tar

這邊Ubuntu20dev是新的名字, 不要跟舊的重複了, import完後就可以用wsl -d Ubuntu20dev 登入進去玩了

不過登入後, 咦, 等一下, 怎麼會是用root? 之前舊的並不是呀! 要解決這個問題, 新增這個檔案/etc/wsl.conf, 裡面內容是

[user]
default=yourloginname

把wsl再shutdown之後再重新進來就不會是root了

PostGIS 是讓PostgresSQL可以支援地理資訊資料的一個擴充, 而geometryPostGIS定義來儲存地理資料的資料型態, 包含了座標點(POINT), 線, 多邊形等等

GORM則是Golang界算蠻有名的ORM套件, 支援了蠻多不同的關聯式資料庫

不過GORM是沒有直接支援PostGIS的geometry這個資料型態的, 畢竟geometry並非一般SQL標準的資料型別, 因此如果要讓GORM可以來存取geometry, 就得使用它所提供的Customize Data Type

要存取這種自訂的資料型別, 需要實做ScannerValuer這兩個介面:

type Scanner interface {
	// Scan assigns a value from a database driver.
	//
	// The src value will be of one of the following types:
	//
	//    int64
	//    float64
	//    bool
	//    []byte
	//    string
	//    time.Time
	//    nil - for NULL values
	//
	// An error should be returned if the value cannot be stored
	// without loss of information.
	//
	// Reference types such as []byte are only valid until the next call to Scan
	// and should not be retained. Their underlying memory is owned by the driver.
	// If retention is necessary, copy their values before the next call to Scan.
	Scan(src interface{}) error
}

type Valuer interface {
	// Value returns a driver Value.
	// Value must not panic.
	Value() (Value, error)
}

那在實做前, 可以先來看看geometry實際存到資料庫是長怎麼樣? 它長的就會像是一串這樣的文字0101000020E61000001E64C73CEA905EC09CD6C962A7C84240, 看起來就是十六進位編碼過的文字, 根據文件, 它是經過EWKB編碼過的(EWKB是Postgis從WKB延伸來的), 然後編碼過的binary資料再轉成Hex字串

這部分的解碼, 可以不用自己寫, 使用open source的好處就是可以踩在前人的肩膀上, 可以利用go-goem來幫我們處理這件事, 這邊以處理座標點為例 (採用常見的EPSG:4326座標系統), 定義一個叫做GeoPoint的型別給GORM使用, 而這個型別的實作可以是這樣:

type GeoPoint geom.Point

func (g *GeoPoint) Scan(val interface{}) error {
	pt, err := ewkbhex.Decode(val.(string))

	if err == nil {
		if p, ok := pt.(*geom.Point); ok {
			*g = GeoPoint(*p)
		} else {
			return errors.New(fmt.Sprint("Failed to unmarshal geometry:", val))
		}
	}

	return err
}

func (g GeoPoint) Value() (driver.Value, error) {
	pt := &g
	toPt, err := ewkbhex.Encode((*geom.Point)(pt).SetSRID(4326), binary.BigEndian)

	return toPt, err
}

其實非常簡單的利用了go-goem提供的ewkbhex來編解碼而已

Docker Desktop要收錢了, 雖然不是跟個人開發者收, 而且一家公司走向營利也合理, 但這作法說實在有點粗糙, 是時候改用不同的工具來玩了

現在在開發階段多多少少會需要在local有一個測試環境亂搞, 但一般的小電腦, 跑起Docker + K8S, 也沒辦法跑太多容器了, 能夠有盡量輕量化的東西當然最好, 在我的linux PC上, 我現在用的是podman + KIND (Windows下我還是用Docker desktop, 還沒切換過去)

Podman

Podman是一個由Redhat開發的工具, 跟Docker最大的不同在於, 它沒需要跑一個daemon常駐在那邊, Docker desktop即使你沒跑任何container狀況下daemon還會在, Podman則完全沒這問題

安裝的話請參考: Podman Installation Instructions

安裝好後, 指令幾乎跟docker 類似, 像是 docker ps可以用podman ps取代, docker run可以用podman run取代, 幾乎沒太大問題

有些狀況, 會需要有docker daemon, 像是如果使用pack, 由於pack會需要呼叫docker daemon來建立image, 如果使用podman, 在這狀況就得跑一個service:

podman system service --time=0 tcp:localhost:1234

這邊可以是tcp或是unix socket, 然後把DOCKER HOST改指到這邊來就好了

kind

kind是一個讓你建立local K8S cluster的工具, 其他類似的還有MicroK8sMiniKube

為何選kind? 有時候會需要模擬多個nodes的cluster環境, 不管是MicoK8s還是MiniKube, 在單機建立出的k8s都只有一個node, 但kind卻可以在單機建立出多nodes的環境

在Mac下可以用Homebrew安裝:

brew install kind

建立一個cluster很簡單, 只要執行

kind create cluster

如果你是要用Podman也可以

KIND_EXPERIMENTAL_PROVIDER=podman kind create cluster

但第一次使用podman建立會發生一個問題:

KIND_EXPERIMENTAL_PROVIDER=podman kind create cluster
using podman due to KIND_EXPERIMENTAL_PROVIDER
enabling experimental podman provider
Creating cluster "kind" ...
 ✗ Ensuring node image (kindest/node:v1.21.1) 🖼 
ERROR: failed to create cluster: failed to pull image "kindest/[email protected]:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6": command "podman pull kindest/[email protected]:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6" failed with error: exit status 125
Command Output: Error: short-name resolution enforced but cannot prompt without a TTY

這是由於podman在pull image時碰到short name(像是java, ubuntu, fedora這類的), 它會請你從/etc/containers/registries.conf裡面設的search host挑一個是可以找到這個image的host, 但在kind跳不出來給你挑, 這時候只要看哪個pull不下來(像這邊是kindest/[email protected]:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6)就自己手動執行一次

podman pull kindest/[email protected]:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6

一次就好, 之後它會記起來, 這時候再來跑kind就沒問題了

要毀掉一個cluster也很簡單

kind delete cluster

雖然文件上有說支援Rootless, 不過實際上試, 要排除的問題還很多, 不建議使用

如果你需要用kubectl或是Lens去存取建立出來的cluster, 那可以輸出kubeconfig

kind export kubeconfig

那, 說好的多肉…喔…多nodes環境呢? 建立以下這樣的config(例如檔名叫config.yaml):

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker

然後用kind create cluster --config config.yaml就可以建立出一個4 nodes (一個control plane, 三個worker)的環境了(在單機)