Kubernetesで同名の異なる内容のリソースを同時にapplyしたときの挙動について調べてみた

はじめに

 Kubernetesではアプリケーションの実行や,そのアプリケーションを外部に公開するための設定の適用,Kubernetesクラスタ(以下クラスタと表記)内の権限管理など多くの操作を"リソース"と呼ばれるものを用いて行います.
例えば下のyaml形式のテキストはKubernetesにおけるアプリケーションの最小実行単位であるPodを定義したマニフェストです.

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: demo
  name: demo
spec:
  containers:
  - image: nginx:latest
    name: nginx
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Never

Kubernetesの使用者はこのようなマニフェストAPIサーバに渡してあげることで様々な操作を行います.

 さてこのマニフェストAPIサーバに渡してあげる方法ですが,これにはいくつかの方法が用意されています. そのうちおそらく誰もが使ったことがあるであろう方法がkubectlと呼ばれるCLIツールを用いる方法です.
 このkubectlにはクラスタを操作するためのサブコマンドがいくつか用意されていますが,リソースの作成に関するコマンドの一つがkubectl applyです. このapplyコマンドでは作成対象のリソースがクラスタになければ新しく作成し,もしあった場合は変更部分を更新します.
実行すると以下のような結果が出力されます.

$ kubectl apply -f demo-pod.yaml
pod/demo created
# 使用イメージのタグを変更したものをapplyする
$ kubectl apply -f demo-pod-changed.yaml
pod/demo configured

ところでこのapplyコマンドを下のように同時に実行したらどうなるのでしょう?

# pod-changed.yamlはpod.yaml内の使用イメージタグのみを変更したもの
$ kubectl apply -f pod.yaml & kubectl apply -f pod-changed.yaml

おそらく結果として考えられるの以下の3つです.

  1. どっちかがcreateされて,どっちかがconfiguredされる
  2. 同じ名前の違う内容のリソース(ここではPod)が複数作成される
  3. エラーで失敗する

さて結果はどうなるのでしょうか?
確認してみましょう!

kubectl applyを同時に2個実行してみた

 実行結果を貼り付けます.

$ kubectl apply -f pod.yaml  & ./kubectl apply -f pod-changed.yaml
pod/demo created
Error from server (AlreadyExists): error when creating "pod.yaml": pods "demo" already exists

どうやらpod.yamlに記載されているPodリソースを作成時にAPIサーバ内でエラーが発生したようです.1
というわけで正解は3. エラーで失敗するでした.

ところで先ほどapplyコマンドについて以下のような説明をしました.

このapplyコマンドでは作成対象のリソースがクラスタになければ新しく作成し,もしあった場合は変更部分を更新します.

にも関わらずAlready Existsで失敗するのは何故でしょう?
すでに存在しているのがわかったなら更新してくれよという気持ちになる人も多いと思います. 少なくとも僕はそんな気持ちです.

どうしてこんなことになるのか,次は実際にコードを見てみることにしましょう.

kubectl apply のコードを見てみる

 幸いKubernetesのコードはGitHubで誰でも見れるようになっているのでこれを見ていきます.

github.com

まずはkubectlコマンドのコードリーディングの入口を確認します.

kubernetes/cmd/kubectl/kubectl.go L35-L52

func main() {
    rand.Seed(time.Now().UnixNano())

    command := cmd.NewDefaultKubectlCommand()

NewDefaultKubectlCommand()を読み進めていくとapplyコマンドの本体が見えてきます.
この中にo.RUN()というapply処理の本体ぽいものが見られます.
kubernetes/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go L162-L206

Run: func(cmd *cobra.Command, args []string) {
            cmdutil.CheckErr(o.Complete(f, cmd))
            cmdutil.CheckErr(validateArgs(cmd, args))
            cmdutil.CheckErr(validatePruneAll(o.Prune, o.All, o.Selector))
            cmdutil.CheckErr(o.Run())
        },

o.Run()に飛ぶとコメントにRun executes the 'apply' command.とありますね. どうやら当たりのようです.
kubernetes/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go L355-L403

// Run executes the `apply` command.
func (o *ApplyOptions) Run() error {

引き続き読み進めているとapplyOneObject()内でinfo.Get()を呼び出しており,作成リソースがすでに存在しないかどうかの判定を行っているのがわかります.
kubernetes/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go

   if err := info.Get(); err != nil {
        if !errors.IsNotFound(err) {
            return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err)
        }

そしてリソースがなかった場合はkubernetes/staging/src/k8s.io/cli-runtime/pkg/resource/helper.goCreate()以降でリソースの作成処理が行われ,あった場合はkubernetes/staging/src/k8s.io/kubectl/pkg/cmd/apply/patcher.go L183-L205Patch()以降でリソースの更新処理が行われています.2

ところでこのCreate()関数ですがkubectl createのコマンドでも使われているようです. つまりapplyコマンドはinfo.Get()で同名リソースが取得できなかった場合,createコマンドと同様の動作をするようです.
 createコマンドはapplyコマンド同様にリソース作成に用いられるコマンドですが,applyコマンドと違いリソースの更新作業は行いません.すでに存在するリソースを作成しようとするとエラー文を表示します. (ここでそういえばAlready Existsのエラーメッセージはcreateコマンドのときに見たことあるなあと思い出すなど...)

$ kubectl create -f pod.yaml
pod/demo created
$ kubectl create -f pod-changed.yaml
Error from server (AlreadyExists): error when creating "pod-changed.yaml": pods "demo" already exists

というわけでapplyコマンドを同時実行した際にエラーが発生したのはinfo.Get()実行時には同名リソースが存在しなかったが,その後のリソースのcreate処理中に同時実行されているもう片方で同名リソースが作成されていた」,というのが原因だということがわかりました.
めでたしめでたし.

さいごに

いかがでしたでしょうか?

実際にコードを読みながら動作を把握するのは面白いですね.
今回はAPIサーバ内の動作については深追いしませんでしたが,ここも見てみると面白いことがわかるかもしれません.
明日から使える知識...というほどの知見ではありませんが,この記事が誰かのクベ活に役立てれば幸いです.

というわけで本記事はあくあたん工房 Advent Calendar 2020 21日目の記事でした.
明日はkobaくんが何か書いてれるようです.お楽しみに〜

adventar.org


  1. 何度か実行すると場合によって片方がcreated,片方がconfigureになることもあります.

  2. 実際にはリソースへの作成または更新リクエストをAPIサーバに投げます