Zalando Postgres Operator を試してみた

ふとしたきっかけで、自宅ラボに PostgreSQL as a Service が欲しくなったので Zalando Postgres Operator を試してみました。

Zalando Postgres Operator とは

Zalando Postgres Operator(以下、Postgres Operator)とは、Kubernetes クラスタに HA 構成の PostgreSQL クラスタを簡単に構築してくれる Operator です。

Postgres Operator を Kubernetes クラスタにデプロイすると、postgresql というカスタムリソースを宣言したマニフェストを使うだけで簡単に PostgreSQL クラスタを構築できます。

また、専用の Web UI を使って PostgreSQL クラスタを管理することもできます。

この Postgres Operator を自宅ラボの Kubernetes クラスタにデプロイして PostgreSQL as a Service として利用します。

Zalando SE

Zalando SE とは、ヨーロッパを中心に展開するオンラインファッションプラットフォームを運営する企業です(日本で言うところの ZOZO でしょうか?)。

Zalando SE は GitHub で複数のオープンソースのプロジェクトを公開しています。

https://github.com/zalando

Zalando Postgres Operator も Zalando が開発しているプロジェクトの一つです。

構成

公式ドキュメント の画像を拝借させてもらうのですが、 Postgres Operator デプロイ後に postgresql カスタムリソースをデプロイすると、下図のリソースが Postgres Operator によって作成されます。

f:id:nnstt1:20210122034306p:plain

HA 構成の PostgreSQL クラスタを構築するため、Pod には Spilo というコンテナイメージが使用されます。

Spilo コンテナの中では PostgreSQLPatroni というサービスが動いています。

Patroni は PostgreSQL クラスタのノードを監視して、マスタが故障した際に自動フェイルオーバーを実現するためのサービスです。

f:id:nnstt1:20210122034401p:plain

Spilo と Patroni は共に Zalando のプロジェクトとなっています。

今回初めて Zalando を知りましたが、ほとんど自分たちで開発していてすごいですね…。

インストール

Postgres Operator のインストール方法には以下の3通りあります。

  • マニュアル
  • Kustomize
  • Helm

今回はマニュアル、つまりマニフェストをデプロイする方法で Postgres Operator をインストールします。

公式リポジトリ内のマニフェストkubectl apply するだけで Operator と Web UI をインストールできます。

$ git clone https://github.com/zalando/postgres-operator.git
$ cd postgres-operator/

$ kubectl create namespace zalando-postgres

# Postgres Operator
$ kubectl apply -f manifests/configmap.yaml
$ kubectl apply -f manifests/operator-service-account-rbac.yaml  # namespace: defualt のものがあるので注意
$ kubectl apply -f manifests/postgres-operator.yaml
$ kubectl apply -f manifests/api-service.yaml

# UI
$ kubectl apply -f ui/manifests/

# 確認
$ kubectl get all -n zalando-postgres
NAME                                        READY   STATUS    RESTARTS   AGE
pod/postgres-operator-f6bfcd5b4-zsh2n       1/1     Running   0          3d7h
pod/postgres-operator-ui-6dc47bbbc6-phdvw   1/1     Running   0          4d9h

NAME                           TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)        AGE
service/postgres-operator      ClusterIP      10.96.100.47     <none>          8080/TCP       6d23h
service/postgres-operator-ui   LoadBalancer   10.104.183.177   192.168.2.252   80:31363/TCP   14d

NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/postgres-operator      1/1     1            1           6d23h
deployment.apps/postgres-operator-ui   1/1     1            1           14d

NAME                                              DESIRED   CURRENT   READY   AGE
replicaset.apps/postgres-operator-f6bfcd5b4       1         1         1       6d23h
replicaset.apps/postgres-operator-ui-6dc47bbbc6   1         1         1       13d

postgres-operatorpostgres-operator-ui の Pod がデプロイされました。

注意点 1

manifests/operator-service-account-rbac.yaml には namespace: default と指定されているため、Postgres Operator 用の namespace を default 以外にする場合には変更が必要です。

# manifests/operator-service-account-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: postgres-operator
  namespace: zalando-postgres

注意点 2

Web UI のマニフェストはそのままでは namespace: default しか管理対象となりません。 全 namespace を対象とする場合は、ui/manifests/deployment.yamlTARGET_NAMESPACE"*" を指定します。

# ui/manifests/deployment.yaml
env:
  - name: "TARGET_NAMESPACE"
    value: "default"    # "*" に変更

以上で Postgres Operator のインストールは完了です。

PostgreSQL クラスタ構築

Postgres Operator の準備ができたので、PostgreSQL クラスタを構築していきます。 クラスタ構築には2通りあります。

  • Web UI
  • マニュアル

Web UI

さきほど作成した postgres-operator-ui に Service または Ingress 経由でアクセスすると、クラスタ作成画面が表示されます。

必要な項目を入力して「Create Cluster」を押すと PostgreSQL クラスタが作成されます。

f:id:nnstt1:20210122024955p:plain

マニュアル

マニュアルの場合は、postgresql リソースを宣言したマニフェストを準備します。

公式リポジトリのサンプルや、Web UI のクラスタ作成画面に表示されるマニフェストが参考になります。

以下は自宅ラボの k8s クラスタにデプロイしたお試しマニフェストです。 Rook で用意した storageClass を追加しています。

# postgres.yaml
apiVersion: acid.zalan.do/v1
kind: postgresql
metadata:
  name: acid-example
  namespace: default
  labels:
    team: acid
spec:
  teamId: acid
  postgresql:
    version: "13"    # note: String
  numberOfInstances: 1
  volume:
    size: 1Gi
    storageClass: rook-ceph-block
  users:
    example: []
  databases:
    example: example
  allowedSourceRanges:
  resources:
    requests:
      cpu: 100m
      memory: 100Mi
    limits:
      cpu: 500m
      memory: 500Mi

postgresql リソースのデプロイが完了すると、Postgres Operator により StatefulSet や Service の各種リソースが作成されます。

$ kubectl get all
NAME                      READY   STATUS    RESTARTS   AGE
pod/acid-example-0            1/1     Running   0          164m

NAME                      TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)        AGE
service/acid-example          ClusterIP      10.98.124.54     <none>          5432/TCP       7d
service/acid-example-config   ClusterIP      None             <none>          <none>         7d
service/acid-example-repl     ClusterIP      10.106.121.165   <none>          5432/TCP       7d

NAME                        READY   AGE
statefulset.apps/acid-example   1/1     7d

NAME                                TEAM   VERSION   PODS   VOLUME   CPU-REQUEST   MEMORY-REQUEST   AGE   STATUS
postgresql.acid.zalan.do/acid-example   acid   13        1      1Gi      100m          100Mi            7d    Running

注意点

Web UI の注意点として、volume.storageClass の指定ができません。

そのため、Rook Ceph で作成した storageClass などを指定する場合はマニフェストを使う必要があります。

Web UI では基本的な情報を埋め込んだマニフェストを作成し、細かい設定はマニフェストに直接記述するのが良さそうです。

接続確認

kubectl port-forward を使い、ローカル環境から PostgreSQL クラスタへ接続できるか確認します。

本当はフェイルオーバーなど HA 構成の特性を検証してみたかったのですが、PostgreSQL について勉強不足のため実施していません。

Postgres Operator によりログイン用のパスワードが Secret として作成されています。

$ export PGMASTER=$(kubectl get pods -o jsonpath={.items..metadata.name} -l application=spilo)
$ kubectl port-forward $PGMASTER 6432:5432 &

$ kubectl get secret postgres.acid-example.credentials -o 'jsonpath={.data.password}' | base64 -d
$ export PGSSLMODE=require
$ psql -U postgres -h localhost -p 6432
Handling connection for 6432
ユーザ postgres のパスワード: 
Handling connection for 6432
psql (13.1)
SSL 接続 (プロトコル: TLSv1.2、暗号化方式: ECDHE-RSA-AES256-GCM-SHA384、ビット長: 256、圧縮: オフ)
"help"でヘルプを表示します。

postgres=# 

無事に Postgres Operator で作成した PostgreSQL クラスタに接続することができました。

参考にさせていただいたサイト

speakerdeck.com

今回 PostgreSQL as a Service を構築するにあたって情報を提供くださった @tzkb さんのスライドです。

そもそもの「Kubernetes 上でデータベースを動かす際の課題」から、より深い話まで説明されています。

仕事では SQL Server メインなのですが、Database on Kubernetes という観点では課題は同じはずですので、とても勉強になりました。

www.sraoss.co.jp

Zalando Postgres Operator の紹介をされている記事です。

今回のネタは、ほぼ上記記事の内容を元にしています。 フェイルオーバーなどのクラスタ構成の検証もされていて興味深かったです。

recruit.gmo.jp

Patroni を試されている記事です。

自分は Patroni が何者か分からない状態だったので参考になりました。

さいごに

PostgreSQL as a Service のための Zalando Postgres Operator の使い方について確認しました。

PostgreSQL については本当に初心者なのですが、そのような状態でも Kubernetes クラスタがあれば簡単に PostgreSQL as a Service が実現できました。

もし手元に Kubernetes クラスタをお持ちなら、クラウドサービスではなく Zalando Postgres Operator も選択肢に入れてみると面白いかもしれません。

2020年振り返り

2020年も残すところ数時間なので、今年の振り返りをサクッとします。

第一子誕生

1月に長男が誕生しました。

2, 3ヶ月は夜に起こされることも多く心身ともに疲労がありましたが、 妻の実家に近いこともあり、かなり助けられました。

大変な時期も含めて、とてもとても可愛くて毎日の成長が楽しみです。

来週には1歳になりますが、まだ誕生日プレゼントを決めきれずにいます。 年始のセールで決めちゃいましょう。

自宅ラボ構築

5月に自宅ラボを構築しました。

今でも十分に大活躍していますが、仮想化基盤としてはメモリ不足を感じているのでスケールアウトかスケールアップしたいですね。 (沼の気配がする)

nnstt1.hatenablog.com

初 LT (オンライン)

6月の Ansible Night で初めてのLTをしました。

Ansible コミュニティ (ansiblejp) では新型コロナが広まる前から、オンラインでのイベント配信やもくもく会が開催されていました。 地方住みということもあり、Ansible コミュニティイベントは参加の敷居が低く、モチベーションを維持しやすかったです。

そこで色々な方と出会い(主にTwitterで)刺激を受けていく中で、いつしか「LTをしてみたい」という気持ちが湧いてきました。 ちょうど新型コロナの影響で Ansible Night がオンラインで開催されることになったので思い切ってエントリーしてみました。

Ansible Night オンライン! 2020.06 - connpass

なんとか無事(?)にLTをやり遂げたのですが、発表内容やプレゼンの仕方についてはまだまだ未熟だと自身でも感じたので、 なるべく機会を見つけてLTなどの経験を積んでいきたいです。

初オンライン飲み

7月に初めてオンラインで飲み会をしました。

Twitterで仕事の愚痴をつぶやいていたところ、Ansible 関連で知り合った(フォローし合った)方からオンライン飲みに誘われました。

初めて喋る方とする初めてのオンライン飲み、始める前は緊張しかなかったのです。 しかし、お酒の力や参加された方々の人柄もあり、終盤はいつもの飲み会と変わらない楽しさがありました。 (その節は愚痴を聞いていただき本当にありがとうございました)

その後も何度か誘っていただき、オンラインならではの「移動が必要ない」とか「安上がり」といったメリットを実感しました。 今後、コロナが収まっても居酒屋に行く機会は減りそうです。

マイホーム

7月に家が建ちました。

2019年3~9月あたりで間取りなどをハウスメーカーの担当さんと計画していたのですが、この時期に妊娠発覚して後半はほぼメーカーにお任せ状態でした。

ところどころ「ここにコンセント欲しかった」とか「冷蔵庫はここじゃなかった」とか、もう少し考えておけばという後悔もありますが、住めば都です。

書斎も作らせてもらったので、今後テレワークになったとしても快適に仕事ができるように「城」を築いていこうと思います。

2回目のLT

12月の Kubernetes Meetup Novice で2回目のLTをしました。

Kubernetes Novice Tokyo #7 - connpass

Kubernetes 関連は仕事では触れる機会がなく完全に趣味レベルではあったのですが、 こちらのコミュニティでは自分のような「Kubernetes初心者」の登壇機会を提供してくださっています。

そして今回のテーマが「○○試してみた」ということで、ちょうど11月に触っていた ExternalDNS をネタにして登壇しました。

nnstt1.hatenablog.com

初回に比べればまだマシになったかな?という程度でまだまだ経験値が足りないと反省しました。

総括

2020年は人生としても大きなイベントがあり、初めての経験もたくさんできました。

繋がってくださった方々、機会をいただいた方々、助けてくださった方々にはとても感謝しています。 本当にありがとうございました。

2021年はアウトプットをたくさんして、自分から機会を提供できればと考えています。

AKS にスポットノードプールを追加してみた

AKS (Azure Kubernetes Service) にスポットノードプールを追加してみました。

以下のドキュメントを参考にしました。

docs.microsoft.com

動機

スポットノードプールを試した理由ですが、AKS を安く使えると思ったから です。

AKS は最低でも 1 台のノードを含む 1 つのノードプールが必要です。 この 1 台分の料金をスポットノードプールを使うことで、スポット料金が適用されて通常のノードを使うよりも安く済むと考えました。

結論から言うと、できません!安くなりません!

スポットノードプール

スポットノードプールとは

スポット VM という、Azure の余っているリソースを通常より安く利用できる VMAKS で利用するノードプールです。 安く利用できる反面、Azure のリソースが不足してきたら自動で VM は排除されてしまいます。

AKS でのスポットノードプールの利用は、バッチ処理ジョブ や 開発・テスト環境 といったワークロードが適しているようです。

制限事項

スポットノードプールにはいくつかの制限事項があります。 ここでは一部だけ抜粋して紹介しますので、詳しくは公式ドキュメントを参照ください。

  • 規定のノードプールに設定不可

    スポットノード プールを AKS クラスタの既定のノード プールにすることはできません。 つまり、AKS クラスタでスポットノードプールのみ利用することはできません。

この時点で、「AKS を安く使う」という動機が満たせないことが分かりました…。 とりあえず、AKS クラスタにスポットノードプールを追加することは試してみました。

AKS クラスタにスポットノードプール追加

AKS クラスタ構築

まずは AKS クラスタを構築します。 今回はノードプールを最小構成 サイズ:Standard_B2S, ノード数:1 で構築しました。

$ az aks create \
    --resource-group aks \
    --name aks-test \
    --location japaneast \
    --kubernetes-version 1.19.3 \
    --node-vm-size Standard_B2S \
    --node-count 1 \
    --no-wait

構築完了後は、ノードプールが 1 つだけ作成されています。

f:id:nnstt1:20201220233314p:plain

このノードプールで使用しているノードサイズ Standard_B2S月額 ¥4,447.744 です。 もしスポット VM として利用できれば、60% オフの 月額 ¥1,779.0976 となります。

f:id:nnstt1:20201221000624p:plain

スポットノードプール追加

次に、構築した AKS クラスタにスポットノードプールを追加します。

公式ドキュメントベースの設定で az aks nodepool add コマンドを実行します。 コマンド自体は通常のノードプールを追加するものと同じです。 --priority, --eviction-policy, --spot-max-price がスポットノードプール用のオプションになります。

az aks nodepool add \
    --resource-group aks \
    --cluster-name aks-test \
    --name spotnodepool \
    --priority Spot \
    --eviction-policy Delete \
    --spot-max-price -1 \
    --enable-cluster-autoscaler \
    --min-count 1 \
    --max-count 3 \
    --no-wait

spotnodepool という名前のノードプールが追加されました。(画像はプール作成途中です)

追加されたスポットノードプールの状態を見てみると、 サイズ:Standard_DS2_v2, ノード数:3 になっています。 コマンドでノードサイズを指定しない場合はデフォルトの Standard_DS2_v2 になります。

オプションの --enable-cluster-autoscalerAKSクラスタオートスケーラを有効化しており、 オートスケーラによる最大ノード数を --max-count で指定しているためノード数が 3 となっています。

f:id:nnstt1:20201220235303p:plain

Standard_DS2_v2 のスポット料金は通常の 78% オフとなりますが、Standard_B2S と比べると約 2 倍の料金となります。 また、ノード数も 3 となっているので 3 台分の料金が掛かります。

f:id:nnstt1:20201221000336p:plain

これでは、せっかくスポットノードプールを使っていても高くついてしまうため、 既定のノードプールと同じく サイズ:Standard_B2S, ノード数:1 でスポットノードプールを作ってみます。

Standard_B2S のスポットノードプール追加

まずは、ノードサイズに Standard_B2S を指定します。

az aks nodepool add \
    --resource-group aks \
    --cluster-name aks-test \
    --name spotnodepool \
    --priority Spot \
    --eviction-policy Delete \
    --spot-max-price -1 \
    --enable-cluster-autoscaler \
    --min-count 1 \
    --max-count 3 \
    --node-vm-size Standard_B2S \    # このオプションを追加
    --no-wait

上記コマンドを実行するとスポットノードプールの作成は始まるのですが、 最終的には Failed となってしまいました。

Azure に Standard_B2S 用のリソースが余ってないということでしょうか。 執筆時点では原因は分かっていません…。

f:id:nnstt1:20201221001908p:plain

ノード数 1 のスポットノードプール追加

次に、ノード数が 1 となるように指定します。

az aks nodepool add \
    --resource-group aks \
    --cluster-name aks-test \
    --name spotnodepool \
    --priority Spot \
    --eviction-policy Delete \
    --spot-max-price -1 \
    --enable-cluster-autoscaler \
    --min-count 1 \
    --max-count 1 \    # 3 → 1 に変更
    --no-wait

上記コマンドを実行すると node-count is not in the range of min-count and max-count というエラーになりました。 どうやら --min-count--max-count は同じ値にしてはいけないようです。

--node-count オプションでノード数を指定してみます。

az aks nodepool add \
    --resource-group aks \
    --cluster-name aks-test \
    --name spotnodepool \
    --priority Spot \
    --eviction-policy Delete \
    --spot-max-price -1 \
    --enable-cluster-autoscaler \
    --min-count 1 \
    --max-count 3 \
    --node-count 1 \    # ノード数指定
    --no-wait

この指定方法だとノード数を 1 にできました。

f:id:nnstt1:20201221003632p:plain

まとめ

スポットノードプールを使って AKS を安く使う計画はうまくいきませんでした。

しかし、ノードの冗長構成を検証するなどであれば、通常のノードプールを使うよりも費用は抑えられ、 勝手にノードが削除されることを利用した可用性の確認などもできるかもしれません。

自宅 k8s クラスタのサービスに FQDN で繋がるようにした

自宅の検証用マシン (Deskmini A300) に ESXi を入れて検証環境として利用しています。

最近はそこへ k8s クラスタを構築して色々試しているのですが、クラスタ内に立ち上げたサービスへは IP アドレスでアクセスしていました。 IP アドレスでアクセスするのはとても面倒だったのですが、やっと k8s で動かしているサービスに FQDN で繋がるようになったので投稿します。

システム構成図

完成後のシステム構成図になります。 (構成図描くの下手で分かりにくいと思います…)

f:id:nnstt1:20201113012536p:plain

見てもらって分かる通り、LAN 用の DNS サーバを VM で建てています (k8s クラスタの外です)。 これは、k8s クラスタを構築する前に DNS サーバ (dnsmasq) を構築していて、それを流用しているためです。

使用するプロダクト

今回利用しているプロダクトは以下になります。

名前 バージョン 用途
Kubernetes 1.19.0 コンテナオーケストレーション
Docker 19.03.12 コンテナランタイム
ExternalDNS 0.7.3 DNS プロバイダに DNS を登録
CoreDNS 1.8.0 DNS サーバ
etcd 3.4.13 DNS レコード格納
MetalLB 0.9.3 ベアメタルロードバランサー

構築

以下の手順で環境を構築しました。

  1. DNS サーバ (CoreDNS & etcd) 構築
  2. MetalLB デプロイ
  3. ExternalDNS デプロイ

DNS サーバ (CoreDNS & etcd) 構築

CoreDNS & etcd を使った DNS サーバを構築します。 この DNS サーバに k8s クラスタのサービス用の DNS レコードを格納していきます。 なぜ dnsmasq を流用しないかというと、後述する ExternalDNS が dnsmasq に対応していないからです。

今までは dnsmasq を yum でインストールしていたのですが、今回からは Docker コンテナを使っていきます。 docker のインストールは省略します。 (Docker Compose を使えばよかったかも)

コンテナネットワーク

CoreDNS コンテナから etcdコンテナへ繋がるようにするために dns-network という名前の bridge を作成しておきます。

$ docker network create --driver bridge dns-network

CoreDNS

CoreDNS とは、Go 言語で書かれた DNS サーバで、 CNCF によってホストされているプロジェクトです。 プラグイン形式で DNS レコードの格納/参照先を柔軟に設定できるところが売りなようです。

今回は etcd プラグインと hosts プラグインを使って、「k8s のサービスは etcd、その他は /etc/hosts に直書き」という処理をさせます。

まずは CoreDNS の設定ファイル Corefile を準備します。 仮に example.com をローカルで使用するドメインとして、example.com の問い合わせは以下の順番で確認するようにします。

  1. etcd (k8s サービス用)
  2. /etc/hosts

example.com 以外の問い合わせは /etc/resolv.conf を参照して外部の DNS サーバに回します。

#Corefile
example.com {
    etcd {
        path /skydns
        endpoint http://etcd:2379
        fallthrough
    }
    hosts
    cache
    errors
    log
}

. {
    forward . /etc/resolv.conf
}

次に、CoreDNS コンテナを起動します。 先ほど作成した Corefile/etc/hosts をマウントしています。

$ docker run -d \
    --name coredns \
    --network dns-network \
    -v /home/nnstt1/Corefile:/root/Corefile \
    -v /etc/hosts:/etc/hosts \
    -p 53:53/udp \
    -p 53:53/tcp \
    coredns/coredns:1.8.0 -conf /root/Corefile

etcd

etcd とは、こちらも Go 言語で書かれた分散 KVS (Key Value Store) です。 今回は CoreDNS で管理する DNS レコードの格納先としてシングルノードで動かします。

こちらは設定ファイルの用意はなく、etcd コンテナを起動するだけです。

$ mkdir -p /tmp/etcd-data.tmp

$ docker run -d \
    -p 2379:2379 \
    -p 2380:2380 \
    --mount type=bind,source=/tmp/etcd-data.tmp,destination=/etcd-data \
    --name etcd \
    --network dns-network \
    gcr.io/etcd-development/etcd:v3.4.13 \
    /usr/local/bin/etcd \
    --name s1 \
    --data-dir /etcd-data \
    --listen-client-urls http://0.0.0.0:2379 \
    --advertise-client-urls http://0.0.0.0:2379 \
    --listen-peer-urls http://0.0.0.0:2380 \
    --initial-advertise-peer-urls http://0.0.0.0:2380 \
    --initial-cluster s1=http://0.0.0.0:2380 \
    --initial-cluster-token tkn \
    --initial-cluster-state new \
    --log-level info \
    --logger zap \
    --log-outputs stderr

動作確認

CoreDNSetcd のコンテナが両方とも起動して、DNS サーバとして動作するか確認します。

まずはコンテナが起動しているか。

$ docker ps
CONTAINER ID        IMAGE                                  COMMAND                  CREATED             STATUS              PORTS                                    NAMES
8064ae23c30c        gcr.io/etcd-development/etcd:v3.4.13   "/usr/local/bin/etcd…"   2 hours ago         Up 2 hours          0.0.0.0:2379-2380->2379-2380/tcp         etcd
e0bce2b55549        coredns/coredns:1.8.0                  "/coredns -conf /roo…"   17 hours ago        Up 17 hours         0.0.0.0:53->53/tcp, 0.0.0.0:53->53/udp   coredns

コンテナ起動していたら、「etcd へ DNS レコードを登録できるか」「登録した DNS レコードを参照できるか」を確認します。

# etcd への DNS レコード登録 (OK が返ってくれば成功)
$ docker exec -it etcd etcdctl put /skydns/com/example/hoge '{"host":"192.168.1.254","ttl":3600}'
OK

# DNS レコード問い合わせ (上記で設定したアドレスが返ってくれば成功)
$ dig +short hoge.example.com @localhost
192.168.2.254

無事に etcd を使った DNS サービスが動作しているようです。

次に、/etc/hosts が参照できるか確認します。

# /etc/hosts へレコード追加
$ sudo sh -c "echo '192.168.1.253 fuga.example.com' >> /etc/hosts"

# DNS レコード問い合わせ (上記で設定したアドレスが返ってくれば成功)
$ dig +short fuga.example.com @localhost

こちらは期待した結果が返ってきませんでした。 どうやら /etc/hosts の編集内容がコンテナのほうに同期されていないようです。 理由は分かっていないのですが、コンテナを再起動することで反映されました。

$ docker restart coredns
coredns
$ dig +short fuga.example.com @localhost
192.168.1.253

これで DNS サーバの構築が完了しました。

MetalLB

MetalLB とは、AKS や GKE などのマネージド k8s と同じようにベアメタル k8s クラスタでも type: LoadBalancer の Service リソースを使えるようにしてくれるロードバランサーの実装です。 k8s クラスタに MetalLB をデプロイすることで、type: LoadBalancer のリソースに対して自動的に外部 IP アドレスを払い出してくれます。

今回は MetalLB のデプロイ方法は省略します。

ExternalDNS

いよいよ本命の ExternalDNS です。 ExternalDNS を利用することで、Kubernetes の Service や Ingress で公開される IP アドレスを Kubernetes クラスタ外部の DNS プロバイダに自動登録して、FQDNKubernetes クラスタ内のサービスにアクセスできるようになります。

執筆時点の v0.7.3 で選択できる DNS プロバイダ は 27 個あります。 Azure DNS や Route 53 など、対応している DNS プロバイダは他にもあるのですが、オンプレに構築していみたいという目的から CoreDNS を選択しました。

デプロイ

DNS プロバイダ毎にチュートリアルが用意されています。 CoreDNS 用のチュートリアルはこちら。

github.com

CoreDNS と etcd は既に構築しているため ExternalDNS のデプロイのみ参照しました。 以下のマニフェストをデプロイします。

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: external-dns
rules:
  - apiGroups: [""]
    resources: ["services","endpoints","pods"]
    verbs: ["get","watch","list"]
  - apiGroups: ["extensions","networking.k8s.io"]
    resources: ["ingresses"]
    verbs: ["get","watch","list"]
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["list"]
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: external-dns-viewer
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: external-dns
subjects:
  - kind: ServiceAccount
    name: external-dns
    namespace: kube-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: kube-system
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: external-dns
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
      - name: external-dns
        image: k8s.gcr.io/external-dns/external-dns:v0.7.3
        args:
        - --source=service
        - --provider=coredns
        - --log-level=debug
        env:
        - name: ETCD_URLS
          value: http://192.168.2.3:2379

これにより、namespace: kube-system で ExternalDNS の Pod が動くようになりました。

アノテーション付与

ExternalDNS で対象とする Service リソースにアノテーションを付与します。 これにより、ExternalDNS がアノテーション内のホスト名と Service が持つ 外部 IP アドレスを CoreDNS へ登録してくれるようになります。

$ kubectl annotate svc <svc_name> "external-dns.alpha.kubernetes.io/hostname=<svc_name>.example.com."
service/<svc_name> annotated

動作確認

DNS サーバ構築時と同じく dig で確認します。 サンプルで動かしている WordPress独自ドメインを対象にしてみます。

# Service 確認
$ kubectl get svc -n wordpress
NAME              TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)        AGE
wordpress         LoadBalancer   10.110.128.227   192.168.2.247   80:30446/TCP   56d
wordpress-mysql   ClusterIP      None             <none>          3306/TCP       56d

# アノテーション付与
$ kubectl annotate svc wordpress "external-dns.alpha.kubernetes.io/hostname=wordpress.nnstt1.work." -n wordpress
service/wordpress annotated

# dig
$ dig +short wordpress.nnstt1.work
192.168.2.247

無事に外部 IP アドレスを参照することができました。 キャプチャは無いのですが、別マシンのブラウザから WordPress を表示することもできました。

これで、IP アドレスを使わずに FQDNk8s クラスタのサービスにアクセスすることができるようになりました。

おわりに

以上が k8s クラスタのサービスに FQDN で繋がるようにした手順です。

ExternalDNS を使うことがメインの目的だったのですが、CoreDNS と etcd を触る機会も得られました。

ExternalDNS も思ってたより簡単に導入できたので、同じような悩みを持っている方はぜひ検討してみてください。

アノテーション付与を手動でする必要がある、という課題があるので、今後は自動的にアノテーションを付与する仕組みを調べたいと思います。

ArgoCD で Kiali Operatr アプリケーションを削除できない場合の対処

ArgoCD に作成した KIali Operator アプリケーションが削除できなくなってしまった問題に対処しました。

再現

Kiali Operator アプリケーション作成

ArgoCD に Kiali Operator のアプリケーションを作成します。

f:id:nnstt1:20200918063242p:plain

f:id:nnstt1:20200918063302p:plain

f:id:nnstt1:20200918063325p:plain

f:id:nnstt1:20200919065533p:plain

作成できました。

アプリケーション削除

ArgoCD から先ほど作成した Kiali Operator アプリケーションを削除します。

下図のように Deleting のまま残ってしまいました(環境に依るかもしれません)。

f:id:nnstt1:20200917065104p:plain

対処

リソースの確認

Kiali Operator で作成されたリソースが残っていないか確認します。

$ kubectl get all -n kiali-operator
No resources found in kiali-operator namespace.

$ kubectl get all -n istio-system
NAME                                       READY   STATUS    RESTARTS   AGE
pod/istio-egressgateway-b9d46896-mb6np     1/1     Running   0          6d5h
pod/istio-ingressgateway-dc76747bf-vk9g5   1/1     Running   0          6d5h
pod/istiod-69c88fcb8-d25rl                 1/1     Running   0          6d5h

NAME                           TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)                                                                      AGE
service/istio-egressgateway    ClusterIP      10.109.88.14    <none>          80/TCP,443/TCP,15443/TCP                                                     9d
service/istio-ingressgateway   LoadBalancer   10.105.166.93   192.168.2.245   15021:30988/TCP,80:31835/TCP,443:31962/TCP,31400:31141/TCP,15443:32537/TCP   9d
service/istiod                 ClusterIP      10.108.38.61    <none>          15010/TCP,15012/TCP,443/TCP,15014/TCP,853/TCP                                9d

NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/istio-egressgateway    1/1     1            1           9d
deployment.apps/istio-ingressgateway   1/1     1            1           9d
deployment.apps/istiod                 1/1     1            1           9d

NAME                                             DESIRED   CURRENT   READY   AGE
replicaset.apps/istio-egressgateway-b9d46896     1         1         1       9d
replicaset.apps/istio-ingressgateway-dc76747bf   1         1         1       9d
replicaset.apps/istiod-69c88fcb8                 1         1         1       9d

$ kubectl get crd
NAME                                          CREATED AT
...(snip)...
kialis.kiali.io                               2020-09-17T21:34:39Z
...(snip)...

Pods や Services は無くなっていますが、CRD (Custom Resource Definition) kialis.kiali.io が残ってしまっています。

$ kubectl describe crd kialis.kiali.io
Name:         kialis.kiali.io
Namespace:    
Labels:       <none>
Annotations:  <none>
API Version:  apiextensions.k8s.io/v1
Kind:         CustomResourceDefinition
Metadata:
  Creation Timestamp:  2020-09-17T21:34:39Z
  Generation:          1
  Managed Fields:
    API Version:  apiextensions.k8s.io/v1beta1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:spec:
        f:conversion:
          .:
          f:strategy:
        f:group:
        f:names:
          f:kind:
          f:listKind:
          f:plural:
          f:singular:
        f:preserveUnknownFields:
        f:scope:
        f:subresources:
          .:
          f:status:
        f:version:
        f:versions:
      f:status:
        f:storedVersions:
    Manager:      argocd-application-controller
    Operation:    Update
    Time:         2020-09-17T21:34:39Z
    API Version:  apiextensions.k8s.io/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:status:
        f:acceptedNames:
          f:kind:
          f:listKind:
          f:plural:
          f:singular:
        f:conditions:
    Manager:         kube-apiserver
    Operation:       Update
    Time:            2020-09-17T21:34:39Z
  Resource Version:  4465211
  Self Link:         /apis/apiextensions.k8s.io/v1/customresourcedefinitions/kialis.kiali.io
  UID:               a36f4355-483b-4a20-92cb-37a2f5a91902
Spec:
  Conversion:
    Strategy:  None
  Group:       kiali.io
  Names:
    Kind:                   Kiali
    List Kind:              KialiList
    Plural:                 kialis
    Singular:               kiali
  Preserve Unknown Fields:  true
  Scope:                    Namespaced
  Versions:
    Name:     v1alpha1
    Served:   true
    Storage:  true
    Subresources:
      Status:
Status:
  Accepted Names:
    Kind:       Kiali
    List Kind:  KialiList
    Plural:     kialis
    Singular:   kiali
  Conditions:
    Last Transition Time:  2020-09-17T21:34:39Z
    Message:               no conflicts found
    Reason:                NoConflicts
    Status:                True
    Type:                  NamesAccepted
    Last Transition Time:  2020-09-17T21:34:39Z
    Message:               the initial names have been accepted
    Reason:                InitialNamesAccepted
    Status:                True
    Type:                  Established
  Stored Versions:
    v1alpha1
Events:  <none>

CRD 削除

残ってしまった CRD kialis.kiali.io を削除します。

$ kubectl delete crd kialis.kiali.io
customresourcedefinition.apiextensions.k8s.io "kialis.kiali.io" deleted

返ってきません…。 ArgoCD も依然として Deleting です。

CRD の状態を見てみます。

$ kubectl describe crd kialis.kiali.io
Name:         kialis.kiali.io
Namespace:    
Labels:       <none>
Annotations:  <none>
API Version:  apiextensions.k8s.io/v1
Kind:         CustomResourceDefinition
Metadata:
  Creation Timestamp:  2020-09-17T21:34:39Z
  Deletion Timestamp:  2020-09-18T21:28:07Z
  Finalizers:
    customresourcecleanup.apiextensions.k8s.io
  Generation:  1

...(snip)...

  Conditions:
    Last Transition Time:  2020-09-17T21:34:39Z
    Message:               no conflicts found
    Reason:                NoConflicts
    Status:                True
    Type:                  NamesAccepted
    Last Transition Time:  2020-09-17T21:34:39Z
    Message:               the initial names have been accepted
    Reason:                InitialNamesAccepted
    Status:                True
    Type:                  Established
    Last Transition Time:  2020-09-18T21:28:07Z
    Message:               CustomResource deletion is in progress
    Reason:                InstanceDeletionInProgress
    Status:                True
    Type:                  Terminating
  Stored Versions:
    v1alpha1
Events:  <none>

Metadata に Finalizers customresourcecleanup.apiextensions.k8s.io が追加されています。 また、Conditions を見ると、削除処理は実行中のようです。

この状態で CRD が残り続けてしまったので、強制的に削除します。

$ kubectl patch crd kialis.kiali.io -p '{"metadata":{"finalizers":[]}}' --type=merge
customresourcedefinition.apiextensions.k8s.io/kialis.kiali.io patched

$ kubectl get crd kialis.kiali.io
Error from server (NotFound): customresourcedefinitions.apiextensions.k8s.io "kialis.kiali.io" not found

CRD を削除できたようです。

このタイミングで、ArgoCD に残り続けていた Kiali Operator アプリケーションも削除完了しました。

おわりに

無事に Kiali Operator アプリケーションを削除することができました。

しかし、なぜ CRD が残り続けてしまったかを把握できていません。

Kubernetes は奥が深いようなのでじっくり勉強していきます。

参考にしたリンク

Extend the Kubernetes API with CustomResourceDefinitions | Kubernetes

https://github.com/kubernetes/kubernetes/issues/60538

朝活_20200917

ArgoCD 動かなくて 1 週間停滞してしまいました。

やったこと

ArgoCD にアプリケーション作成

まずは k8s クラスタの土台となる manifests 群を GitHub から Pull できるように、ArgoCD にアプリケーション作成。

  • MetalLB
  • Rook-ceph
  • Prometheus

Istio はインストール方法が上記と異なるので未対応。 Helm からインストールできる?(未確認)

またサンプルアプリケーションとして、WordPress と Istio の guestbook-ui をデプロイ。

Kiali インストール

Istio で構築したサービスメッシュを観るため Kiali を導入。

Helm から Kiali Operator を入れられるということで ArgoCD 経由で入れようとしたが、うまくデプロイできなかったので削除。

しかし延々と Deleting 状態…。

f:id:nnstt1:20200917065104p:plain

Grafana のデータ永続化

kube-prometheus についてきた Grafana は ストレージとして emptyDir 使っていたので、Pod 削除すると認証情報とかが吹き飛ぶ。

Rook-ceph で用意した storageclass を指定した persistentVolumeClaim を作成して、Grafana のデータを永続化。

わかったこと

ArgoCD で GitOps を簡単に体験できる

git に登録した manifests を変更したら、自動的に k8s クラスタの状態も変更される。 これはとても便利!

Istio 入れただけではサービスメッシュの観測はできない

KialiJaeger を使う必要があるみたい。

自宅ラボでの目標が不明瞭

とりあえず自宅ラボに k8s クラスタ立てて触ってるけど、何をしたいのか分からなくなっている。

明確な目標(指標)立てたほうが良いのでは。

次にやること

  • Grafana 以外のデータ永続化
  • ArgoCD から Kiali 消せない問題の解決
  • 自宅ラボの目標決め

朝活_20200910

やったこと

# ceph dashboard
$ kubectl apply -f rook/cluster/examples/kubernetes/ceph/dashboard-loadbalancer.yaml
$ kubectl -n rook-ceph get secret rook-ceph-dashboard-password -o jsonpath="{['data']['password']}" | base64 --decode && echo
MetalLB で払い出された Exernal IP にブラウザでアクセス
[f:id:nnstt1:20200910063407p:plain]
  • ArgoCD CLI ツール インストール
# argocd cli
    $ VERSION=$(curl --silent "https://api.github.com/repos/argoproj/argo-cd/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
    $ sudo curl -SL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/$VERSION/argocd-linux-amd64
    $ sudo chmod +x /usr/local/bin/argocd
    </code></pre>
    admin パスワード変更
    <pre style="background-color: rgb(40, 44, 52) !important;"><code class="bash"># change admin pass
    $ kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2
    $ argocd login <Exernal IP>
    $ argocd account update-password
Web Console ログイン
[f:id:nnstt1:20200910064509p:plain]
サンプルアプリ デプロイ
[f:id:nnstt1:20200910065126p:plain]
[f:id:nnstt1:20200910065153p:plain]
[f:id:nnstt1:20200910065216p:plain]

うまく動かず…

[f:id:nnstt1:20200910065430p:plain]
  • Istio インストール
    $ curl -L https://istio.io/downloadIstio | sh -
    $ cd istio-1.7.0
    $ export PATH="$PATH:/home/nnstt1/istio/istio-1.7.0/bin"
    $ istioctl x precheck
    $ istioctl install --set profile=demo
    $ kubectl label namespace default istio-injection=enabled
サンプルアプリ デプロイ
# sample
    $ kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml
    $ kubectl exec "$(kubectl get pod -l app=ratings -o jsonpath='{.items[0].metadata.name}')" -c ratings -- curl -s productpage:9080/productpage | grep -o "<title>.*</title>"
    $ kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml
    $ istioctl analyze
    Warn [IST0103] (Pod busybox-sleep-less.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.
    Warn [IST0103] (Pod busybox-sleep.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.
    Warn [IST0103] (Pod wordpress-7b989dbf57-6956z.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.
    Warn [IST0103] (Pod wordpress-mysql-6965fc8cc8-k76t4.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.
    Info [IST0118] (Service wordpress.default) Port name  (port: 80, targetPort: 80) doesn't follow the naming convention of Istio port.
    Error: Analyzers found issues when analyzing namespace: default.
    See https://istio.io/docs/reference/config/analysis for more information about causes and resolutions.

わかったこと

  • ArgoCD がそのままでは動かない
    • kubectl logs -f -n argocd argocd-server-xxxx でログを見ても、エラー内容をよくわかっていない
  • Istio の概要はわかっていても使い方を把握できていない

次にやること

  • ArgoCD サンプルアプリを動くようにする

やり残し

  • Grafana Dashboard インポートできない事象の調査
  • Istio の使い方を調べる

参照したリンク

Rook Docs

Getting Started - Argo CD - Declarative GitOps CD for Kubernetes

Istio / Getting Started

追記

今日の記録を Twitter に流したところ、@zaki_hmkc さんから Istio のことについて教えていただきました!ありがとうございます!

Pod 削除前

Istio 入れる前から動かしていた namespace: default の Pod たちで Warn が出ています。

$ kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
busybox-sleep                      1/1     Running   0          26h
busybox-sleep-less                 1/1     Running   94         26h
details-v1-79c697d759-hb47t        2/2     Running   0          87m
productpage-v1-65576bb7bf-lr2tx    2/2     Running   0          87m
ratings-v1-7d99676f7f-nd8hr        2/2     Running   0          87m
reviews-v1-987d495c-w7png          2/2     Running   0          87m
reviews-v2-6c5bf657cf-98dnp        2/2     Running   0          87m
reviews-v3-5f7b9f4f77-tr4j9        2/2     Running   0          87m
wordpress-7b989dbf57-6956z         1/1     Running   0          3d1h
wordpress-mysql-6965fc8cc8-k76t4   1/1     Running   0          3d1h

$ istioctl analyze
Warn [IST0103] (Pod busybox-sleep-less.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.
Warn [IST0103] (Pod busybox-sleep.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.
Warn [IST0103] (Pod wordpress-7b989dbf57-6956z.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.
Warn [IST0103] (Pod wordpress-mysql-6965fc8cc8-k76t4.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.
Info [IST0118] (Service wordpress.default) Port name  (port: 80, targetPort: 80) doesn't follow the naming convention of Istio port.
Error: Analyzers found issues when analyzing namespace: default.
See https://istio.io/docs/reference/config/analysis for more information about causes and resolutions.

WordPress Pod 削除

Rook のサンプルとして使っていた WordPress Pod を削除してみます。

$ kubectl delete pods wordpress-7b989dbf57-6956z
pod "wordpress-7b989dbf57-6956z" deleted

$ kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
busybox-sleep                      1/1     Running   0          26h
busybox-sleep-less                 1/1     Running   94         26h
details-v1-79c697d759-hb47t        2/2     Running   0          87m
productpage-v1-65576bb7bf-lr2tx    2/2     Running   0          87m
ratings-v1-7d99676f7f-nd8hr        2/2     Running   0          87m
reviews-v1-987d495c-w7png          2/2     Running   0          87m
reviews-v2-6c5bf657cf-98dnp        2/2     Running   0          87m
reviews-v3-5f7b9f4f77-tr4j9        2/2     Running   0          87m
wordpress-7b989dbf57-rwwqb         2/2     Running   0          16s <- READY が 2/2 になった(削除前は 1/1)
wordpress-mysql-6965fc8cc8-k76t4   1/1     Running   0          3d1h

$ istioctl analyze
Warn [IST0103] (Pod busybox-sleep-less.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.
Warn [IST0103] (Pod busybox-sleep.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.
Warn [IST0103] (Pod wordpress-mysql-6965fc8cc8-k76t4.default) The pod is missing the Istio proxy. This can often be resolved by restarting or redeploying the workload.        <- wordpress-7b989dbf57-6956z の Warn がなくなった
Info [IST0118] (Service wordpress.default) Port name  (port: 80, targetPort: 80) doesn't follow the naming convention of Istio port.
Error: Analyzers found issues when analyzing namespace: default.
See https://istio.io/docs/reference/config/analysis for more information about causes and resolutions.

サイドカーとして istio-proxy コンテナが WordPress Pod 内で立ち上がったようです。

Pod 全削除

Warn となっていた他の Pod も削除してみました。 busybox のPod は Deployment で起動したものでは無いため、失くなってしまいました。 (restart ではなく delete してしまいました。。)

$ kubectl delete pods busybox-sleep busybox-sleep-less wordpress-mysql-6965fc8cc8-k76t4
pod "busybox-sleep" deleted
pod "busybox-sleep-less" deleted
pod "wordpress-mysql-6965fc8cc8-k76t4" deleted

$ kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
details-v1-79c697d759-hb47t        2/2     Running   0          102m
productpage-v1-65576bb7bf-lr2tx    2/2     Running   0          102m
ratings-v1-7d99676f7f-nd8hr        2/2     Running   0          102m
reviews-v1-987d495c-w7png          2/2     Running   0          102m
reviews-v2-6c5bf657cf-98dnp        2/2     Running   0          102m
reviews-v3-5f7b9f4f77-tr4j9        2/2     Running   0          102m
wordpress-7b989dbf57-rwwqb         2/2     Running   0          14m
wordpress-mysql-6965fc8cc8-t7s92   2/2     Running   0          12m <- READY が 2/2 になった(削除前は 1/1)

$ istioctl analyze
Info [IST0118] (Service wordpress.default) Port name  (port: 80, targetPort: 80) doesn't follow the naming convention of Istio port.

無事に Warn は出なくなりました。

Info メッセージが出ていますが、時間がないのはこれはまた後日確認します。。