Debian12 bookwormの更新でハマった件もしくはAlpine Linuxからdistrolessへの乗り換え時の注意点

こんにちは もしくは こんばんわ! ANDPADボード プロダクトテックリードの原田 土屋(tomtwinkle)です 最近めでたく戸籍が代わり名字がリネームされました

この記事はDebian12 bookwormが正式リリースされ、Debian11 Bullseyeが今までの流れでいうと来年辺りEOLになりそうな雰囲気なので今のうちに切り替えておこうと奮闘した記録とAlpine Linuxからdistrolessに変更したらKubernetesのpreStopが上手く動かなくなった件の対応をした記録の合せ技です。

TL;DR

  • DockerのBuild base imageを Debian11 Bullseye から Debian12 bookworm にしただけで docker build がコケるようになったなら docker/buildx のversionを上げてみよう
  • circleci/aws-ecr@9.0.1 では ecr_login の前に circleci/aws-clisetup が必要
  • Kubernetesで distroless image を使う時 sleep command の移植は忘れるな

Debian11 Bullseye からDebian12 bookworm へアップデートするまでの戦い

アップデート作業の前にアップデートしたい環境がどんな構成かを軽く説明しておくと

  • EKS上でApp(golang)は動作している
  • CircleCIでDocker imageをbuildしてECRにpushし、ECRのimageを使ってhelm upgradeでKubernetesの更新をかけている

というよくある構成です。 今回、CircleCIで利用していたorbのバージョンが重要なので記載しておきます

aws-cliの最新がv4.1.2なのでめちゃくちゃ古いですね。今回そっちは関係ないんですけれども……

1. まずはDockerfileを更新してみる

Debian11 Bullseye でbuildするのに使用していたDockerfileはだいたい以下のような感じでした

# Build用image
FROM golang:1.21.4-bullseye as builder
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

ENV GOPATH /go
ENV PATH $GOPATH/bin:$PATH
ENV CGO_ENABLED 0
ENV GOPRIVATE github.com/xxxxx # go moduleで参照しているprivateのgithub repository

ARG GITHUB_TOKEN  # privateのgithub repositoryを参照可能なPAT
RUN   : \
  && apt-get update \
  && apt-get install --no-install-recommends -y curl git make \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/* \
  && git config --global url."https://${GITHUB_TOKEN}:x-oauth-basic@github.com/".insteadOf "https://github.com/" \
  &&  :

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

# 以下go buildのための諸々省略

# Deploy用 image
FROM gcr.io/distroless/static-debian11 as runner
COPY --from=builder /main ./

# 以下Security対策のための諸々省略

これを素直に Debian12 bookworm に更新してみます、変更するのはimageだけなので差分だけならこうです

# Build用image
-FROM golang:1.21.4-bullseye as builder
+FROM golang:1.21.4-bookworm as builder

# Deploy用 image
-FROM gcr.io/distroless/static-debian11 as runner
+FROM gcr.io/distroless/static-debian12 as runner

良さそうですね。特に問題無さそうに見えます。 しかし、CircleCIでbuildしてみると以下のエラーが出ました。

#9 [base 3/6] RUN   :   && apt update   && apt-get install --no-install-recommends -y curl git make   && apt-get clean   && rm -rf /var/lib/apt/lists/*   && git config --global url."https://****************************************:x-oauth-basic@github.com/".insteadOf "https://github.com/"   &&  :
#9 sha256:0e532e7716c1cbffe6c9f5faa2a948d98525829391e968a28bfec62b79aa8e08
#9 0.391 Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
#9 0.398 Get:2 http://deb.debian.org/debian bookworm-updates InRelease [52.1 kB]
#9 0.398 Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
#9 0.450 Get:4 http://deb.debian.org/debian bookworm/main amd64 Packages [8780 kB]
#9 0.521 Get:5 http://deb.debian.org/debian bookworm-updates/main amd64 Packages [6668 B]
#9 0.552 Get:6 http://deb.debian.org/debian-security bookworm-security/main amd64 Packages [105 kB]
#9 1.413 Fetched 9143 kB in 1s (8886 kB/s)
#9 1.413 Reading package lists...
#9 1.891 E: Problem executing scripts APT::Update::Post-Invoke 'rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true'
#9 1.891 E: Sub-process returned an error code

2. エラー原因探索

どうも apt-get update のscriptでコケてるらしいので問題の切り分けのためひとまず apt-get update を外してみると 当該処理は通り、apt-get updateさえ何とかすればいけるか?と思ったのもつかの間

#13 [base 6/6] RUN go mod download
#13 sha256:xxxxxxxxxx
#13 1.599 go: github.com/xxxxx/xxxxxxx@vx.x.x: reading github.com/xxxxx/xxxxxxx/go.mod at revision vx.x.x: git ls-remote -q origin in /go/pkg/mod/cache/vcs/xxxxxxxxxx: exit status 128:
#13 1.599       fatal: unable to access 'https://github.com/xxxxx/xxxxxxx/': getaddrinfo() thread failed to start
#13 ERROR: executor failed running [/bin/bash -o pipefail -c go mod download]: exit code: 1
------
 > [base 6/6] RUN go mod download:
------
error: failed to solve: rpc error: code = Unknown desc = executor failed running [/bin/bash -o pipefail -c go mod download]: exit code: 1

今度は go mod download がコケました。むむむ…… PrivateのGitHub Repositoryを参照するPAT自体は有効であることは確認し GOPRIVATE の環境変数の渡し方を変更してみたり色々試行錯誤してもどうも通らない。

3. エラー原因解明! 新たなトラップ発動

そこで最初に出た APT::Update::Post-Invoke の errorに立ち戻ってBingると以下のStack Overflowの投稿を見つけました。

stackoverflow.com

There was a Debian release a few days ago. python:3.9-slim derives from this new Debian version and there are some issues with running images based on this Debian version with older versions of docker (certificates and/or keys, updated version of glibc perhaps).

Docker更新しないと証明書や鍵、glibcのバージョンの差異で問題起きるよとのこと 天才!それだ!ありがとう僕らのstackoverflow!! と感謝を述べつつaws-ecrとaws-cliのorbを更新します。

orbs:
-  aws-cli: circleci/aws-cli@1.3.1
+  aws-cli: circleci/aws-cli@4.1.2
-  aws-ecr: circleci/aws-ecr@8.2.1
+  aws-ecr: circleci/aws-ecr@9.0.1

circleci/aws-ecr@8.2.1 から circleci/aws-ecr@9.0.1 への更新は何故かcommandやargumentの名称がハイフンから全部アンダースコアになっていたりして変換がちょい面倒でしたがそれ以外大きな変更はなさそうに見えました。

これで無事docker buildが通るようになりました! やったか!? (ゴジラ-1.0面白かったですね)

と思ったのもつかの間今度は更新したcircleci/aws-ecrの方でエラーが出現 やってなかった!

Removing login credentials for ************.dkr.ecr.**************.amazonaws.com

The config profile (default) could not be found
Error: Cannot perform an interactive login from a non TTY device

Exited with code exit status 1

4. そして完走へ

CircleCIのbuild + push jobは以下のような構成で 問題のエラーは aws-ecr/ecr_login で発生していました。 さっきアップデートしたところだ!!!

  build-and-push:
    executor: awsecr/default
    environment:
      AWS_ECR_REGISTRY_ID: "<AWS Account ID>"
      AWS_REGION: <AWS Region>
      IMAGE_REPO: <ECR Repository>
      DOCKERFILE: Dockerfile
    steps:
      - git-shallow-clone/checkout
      - aws-ecr/ecr_login # <---- ここでエラー
      - aws-ecr/build_image:
          dockerfile: Dockerfile
          extra_build_args: --build-arg GITHUB_TOKEN=${GITHUB_TOKEN}
          repo: ${IMAGE_REPO}
          tag: ${CIRCLE_SHA1}

The config profile (default) could not be found なのでエラーの通りなのですが どうやら aws config に defaultの設定がなくて怒られているらしい。

どうもaws-cliのバージョンアップで環境変数でアクセストークン、シークレットトークン設定していた場合でも本来読む必要のない ~/.aws/config ファイルを読みに行っているようです。 なので aws-cli/setup でaws configを作成してあげます。

  build-and-push:
    executor: awsecr/default
    environment:
      AWS_ECR_REGISTRY_ID: "<AWS Account ID>"
      AWS_REGION: <AWS Region>
      IMAGE_REPO: <ECR Repository>
      DOCKERFILE: Dockerfile
    steps:
      - git-shallow-clone/checkout
      - aws-cli/setup  # <-----  これを追加
      - aws-ecr/ecr_login
      - aws-ecr/build_image:
          dockerfile: Dockerfile
          extra_build_args: --build-arg GITHUB_TOKEN=${GITHUB_TOKEN}
          repo: ${IMAGE_REPO}
          tag: ${CIRCLE_SHA1}

これで通るようになりました。めでたしめでたし。

aws-ecr orbのissueを眺めていたら同じ場所で困ってる人がいるみたいで同じ解決策が提案されていました。 aws-ecr orbのDiff眺めても分からなかったのでせめてドキュメントに書いていて欲しいですね。

github.com

The docs offer zero context on how to set this up. In our case, we migrated from version 8.x to 9.x.

What solved the same problem for us, was adding AWS_DEFAULT_REGION to the environment and calling aws-cli/setup before the build and push command.

Alpine Linuxからdistrolessへの乗り換え時の注意点

これでおしまい、と思いきや以前にAlpine Linuxからdistrolessへimageを変更して以降、Kubernetesの更新時稀にPodがBackOffしてしまう事象が発生していました。 今回改めて詳しく調査していくとKubernetesのPreStopの際に以下エラーが出ていることがわかりました。

# Rolloutのhelmテンプレート
kind: Rollout
spec:
  template:
    spec:
      containers:
        lifecycle:
          preStop:
            exec:
              command: ["sleep", "20"]

podのエラー内容

FailedPreStopHook: Exec lifecycle hook ([sleep 20]) for Container "xxxxxxx" in Pod "xxxxxx" failed - error: rpc error: code = Unknown desc = failed to exec in container: failed to start exec "xxxxxxxxx": OCI runtime exec failed: exec failed: unable to start container process: exec: "sleep": executable file not found in $PATH: unknown, message: ""

Kubernetes PreStopでGraceful Shutdownのために指定している sleep commandが存在しないというエラーですね。

distroless は極力imageサイズを抑えるため余計なshellやappを含まないimageです。 なので当然 sleep も存在しないわけです。睡眠の重要性!

そこでdocker buildの際にBuild用のDebian imageからsleepのみを移植するように修正しました。

# Build用image
FROM golang:1.21.4-bookworm as builder

# 省略

# Deploy用 image
FROM gcr.io/distroless/static-debian12 as runner
COPY --from=builder /main ./
COPY --from=builder /bin/sleep /bin/sleep # <---- 追加

これでようやく直近出ていたDocker imageの問題はなくなり正常に更新が行えるようになりました。 めでたしめでたし。

preStop は発動するタイミング的に確実にコケるとは限らないので発覚しづらい問題だったかと思います。 preStopフックでのsleepはアルパカ界でも一般的な手法なのでKubernetesを利用しているなら設定している事が多いと思います、distrolessを利用しているor利用しようとしている皆さんも気をつけて下さいませ。

今回の現象に遭遇した際に爆速でエラーを特定して頂いたSREメンバーに猛烈感謝です!

We are hiring!

アンドパッドでは、「幸せを築く人を、幸せに。」というミッションの実現のため、一緒に働く仲間を大募集しています。 数あるユーザーの課題を解決するには何が最善かを考え抜き、よりよいプロダクトを作りたい!と思われる方はぜひぜひご応募ください!

engineer.andpad.co.jp