koukiblog

たぶんweb系の話題

Djangoのユニットテスト内で発行されたクエリをキャプチャする

CaptureQueriesContextを利用すればクエリをキャプチャできたのでメモ。

こんな感じで使える

from django.db import connection
from django.test.utils import CaptureQueriesContext


with CaptureQueriesContext(connection) as ctx:
  # test method

  for q in ctx.captured_queries:
    print(f"time{q['time']}, sql:{q['sql']}")
    print(len(ctx.captured_queries))

HelmでCRDを利用するときの注意

HelmでCRDを扱う際にいくつか注意点があったので備忘のため残しておきます

helmで管理しているリソースのCRD、APIVersionが廃止されると、chart内のCRD,APIVersionをアップデートしてもhelm upgrade時にエラーになります。

たとえば、Kubernetes 1.16から下記のようなDeploymentのapps/v1beta1が削除されたり

apiVersion: apps/v1beta1
kind: Deployment

CertManagerのようなCRDを利用しているツールを導入している場合、バージョンアップに伴いCRDが廃止されたりします。CertManagerでは、1.7でv1beta1が廃止されています。

Release 1.7 - cert-manager Documentation

CRD, APIVersionの廃止までには移行期間があるので、この期間内にマニフェストのアップデートを行えば問題ありません。様々な事情でアップデートできないまま廃止されるとマニフェストを更新しても HelmによるUpgradeができなくなります。これはHelmのアーキテクチャによる制約です。

なぜ?

Helmはリソースの更新を行う場合、デプロイ済みのreleaseと、デプロイされる予定の次のreleaseの差分を確認して、それを反映しています。Helmは、Goのテンプレートを展開してkubectl applyしてるだけではないんです。

そのため、デプロイ済みのrelease内に廃止されたCRD, APIVersionが含まれているとそれがパースできなくなりエラーになります。

Helmのコード見ると、現在のリソースとデプロイ予定のリソースを比較しているコードが確認できます。 github.com

特にこのループでは、orignialにあってtargetに存在しないリソースを削除していて、これは kubectl apply とは異なる挙動です。 kubectl applyではリソースの削除は行われないため、ある程度の期間運用しているとリソース削除は問題になってきます(upgradeしていくだけでリソース削除できるのはHelmを利用するメリットの1つだと思います

   for _, info := range original.Difference(target) {
        c.Log("Deleting %s %q in namespace %s...", info.Mapping.GroupVersionKind.Kind, info.Name, info.Namespace)

        if err := info.Get(); err != nil {
            c.Log("Unable to get obj %q, err: %s", info.Name, err)
            continue
        }
        annotations, err := metadataAccessor.Annotations(info.Object)
        if err != nil {
            c.Log("Unable to get annotations on %q, err: %s", info.Name, err)
        }
        if annotations != nil && annotations[ResourcePolicyAnno] == KeepPolicy {
            c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy)
            continue
        }
        if err := deleteResource(info); err != nil {
            c.Log("Failed to delete %q, err: %s", info.ObjectName(), err)
            continue
        }
        res.Deleted = append(res.Deleted, info)
    }

対応

上記のような状態になると、releaseを更新するしかありません。以前はreleaseを手元にダウンロードして編集するしか方法がなかったのですが、現在はHelmが便利なpluginを提供してくれています。

mapkubeapis というHelm Pluginを利用することで簡単にreleaseを更新することができます。

GitHub - helm/helm-mapkubeapis: This is a Helm plugin which map deprecated or removed Kubernetes APIs in a release to supported APIs

たとえば、cert-manager.io/v1alpha2 のCertificateリソースを、cert-manager.io/v1 に置き換えたい場合

下記のようなyamlを作成してmap.yamlとして保存します。(depreactedInVersion, remvedInVersionは置き換えが発生するように適当に設定すればok)

mappings:
   -  deprecatedAPI: "apiVersion: cert-manager.io/v1alpha2\nkind: Certificate"
      newAPI: "apiVersion: cert-manager.io/v1\nkind: Certificate"
      deprecatedInVersion: "v1.9"
      removedInVersion: "v1.16"

作成したmap.yamlを利用してmapkubeapis を実行することでreleaseを更新することが可能です。

helm mapkubeapis #{release_name} --mapfile map.yaml

まとめ

廃止される前にマニフェストをアップデートしていくのが一番よいのですが、様々な事情でこのHelmの制約にハマってしまう方もいると思うので役に立つとうれしいです。

なお、公式のドキュメントではこのトピックについて、専用のページが設けられていて、releaseを直接編集する方法も紹介されています。

helm.sh

PreemptibleVMを利用しているGKEクラスタで "shutdown"ステータスのPodが残るようになった

PreemptibleVMを利用しているGKEクラスタで去年の夏ぐらいからshutdownステータスのPodが残るようになっていました。 ノードがシャットダウンの影響を受けたPodなんだろうなと思いつつ、調べる時間は取れてなかったので改めて時間をとって調べたのでその結果を残しておきます。

原因

KubernetesにGracefulNodeShutdownという機能が追加されており、この影響です。

https://kubernetes.io/blog/2021/04/21/graceful-node-shutdown-beta/

GKEでは、1.20.5 以降で有効化されています

https://cloud.google.com/kubernetes-engine/docs/release-notes#May_03_2021

1.20.5 以前では、PreemptibeVMがシャットダウンされた場合Podがそのまま削除されていたが、GracefulNodeShutdown機能が追加されたことにより、一定の猶予時間内にevictされるようになり、evictされたPodはshutdownステータスで残るようになる、ということのようです。

リリースされたのが2021/5/3なので、去年の夏頃から仕様が変わったという体感ともあっています。

shutdown Podへの対応

ガベージコレクション( https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-garbage-collection ) されるまではずっと残ります。 この閾値は、GKEユーザーからは変更することができないようです。かつそのデフォルト値は12500と非常に大きいです。 https://issuetracker.google.com/issues/172663707?pli=1

削除する場合、kubectl コマンドでまとめて削除することができます。GKEのドキュメント内にも記載があります。

1.21.3 以前:

kubectl get pods --all-namespaces | grep -i shutdown | awk '{print $1, $2}' | xargs -n2 kubectl delete pod -n

1.21.3 以降:

kubectl get pods --all-namespaces | grep -i NodeShutdown | awk '{print $1, $2}' | xargs -n2 kubectl delete pod -n
kubectl get pods --all-namespaces | grep -i Terminated | awk '{print $1, $2}' | xargs -n2 kubectl delete pod -n

https://cloud.google.com/kubernetes-engine/docs/how-to/preemptible-vms#graceful-shutdown

参考URL

https://stackoverflow.com/questions/68687716/k8s-pods-stuck-in-failed-shutdown-state-after-preemption-gke-v1-20

Kubernetes The Hard Wayをやった

前からずっとやろうと思った Kuberentes The Hard Way( GitHub - kelseyhightower/kubernetes-the-hard-way: Bootstrap Kubernetes the hard way on Google Cloud Platform. No scripts. ) をやっとやりました。

GCPアカウントがあったのでそこで進めたのですが、クラウドシェルで作業すべて完結できました。 GCPアカウントを新規作成していない場合は、無料枠内で出来ると思います。

途中詰まったところ

  • HOSTNAMEが取得できない

kubernetes-the-hard-way/09-bootstrapping-kubernetes-workers.md at master · kelseyhightower/kubernetes-the-hard-way · GitHub

9章で利用しているHOSTNAMEの取得方法が載っていなくてちょっと迷いました

HOSTNAME=$(curl -H Metadata-Flavor:Google http://metadata/computeMetadata/v1/instance/hostname | cut -d. -f1) で解決。

参考: google cloud platform - GCE metadata - get instance name - Server Fault

  • core-dns pod がずっとpending

手順をすべて終えたあとの動作確認で、core-dns podを作成するのですが、そこで作成したpodがずっとpendingのままという問題が起きました。Kubernetesマスターノードの運用はやったことないので、どのログ見ればいいのかもわからない状態で結構悩みました。

podをNodeに配置するところで止まってそうなので、kube-schedulerかなと思ってkube-schedulerのログを見ると、作業ミスで設定ファイルがなくて起動失敗していて、それが原因でした。

感想

GKEのようなマネージドサービスを使った運用しかしていなかったので、Masterノードがどういうコンポーネントで構成されているか少しでも体験できたのはよかったです。特にkube-schedulerの起動ミスのようなトラブルシューティングを経験できたのはよい経験になりました。

あとGCPクラウドシェルが思ってたより高機能で便利。Tmuxも使えました。普段の開発ももしかしたらクラウドシェル上で出来るかも。

Nginx Ingress Controller v1.0 でIngress リソースが作れなくなって困った話

Nginx Ingress Controllerをv0.3からV1.0系にアップデートしたのだけど、そのときにIngressリソース作成時にエラーが出るようになってはまったのでメモ。

エラーはこういうエラーが出ました

Internal error occurred: failed calling webhook "validate.nginx.ingress.kubernetes.io": Post https://ingress-nginx-controller-admission.ingress-nginx.svc:443/extensions/v1beta1/ingresses?timeout=30s: context deadline exceeded

同じエラーのIssueがありました。 github.com

Issue内にも記載がある通り、MasterノードからWorkerノードのポート:8443でのアクセスを許可すれば解決しました。(なぜかIssueは解決していなくて、0.3系にバージョンダウンしたりAdmission Webhookを無効にしたりして解決してる方もいる様子)

この問題は、GKEのプライベートクラスタなど、MasterノードからWorkerノードへのアクセスを制限しているのが原因のようです。僕が検証していた環境はGKEのプライベートクラスタだったので、ファイアウォールルールの追加が必要でした。Issueをみると様々な環境でこの問題が発生してるようなので、もしかすると他の要因もあるかもしれないですが・・

NginxIngressControllerとGKEどちらのドキュメントにも記載がありました。

For private clusters, you will need to either add an additional firewall rule that allows master nodes access to port 8443/tcp on worker nodes, or change the existing rule that allows access to ports 80/tcp, 443/tcp and 10254/tcp to also allow access to port 8443/tcp.

See the GKE documentation on adding rules and the Kubernetes issue for more detail. Installation Guide - NGINX Ingress Controller

  • GKE
このセクションでは、限定公開クラスタにファイアウォール ルールを追加する方法について説明します。デフォルトでは、クラスタ コントロール プレーンはポート 443(HTTPS)および 10250(kubelet)上のみでノードおよび Pod への TCP 接続を開始するようにファイアウォール ルールによって制限されています。一部の Kubernetes 機能では、他のポート上でアクセスを許可するためにファイアウォール ルールを追加する必要があります。

追加のファイアウォール ルールを必要とする Kubernetes 機能は次のとおりです。

アドミッション Webhook
集計 API サーバー
Webhook 変換
動的監査の構成
通常、ServiceReference フィールドを持つ API では、追加のファイアウォール ルールが必要です。

cloud.google.com

Admission Webhookはv1.0から利用されているので、アップデート時にこの問題に直面すると結構はまりやすい気はしました。アプリケーションの動作確認はしてたけど、Ingressリソース作成成功するかのような確認はしてなかったので結構あせりました。

GCPのCloudCDNにはStaleコンテンツという機能がある

GCPのCloudCDN(https://cloud.google.com/cdn) を設定したのですが、staleコンテンツという機能があるのを知らずに結構はまったので残しておきます。

Staleコンテンツ?

Staleコンテンツは、オリジンがエラーを返したときなどある条件を満たすと、CDNから期限切れの古いコンテンツを返す設定です。

cloud.google.com

cdn_cache_statusをカスタムレスポンスヘッダーに設定しておくと、"stale"というステータスになっているのでそれで判別できます。

この機能が意図せず有効になっていて、なぜ古いコンテンツが表示されるのかわからず結構悩みました。

Flux2について調べた

久しぶりにFluxを調べたらFlux2が出ていて、別物になっていたので調べたことをまとめます。

Flux2について

ArgoCDと合流して、GitOpsEngineを開発していくみたいな話になったのですが、結局両者は別れることにことになり、WeaveWorks社が出したのがFlux2 です。 Fluxをカスタムコントローラーなどを利用してリライトしたものという理解でよさそうです。 思想は引き継いでいますが、インストール方法や設定方法はFlux1とは全然違います。

変わったところ

Fluxの各機能はコンポーネント化されていて、カスタムリソースを利用して設定していく形になっています。 また、Helmも正式サポートされています。

Helmを使った例がわかりやすいので紹介すると、今までHelmを利用する場合は下記のようにhelmコマンドを使ってインストールしていたと思います。 helm repo add で chartをレポジトリに追加し、 helm installでchartをクラスタにインストールしています。

helm repo add traefik https://helm.traefik.io/traefik
helm install my-traefik traefik/traefik \
  --version 9.18.2 \
  --namespace traefik

それが、Fluxを使うとこうなります。

flux create source helm traefik --url https://helm.traefik.io/traefik
flux create helmrelease --chart my-traefik \
  --source HelmRepository/traefik \
  --chart-version 9.18.2 \
  --namespace traefik

flux create コマンドはk8sクラスタにリソースを作っているわけではなく、それぞれ、 Source, HelmRelease というFluxが定義したカスタムリソースを作成しています。 そして、そのカスタムリソースをHelm Controllerが参照してChartをクラスタにデプロイします。

Gitレポジトリからマニフェストを取得してApplyする場合は、 source として GitRepositryを定義し、それをapplyするkustomizeリソースを作成します。最初ちょっとわかりづらかったのですが、kubectl applyするだけのリソースはなく、 kustomizeリソースを利用するようです。

先ほどのHelmの例で作ったカスタムリソースは下記のようになります。これをkustomizationリソースで反映しても flux create コマンドと同じことができます。

# /flux/boot/traefik/helmrepo.yaml
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
  name: traefik
  namespace: traefik
spec:
  interval: 1m0s
  url: https://helm.traefik.io/traefik
# /flux/boot/traefik/helmrelease.yaml
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: traefik
  namespace: traefik
spec:
  chart:
    spec:
      chart: traefik
      sourceRef:
        kind: HelmRepository
        name: traefik
      version: 9.18.2
  interval: 1m0s

GitOpsツール自体の設定の可視化は課題だと思っていたので、カスタムリソースで定義できるのは非常によさそうです。

Configレポジトリのサンプル

マニフェストを保存するConfigレポジトリのサンプルが提供されていました。これをベースにカスタマイズしていくのがよさそうかなと思います。

github.com

ArgoCD との比較

こちらにまとまっていました。個人的にはArgoCDは権限周りが複雑そうで、k8sのRBACをそのまま使えるFluxが好みです。

ArgoCD と Flux2 の比較