GeekFactory

int128.hatenablog.com

Prometheus Alertmanagerの通知テンプレートを改善する

AlertmanagerのSlack通知テンプレートで四苦八苦したのでメモを残します。

Prometheus OperatorのHelm chartには便利なデフォルトルールが組み込まれています。例えば、Podが頻繁に再起動している場合に通知するルール(KubePodCrashLooping)が組み込まれています。しかし、Slackやメールなどのアラート通知は自分で設定する必要があります。

アラート通知の書き方は公式の Notification template examples | Prometheus に例がありますが、そのままではうまくメッセージが表示されない場合があります。ドキュメントでは以下の例が挙げられていますが、 summarydescription というアノテーションが付いているルールでないとメッセージが空白になってしまいます。また、CommonAnnotations を使っているため、複数のアラートがグループ化された場合は共通のアノテーションしか表示されません。

- name: 'team-x'
  slack_configs:
  - channel: '#alerts'
    # Alertmanager templates apply here.
    text: "<!channel> \nsummary: {{ .CommonAnnotations.summary }}\ndescription: {{ .CommonAnnotations.description }}"

通知テンプレートのよい例はないか探したところ、下記の記事が参考になりました。

medium.com

Prometheus Operatorのデフォルトルールには message というアノテーションが付いているため、テンプレートでは message を使うとよさそうです。また、グループ化された場合にうまく表示されない問題は Alerts フィールドに入っているアラートを列挙すると解決できそうです。

というわけで試行錯誤の結果、以下のテンプレートを利用しています。

alertmanager:
  config:
    receivers:
      - name: "null"
      - name: slack
        slack_configs:
          - api_url: https://slack.example.com/webhook
            channel: "#alerts"
            send_resolved: true
            username: dev/Alertmanager
            icon_url: https://prometheus.io/assets/favicons/favicon.ico
            title: |
              {{ .Status | toUpper }}
            text: |
              {{- range .Alerts }}
              - **{{ .Labels.alertname }}**: {{ .Annotations.message }}
              {{- end }}
    route:
      receiver: "null"
      routes:
        - receiver: "null"
          match:
            alertname: Watchdog
        - receiver: slack

Fluxによるアプリケーションの継続的デプロイ

FluxのAutomated deployment of new container imagesを利用して、Kubernetes上でアプリケーションの継続的デプロイを構成する機会があったのでまとめます。

GitOpsの基本形

GitOpsを採用する場合は下図のデプロイフローが基本形になります。

https://raw.githubusercontent.com/int128/continuous-deployment-flux-demo/master/gitops-basic-flow.svg?sanitize=true

具体的には以下の流れになります。

  1. 開発者がアプリケーションリポジトリを更新する。
  2. CIがアプリケーションをビルドし、新しいDockerイメージをプッシュする。
  3. 開発者がマニフェストリポジトリを更新する。
  4. GitOpsが新しいマニフェストをデプロイする。

これはWeaveworksのGuide To GitOpsで説明されている開発フローです。チームの大きさやアプリケーションの特性によっては、以下のような違いが出てくると思います。

継続的デプロイに必要なもの

チームが小さい、あるいは、チームが自己完結している場合は、アプリケーションを更新した契機で開発環境にデプロイされる方が効率的に開発できることがあります。ビジネス上の受入テストまで自動化している場合は本番環境まで自動デプロイすることもあります。これを継続的デプロイ(Continuous Deployment)といいます。下記の記事が参考になります。

blog.kyanny.me

継続的デプロイを実現するにはアプリケーションのビルドとデプロイを連続して行う必要があります。Cloud FoundryのようなPaaSを利用するとこのような開発フローを簡単に実現できますが、KubernetesのようなCaaSでは仕組みを作る必要があります。具体的には、以下が自動的に流れる仕組みが必要になります。

  1. 開発者がアプリケーションリポジトリを更新する。
  2. CIがアプリケーションをビルドし、新しいDockerイメージをプッシュする。
  3. (何らかの仕組みで)マニフェストリポジトリにあるマニフェストのイメージタグを書き換える。
  4. GitOpsが新しいマニフェストクラスタに適用する。

今のところ、GitOpsで継続的デプロイを実現するにはFluxを利用するか、上記3を自作する必要があります。Argo CDでは実現できません。

Flux automated deployment of new container images

FluxにはAutomated deployment of new container imagesという機能があります。この機能を有効にすると、Dockerレジストリに新しいイメージが存在したらマニフェストのイメージタグを新しいものに書き換えてくれます。ちょうど下図のようなデプロイフローになります。

https://raw.githubusercontent.com/int128/continuous-deployment-flux-demo/master/gitops-continuous-deployment-flow.svg?sanitize=true

具体的には以下の流れになります。

  1. Gitリポジトリからマニフェストを取得する。
  2. Dockerレジストリにあるイメージをスキャンする。
  3. 新しいタイムスタンプのイメージが存在する場合は以下を行う。
    1. マニフェストに書かれているイメージタグを新しいものに書き換える。
    2. Gitリポジトリに新しいコミットをpushする。
  4. 新しいマニフェストクラスタに適用する。

公式ドキュメントには詳細が書かれていないので、Fluxのログを元に動作を推測しています。間違っていたら教えてください。

Automated deployment of new container imagesを有効にするには、Deploymentに以下のアノテーションを付加します。

metadata:
  annotations:
    fluxcd.io/automated: "true"

スキャン対象のイメージタグを正規表現やglobで絞り込むことも可能です。以下に例を示します。

metadata:
  annotations:
    fluxcd.io/automated: "true"
    # appコンテナについて、master-で始まるイメージタグのみ自動更新とする
    fluxcd.io/tag.app: glob:master-*
spec:
  template:
    spec:
      containers:
      - name: app
        image: ...

スキャン対象のイメージ名やDockerレジストリを限定することも可能です。詳しくはfluxdの引数リストを参照してください。

下記のリポジトリにデモを置いているので、よかったらご覧ください。

github.com

FlaggerによるCanary Release

参考までに、FluxとFlaggerを併用するとCanary Releaseが可能です。アプリケーションリポジトリを更新した契機でデプロイを開始し、一定の基準(例:10%のトラフィックに新しいバージョンを適用して99%が成功ステータスだった場合)を満たしたら全トラフィックを新しいバージョンに切り替える、といったことも可能です。

github.com

Flaggerを使うにはIstioなどのサービスメッシュが必要です。また、Kialiなどの可視化ツールがあるとトラフィックの切り替えがよく分かります。詳しくは下記の記事が参考になります。

medium.com

Fluxによる継続的デプロイの課題

Fluxには以下の制約があるため、実際に運用してみると辛いところがあります。

  • デプロイが成功した場合やエラーが発生した場合の通知がない。
    • WeaveworksとしてはWeave Cloudを使ってほしいみたいです。
    • fluxdのログをelastalertなどで監視してSlackに通知する仕組みを作れば可能です。
    • デプロイが成功するとflux-syncタグが更新されるので、GitHubでタグの更新をSlackに通知するという方法もあります。
    • FlaggerにはSlack通知の機能があります。
  • GUIがないので、クラスタで何が起きているのか分かりにくい。
    • クラスタで何が起きているのか確認するには以下の方法があります。
      • fluxdのログを見る。
      • fluxctlコマンドでDeploymentのステータスを見る。
      • Prometheusでfluxdのステータスを取得する。
    • Argo CDのような使うやすいGUIはありません。
  • 同期状態を管理するためにマニフェストリポジトリflux-syncというタグが作成される。
    • flux-syncタグ契機でCIが実行されないように設定するとよいです。

参考までに、fluxctlコマンドを利用すると以下のようなステータスを表示できます。

% fluxctl --k8s-fwd-ns flux -n develop list-images
WORKLOAD                         CONTAINER   IMAGE                                                                  CREATED
develop:deployment/echoserver    echoserver  gcr.io/google_containers/echoserver
                                             '-> 1.10                                                               22 Mar 18 17:30 UTC
                                                 1.9                                                                14 Feb 18 22:35 UTC
                                                 1.8                                                                27 Jul 17 18:50 UTC
                                                 1.7                                                                18 Jul 17 19:23 UTC
                                                 1.6                                                                16 Jun 17 22:41 UTC
                                                 1.5                                                                06 Jun 17 23:28 UTC
                                                 1.4                                                                29 May 16 05:03 UTC
                                                 1.3                                                                08 Mar 16 19:10 UTC
                                                 1.2                                                                23 Feb 16 02:18 UTC
                                                 1.1                                                                26 Jan 16 18:13 UTC

デプロイの前後で自動テストなどの処理を入れたい場合は今のところ方法がありません。Tekton CDでは柔軟なデプロイメントパイプラインを構成できるので、今後Tekton CDのエコシステムが発達してきたら有力候補になるかもしれませんね。

本物のクラスタを利用してkubectl pluginをテストする

kubectlのプラグインを開発していると、ユニットテストだけでなく、本物のKubernetesクラスタを利用したテストが欲しくなります。プラグインの振る舞いが複雑な場合は自動テストがあると安心してリリースできます。

本稿では、本物のKubernetesクラスタを利用してkubectlプラグインをテストする方法を考えます。

テストの基本形

本物のクラスタを利用してプラグインをテストするための構成を下図に示します。

https://github.com/int128/kubectl-tree-e2e-test

必要なのは以下の3つです。

テストの流れは以下のようになります。

  1. クラスタを作成する。
  2. 必要なリソースをデプロイする。
  3. kubectlを実行する。
  4. 間接的にkubectl pluginが実行される。
  5. 実行結果が期待通りか検証する。

このようなテストはプラグインのリリース前に手動でやっていることが多いと思います。自動テストを導入することで、

  • リリース前ではなくPull Requestの契機で不具合を検出できる
  • クリーンな環境でテストできるので信頼性が高い
  • 手動では手間のかかる組合せテストが可能になる(複数のクラスタバージョンなど)

といったメリットがあります。

例: kubectl-treeのテストを書いてみる

ここでは、kubectl-treeというプラグインをテストする例を考えます。これはクラスタにあるリソースを木構造で表示してくれる便利なプラグインです。

簡単のため、テストシナリオはMakefileで書くことにします。Makefileをよく知らない方はシェルスクリプトだと思って読んでみてください。

クラスタの作成とリソースのデプロイ

まずは新しいクラスタを作成するターゲットを定義します。Kindを利用すると、Docker上に簡単にクラスタを作成できます。

CLUSTER_NAME := kubectl-tree-e2e-test
OUTPUT_DIR := $(CURDIR)/output

KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
export KUBECONFIG

.PHONY: cluster
cluster: $(OUTPUT_DIR)/kubeconfig.yaml
$(OUTPUT_DIR)/kubeconfig.yaml:
  kind create cluster --name $(CLUSTER_NAME)
  kubectl cluster-info

.PHONY: delete-cluster
delete-cluster:
  kind delete cluster --name $(CLUSTER_NAME)
   -rm $(KUBECONFIG)

これでmakeを実行すると新しいクラスタが作成される仕組みができました。実行にはDockerとKindが必要です。

% make
kind create cluster --name kubectl-tree-e2e-test
Creating cluster "kubectl-tree-e2e-test" ...
 ✓ Ensuring node image (kindest/node:v1.17.0) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kubectl-tree-e2e-test"
You can now use your cluster with:

kubectl cluster-info --context kind-kubectl-tree-e2e-test

Not sure what to do next? 😅 Check out https://kind.sigs.k8s.io/docs/user/quick-start/
kubectl cluster-info
Kubernetes master is running at https://127.0.0.1:32771
KubeDNS is running at https://127.0.0.1:32771/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

% make delete-cluster
kind delete cluster --name kubectl-tree-e2e-test
Deleting cluster "kubectl-tree-e2e-test" ...
rm /kubectl-tree-e2e-test/output/kubeconfig.yaml

テストを行うにはクラスタだけでなく適当なリソースも必要です。ここではechoserverをデプロイすることにします。以下のようにkubectl applyコマンドを追記します。

.PHONY: cluster
cluster: $(OUTPUT_DIR)/kubeconfig.yaml
$(OUTPUT_DIR)/kubeconfig.yaml:
  # create a cluster
  kind create cluster --name $(CLUSTER_NAME)
  kubectl cluster-info
  # deploy the echoserver
  kubectl apply -f fixture.yaml
  # wait for the deployment
  kubectl -n echoserver rollout status deployment/echoserver

これで、テストに必要なクラスタとリソースが揃いました。

プラグインの実行

kubectl treeコマンドを実行すると以下のような結果が表示されます。

% kubectl tree -n echoserver deployment echoserver
NAMESPACE   NAME                                  READY  REASON  AGE
echoserver  Deployment/echoserver                 -              59s
echoserver  └─ReplicaSet/echoserver-5d8cc8d48   -              48s
echoserver    └─Pod/echoserver-5d8cc8d48-bcvs4  True           46s

ここでは簡単のため、grepで必要な文字列が表示されているかチェックします。もっと複雑な条件判定が必要な場合はスクリプトを書く方がよいでしょう。

.PHONY: test
test: cluster
  # run kubectl-tree
  kubectl tree -n echoserver deployment echoserver | tee $(OUTPUT_DIR)/actual
  # make sure the output contains the expected lines
  egrep --color '^echoserver +Deployment/echoserver' $(OUTPUT_DIR)/actual
  egrep --color '^echoserver +└─ReplicaSet/echoserver-' $(OUTPUT_DIR)/actual
  egrep --color '^echoserver +└─Pod/echoserver-' $(OUTPUT_DIR)/actual

makeを実行してみましょう。以下のようにテストに合格するはずです。

% make
# run kubectl-tree
kubectl tree -n echoserver deployment echoserver | tee /kubectl-tree-e2e-test/output/actual
NAMESPACE   NAME                                  READY  REASON  AGE
echoserver  Deployment/echoserver                 -              5m4s
echoserver  └─ReplicaSet/echoserver-5d8cc8d48   -              4m56s
echoserver    └─Pod/echoserver-5d8cc8d48-nk47g  True           4m55s
# check actual output
egrep '^echoserver +Deployment/echoserver' output/actual
echoserver  Deployment/echoserver                 -              5m4s
egrep '^echoserver +└─ReplicaSet/echoserver-' output/actual
echoserver  └─ReplicaSet/echoserver-5d8cc8d48   -              4m56s
egrep '^echoserver +└─Pod/echoserver-' output/actual
echoserver    └─Pod/echoserver-5d8cc8d48-nk47g  True           4m55s

これでローカルでのテストは完了です。

CIで実行する

品質を継続的に向上させるにはCIが不可欠です。ここではGitHub Actionsでテストを実行します。

GitHub ActionsのUbuntu 18.04にはすでにKindがインストールされていますが、バージョンが古いので改めて最新版をインストールすることにします。CIの最初のステップでは以下のツールをインストールします。

  • Kind
  • krew
  • kubectl-tree

それからmakeを実行します。

最終的に、workflowは以下のようになります。

name: test
on: [push]
jobs:
  build:
    name: test
    # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners#ubuntu-1804-lts
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1
      # https://kind.sigs.k8s.io/docs/user/quick-start/
      - run: |
          wget -q -O ./kind "https://github.com/kubernetes-sigs/kind/releases/download/v0.7.0/kind-linux-amd64"
          chmod +x ./kind
          sudo mv ./kind /usr/local/bin/kind
          kind version
      # https://github.com/kubernetes-sigs/krew
      - run: |
          (
            set -x; cd "$(mktemp -d)" &&
            curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/download/v0.3.3/krew.{tar.gz,yaml}" &&
            tar zxvf krew.tar.gz &&
            KREW=./krew-"$(uname | tr '[:upper:]' '[:lower:]')_amd64" &&
            "$KREW" install --manifest=krew.yaml --archive=krew.tar.gz &&
            "$KREW" update
          )
      # https://github.com/ahmetb/kubectl-tree
      - run: PATH=$PATH:$HOME/.krew/bin kubectl krew install tree
      - run: PATH=$PATH:$HOME/.krew/bin make

ここまでの内容は下記のリポジトリにまとめてあります。CIの実行時間や実行結果が気になる方はぜひご覧ください。

github.com

例:kubeloginの受け入れテスト

拙作のkubeloginでは、本物のKubernetesクラスタOpenID Connect OPを利用したテストを下図の構成で行っています。

https://github.com/int128/kubelogin

テストを支える裏方の仕組みは https://github.com/int128/kubelogin/tree/master/acceptance_test をご覧ください。

まとめ

DockerとKindを利用すると、本物のクラスタを用いたkubectl pluginのテストを実現できます。