Skaffold + MicroK8s で快適なコンテナ開発環境を構築する（Mac編）

**2023年11月5日追記**
*macOS アップデート後に問題が発生した方は記事の最後を参照してください。*

`WSL/Ubuntu` での構築方法は同時に公開した記事を参照してください。

https://qiita.com/aoyagi9936/items/5b93160155d3660a25a6

開発環境の構築はエンジニアにとって最初の登竜門ですが、コンテナ環境は特に Docker Desktop の有料化やローカルでの Kubernetes の実行などのハードルもあり、経験豊富なエンジニアにとっても一筋縄にはいかない難しさがあるように感じます。

今回は `Windows(WSL)` `Mac` `Linux(Ubuntu)` ユーザーが同一の開発体験を得られることを目標にコンテナ開発環境の構築方法をご紹介します。
また、

- `Kubernetes` を学びたいがどうして良いかわからない
- `本番環境に近いコンテナ実行環境` をローカルで実現したい
- `CI/CD` を回して高速にコンテナアプリケーションを開発したい

という方も参考にしてみてください。

**前提条件**
Mac OS のパッケージマネージャー `Homebrew` を利用しますので事前にセットアップしておいてください。

# Skaffold とは

https://skaffold.dev/

Google が開発しオープンソースで提供している、ローカルで Kubernetes ネイティブなアプリケーションを開発するためのコマンドラインツールです。
Golang で書かれておりシングルバイナリで動きます。
`Google Cloud Deploy` を用いればローカルだけではなくクラウドでもデリバリーパイプラインを構築できますし、`Kustomize` `Helm` など Kubernetes をより効率的に扱う支援ツールにも対応しているので覚えておいて損はないと思います。

https://cloud.google.com/deploy/docs/using-skaffold?hl=ja#what_is_skaffold

# MicroK8s とは

`MicroK8s` はローカルでも動作する軽量な Kubernetes ディストリビューションです。
他の候補としては `Minikube` や `K3s` 等が挙げられますが、 MicroK8s はエンタープライズサポートや複数ノード対応など本番環境としての運用も可能で、開発や本番で有用なアドオンも充実しているのが特徴です。
詳しくは本家の比較表を参照してください。

[MicroK8s vs K3s vs minikube](https://microk8s.io/compare)

ちなみに私は以前は Minikube を使っていましたが `registry` `hostpath-storage` `linkerd` といった実用的なアドオンに惹かれて乗り換えました。

# Mac に MicroK8s (+ Multipass ) をインストールする

Mac では MicroK8s は Multipass という仮想マシン管理ツールで作成された Ubuntu 上で動作します。
どちらも Canonical 社が開発しているので相性については問題なく、以下のコマンドで Multipass と MicroK8s の両方がインストールされます。

```shell
brew install ubuntu/microk8s/microk8s
microk8s install
# リリースチャンネルを指定する場合
microk8s install --channel 1.21
```

`microk8s install` で失敗した場合は `microk8s uninstall` して Mac を再起動してやり直してみてください。

ちなみに今は大丈夫ですが、 Apple シリコンの Mac が登場してすぐの頃はインストールされる Multipass のバージョンが `1.7` で Apple シリコンに対応していなかったので Github の [Multipassリポジトリ](https://github.com/canonical/multipass/releases) から対応したバージョン `1.8 以降` を手動でインストールする必要がありました。
当時は Apple シリコンの Mac で Docker Desktop を動作させるには `Rossetta 2` をインストールする必要があったので、Rossetta 2 なしで Docker 環境を作る際に Mutipass はとても重宝しました（今は Docker Deskop も Rosseta 2 が必須要件ではなくなったようです[^1]）。
[^1]:[Docker Desktop for Apple silicon](https://docs.docker.com/desktop/mac/apple-silicon/)

Kubernetes の設定ファイルは `.kube` に格納されます。
存在しない場合は作成し、存在する場合は既存のものとマージします。
```shell
cd $HOME
mkdir .kube
cd .kube
microk8s config > config​
```

既存の環境と分けて設定を管理したい場合は先ほどの手順を　`.kube` ではなく `.microk8s` フォルダで実行し `KUBECONFIG` 環境変数を利用するか `--kubeconfig` フラグを利用してください（後述する `Skaffold` にも `--kubeconfig` は指定できます）。
```shell
export KUBECONFIG=$HOME/.microk8s/config
# または
kubectl --kubeconfig=$HOME/.microk8s/config
```

再起動し以下のコマンドで MicroK8s が正常に動作していることを確認します。
（起動には少し時間がかかります）

```shell
microk8s status
```

MicroK8s には多くの便利なアドオンがありますが今回有効にするのは以下の2つです。

```shell
microk8s enable dns
microk8s enable registry
```

Kubernetes に慣れていない方はダッシュボードを使うことで状態をグラフィカルに確認できます。
~~アドオンを有効にするだけではなく追加でポートフォワード等の設定も必要なので脚注をご確認ください。~~[^2]
[^2]:[Addon:dashboard - MicroK8s](https://microk8s.io/docs/addon-dashboard)

ダッシュボードのインストールからアクセスに必要な設定までを一度にやってくれるコマンドがあります（version 1.19+）。

```shell
microk8s dashboard-proxy
```

ダッシュボードの起動後にターミナルに URL と トークンが表示されます。
指示の通りに `https://192.168.64.2:10443` にアクセスするとトークンを入力できますのでコピー＆ペーストしてサインインしてください。
※ Multipass 上の MicroK8s VM インスタンスの IPアドレスは環境によって異なりますので `microk8s info --all` コマンドで確認してください

```
Checking if Dashboard is running.
Infer repository core for addon dashboard
Infer repository core for addon metrics-server
Waiting for Dashboard to come up.
Trying to get token from microk8s-dashboard-token
Waiting for secret token (attempt 0)
Dashboard will be available at https://192.168.64.2:10443
Use the following token to login:
<TOKEN>
Forwarding from 0.0.0.0:10443 -> 8443
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2767381/2d231c31-b7a7-4822-1474-eeaf5023aa55.png)

# Mutipass 上の Ubuntu に Docker Engine をインストールする
今回は Docker Desktop に頼らずに Docker 環境を作成します。
問題点は macOS には Docker CLI はインストールできますが Docker Engine はインストールできないことです。
そこで Mac に Docker CLI を、 Multipass 上の Ubuntu に Docker Engine をそれぞれインストールしてローカル環境内で連携させることで Docker 環境を構築します。

まずは　Mac から Multipass 上の Ubuntu にログインします。
（ `microk8s install` で自動で作成されたものをそのまま使います）

```shell
multipass shell microk8s-vm
```

ログインできたら IP アドレスを確認してメモしておきます。
（おそらく `192.168.64.2` かと思います）

次に Ubuntu に Docker Engine をインストールしますが概ね公式の手順の通りです。
[Install Docker Engine on Ubuntu](https://docs.docker.com/engine/install/ubuntu/)
Docker CLI は Mac 側のものを使うため本来は不要ですが、念の為に Docker CLI もインストールしています（但し Docker Compose は除外しています）。

```shell
# GPG キーの設定やリポジトリの更新は公式通りのため省略
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
```

さて、ここからが Mac + MicroK8s + Docker + Skaffold のための少しトリッキーな設定となります。
まず、 Docker Engine API を外部から実行できるようにします。
```shell
sudo systemctl edit docker
```

`nano` エディタが起動するので以下の記述を貼り付けますが、その際 `[PORT]` をウェルノウンポート以外の任意の番号に書き換えてください。
特に Docker Engine のデフォルトポート `2376` は避けておきましょう。
（セキュアにするために通信を `TLS` で保護する方法もありますが少し複雑なのでご希望の方は脚注を実践してください[^3]）
また、最初の何も指定されていない `ExexStart=` も必要なので消さないようにしてください。
[^3]:[Use TLS (HTTPS) to protect the Docker daemon socket - docs.docker.com](https://docs.docker.com/engine/security/protect-access/#ssh-tips)

```config
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:[PORT] --containerd=/run/containerd/containerd.sock
```

書き換えたら Docker を再起動します。

```shell
sudo systemctl restart docker
```

# Mac に Docker CLI をインストールする

Mac には以下のコマンドで Docker CLI をインストールできます。

```shell
brew install docker
```

Docker Desktop を利用する場合はこの後さらに `cask` オプションをつけて GUI アプリケーションをインストールしますがここでは行いません。

`DOCKER_HOST` 環境変数に先ほどメモした接続先の VM マシンの IP アドレスとポート番号を指定します。
何度も実行しなくても良いように `.bashrc` や `.zshrc` などのシェルスクリプトの設定ファイルに以下を追記してください。

```config
export DOCKER_HOST=tcp://192.168.64.2:[PORT]
```

`exec $SHELL -l` で設定を再読み込みできます。
`docker images` コマンド等で正常に返ってくれば Docker のセットアップは完了です。

ちなみに、 Mac + Docker だけ使えれば良い場合は `Docker Context` による `ssh` 構成で接続した方がセキュアなためオススメです。
ただ今回は Skaffold とも連携させる必要があり、Skaffold は Docker Context を認識できないため上記のような方法を取っています。

# Mac に Skaffold をインストールする

最後に Skaffold をセットアップします。
快適な環境までもう一息ですので頑張りましょう。

以下のコマンドで Skaffold をインストールします。

```
brew install skaffold
```

インストールができたら MicroK8s の組み込みレジストリを登録します。
以下のコマンドで現在の Kubernetes コンテキスト（接続先クラスタ）で利用するレジストリを登録します。

```shell
skaffold config set insecure-registries localhost:32000
skaffold config set default-repo localhost:32000
```

今までの手順の通りであれば Skaffold に登録されるコンテキスト名は `microk8s` となるはずです。
`~/.skaffold/config` ファイルの中を見ると設定を確認できますが、異なる場合は `kubectl config current-context` コマンドを実行して表示されるコンテキスト名を確認してください。

```yaml:~/.skaffold/config
kubeContexts:
- kube-context: microk8s
  default-repo: localhost:32000
  insecure-registries:
  - localhost:32000
```

最後に `microk8s status` で `registry` アドオンが `enabled` であることを確認しておきましょう。
お疲れ様でした。これで環境は完成です！

# Skaffold を構成してコンテナをローカルの MicroK8s にデプロイする

Dockerfile と Kubernetes のマニフェストファイルを作成し、 Skaffold で Docker Build と MicroK8s へのデプロイの一連の流れを構成します。
テストシナリオとして Postgres データベースを Pod で立ち上げてローカルから接続してみます。
サンプルソースも用意しておいたので `git clone` してください。

https://github.com/aoyagi9936/skaffold-microk8s-example

#### ディレクトリ構成
```shell
.
└── skaffold-microk8s-example/
    ├── skaffold.yaml
    ├── kubernetes-manifests/
    │   └── pod.yaml
    └── src/
        └── infra/
            └── database/
                ├── Dockerfile
                ├── 0_create_database.sql
                └── 1_insert_table.sql
```

#### Docker コンテナ設定ファイル
```Dockerfile:Dockerfile
FROM postgres:14.4-alpine

COPY *.sql /docker-entrypoint-initdb.d/
```

```sql:0_create_database.sql
DROP DATABASE IF EXISTS test_db;
CREATE DATABASE test_db;
```

```sql:1_insert_table.sql
¥c test_db;

CREATE TABLE example_tbl (
  id serial PRIMARY KEY,
  name TEXT
);

INSERT INTO example_tbl (name) VALUES ('aoyagi');
```

#### Skaffold 設定ファイル
```yaml:skaffold.yaml
apiVersion: skaffold/v4beta2
kind: Config
build:
  tagPolicy:
    sha256: {}
  local:
    push: true
    useBuildkit: true
  artifacts:
  - image: example-db
    context: src/infra/database
deploy:
  kubeContext: microk8s
profiles:
  - name: local
    portForward:
      - resourceType: pod
        resourceName: my-example-pod
        port: 5432
    manifests:
      rawYaml: [
      "./kubernetes-manifests/pod.yaml"
      ]
```

#### Kubernetes マニフェストファイル
```yaml:pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-example-pod
spec:
  containers:
  - name: my-example-pod
    image: example-db
    ports:
      - containerPort: 5432
    env:
      - name: POSTGRES_USER
        value: postgres
      - name: POSTGRES_PASSWORD
        value: postgres
      - name: POSTGRES_DB
        value: test_db 
      - name: TZ
        value: Asia/Tokyo
```

全ての準備が整ったら Skaffold を実行してみましょう。
`skaffold.yaml` のあるパスで以下のコマンドを実行します。

```
skaffold dev -p local
```

コンテナが MicroK8s へデプロイされたらお手元のDB接続ツールで接続してみましょう。

| 接続情報 | 設定値 |
| --- | --- |
| ホスト | localhost |
| ポート | 5432 |
| ユーザー名 | postgres |
| パスワード | postgres |
| データベース名 | test_db |

これだけでは Skaffold の良さが伝わりきらないので **Skaffoldを実行したまま** `1_insert_table.sql` にデータを追加してみましょう。

```sql:1_insert_table.sql
INSERT INTO example_tbl (name) VALUES ('aoyagi');
+ INSERT INTO example_tbl (name) VALUES ('your_name');
```

Skaffold が変更を自動で検知してコンテナを再作成し MicroK8s にデプロイします。
開発中は仕様が頻繁に変わりますが変更の都度環境を再作成してくれることで安全性が高まります。
環境をイミュータブルに作成することの良さはまたどこかでお伝えできればと思いますが、タイトルの目標は達成できましたので本記事はここまでとなります。

本投稿が皆様の快適なコンテナ開発ライフの一助になれば幸いです。

# 補足：macOS アップデート後に MicroK8s が正常に動作しなくなる問題の対処法

**2023年11月5日更新**
macOS 12 以降にアップグレード後に環境によっては MicroK8s の起動に必要な Multipass で VM マシンが `Unknown` になってしまう問題が発生する場合があり、私も長らく悩まされていましたが以下の方法で解消することができました。どうやら OS アップデートで DHCP のリースログファイルが破損してしまうことが原因のようですので、同様の事象が発生した方は関連リソースを一度削除して PC を再起動してください。

```shell
# 破損した　DHCP のリースログファイルを削除する
$ sudo rm /var/db/dhcpd_leases
# OS 再起動
```

現在は以下の環境で問題なく動作しています。

- macOS Sonoma 14.1.2
- MicroK8s 1.28.2 revision 6041
- multipass 1.12.2+mac

解決の糸口になった Issue です。

https://github.com/canonical/multipass/issues/2387

**2023年12月3日追記**
Mac の OS アップデート実施前に `microk8s stop` で VM マシンを終了しておくと DHCP リースログが破損しないことを確認しました。
もし停止コマンドを実行せずアップデートしてしまい VM マシンが Unknown になってしまう場合は前述した方法で復旧してみてください。

**2024年2月25日追記**
Apple M3 Silicon で Multipass インスタンスが起動しない問題がバージョン 1.13.1 で解消されたようです。
以下のコマンドでアップグレードできます。

```shell
brew upgrade multipass
```

M1 ですが以下の環境で正常に動作することを確認済みです。

- macOS Sonoma 14.3.1
- MicroK8s v1.28.7 revision 6565
- multipass 1.13.1+mac