👨‍🦯

備忘録: ビルドパイプラインがなんか壊れた

2023/04/29に公開約9,600字

導入

一か月ぐらい前、CircleCIでtflintによりTerraformの確認を行っていた環境が壊れて、
急にテストをパスしなくなったことがありました。

謎が解けてみれば非常に単純ではありましたが、
地味に知らないことが多かったので記録に残したいと思います。

対象のCircleCIのジョブ(障害発生版)

jobs:
  terraform-lint-check:
    docker:
      - image: hashicorp/terraform:1.3.0
        environment:
          TF_WORKSPACE: dev
    working_directory: ~/go/src/github.com/foo/bar/infrastructure/terraform
    steps:
      - checkout:
          path: ~/go/src/github.com/foo/bar
      - run:
          name: Terraform init
          command: |
            terraform init -backend=false
      - run:
          name: Install tflint
          command: |
            apk update --no-cache && apk add --no-cache unzip curl bash
            curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
      - run:
          name: Lint check
          command: |
            tflint --init
            tflint

障害が発生した経緯

チームのメンバーの一人が朝方、CircleCIのtflintの部分でエラーが発生するのだけど...と
Slackに投稿したのが最初のインシデント報告。

続くスレッドに張られたログを見ると
確かにCircleCIが48のエラーコードで失敗していることがわかりました。

この段階では当然原因はわかりませんでした。

初報にて

まっさきに検討に上がったのは tflint がサイレント変更されたのではないか?という疑惑でした。(完全に濡れ衣だと後でわかります)

インストールのスクリプトでバージョンを固定していないことも疑惑に拍車をかけ、
下記の切り分けを関係者で手当たり次第に行ってました。

  • tflintで関連するissueは上がっているか?
    • →問題なし
  • tflintをローカル環境(WSL)でインストールしたら?
    • →問題なし
  • tflintのバージョンを明示的に指定したら?
    • →いくつか変えてみて、どれもCircleCIでNGでした

「ローカル環境」という言葉で別の再現方法を思いつく

わちゃわちゃしだしたスレッドを見て、この辺で先行者の再現方法が違ってたんじゃね?と気づきました。

「ローカル環境というけど、CircleCIなんだからコンテナ環境で検証する必要があるのでは?」

ジョブ内容を見ていると hashicorp/terraform:1.3.0 を使っていることがわかります。ローカル環境で動かしてみました。

$ docker run -it --rm  hashicorp/terraform:1.3.0 /bin/sh
Terraform has no command named "/bin/sh".

To see all of Terraform's top-level commands, run:
  terraform -help

動きません。

hashicorp/terraformDockerfileを見ると次のことがわかります。

  • ベースイメージは golang:alpine イメージを使用している
    • →shは動くことがわかります
  • エントリポイントは /bin/terraform"
    • →コンテナでそのまま /bin/sh を実行すると /bin/terraform コマンドにパラメータが渡されたと見なされます

terraform/bin/sh という引数はないのでエラーになったのでした。
こういう時は動的にエントリポイントを書換えてあげる必要があります。

$ docker run -it --rm --entrypoint=/bin/sh hashicorp/terraform:1.3.0
/ # 

コンテナ内に入れました。後は同じコマンドを試すだけです。

/ # apk update --no-cache && apk add --no-cache unzip curl bash
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
v3.16.5-42-g1ce1b018120 [https://dl-cdn.alpinelinux.org/alpine/v3.16/main]
v3.16.5-41-geeab6d0b981 [https://dl-cdn.alpinelinux.org/alpine/v3.16/community]
OK: 17046 distinct packages available
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
(1/4) Installing readline (8.1.2-r0)
(2/4) Installing bash (5.1.16-r2)
Executing bash-5.1.16-r2.post-install
(3/4) Installing curl (8.0.1-r0)
(4/4) Installing unzip (6.0-r9)
Executing busybox-1.35.0-r17.trigger
OK: 28 MiB in 35 packages

/ # curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
(...すんっ)

既存のCircleCIログでは、この後でtflintのインストールログが走ります。
(...すんっ)のところで、確かにインストールが止まってしまったようです。

ただ、これだけではCircleCIで起きた事象と同一かまではわかりません。
内容を細かく見るためにスクリプトをwgetで落として実行してみることにしました。

/ # wget https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh
Connecting to raw.githubusercontent.com (185.199.108.133:443)
saving to 'install_linux.sh'
install_linux.sh     100% |*************************************************************************************|  2980  0:00:00 ETA
'install_linux.sh' saved

/ # bash install_linux.sh
arch=amd64
os=linux_amd64


====================================================
Looking up the latest version ...
curl: (48) An unknown option was passed in to libcurl
Downloading TFLint
curl: (48) An unknown option was passed in to libcurl
Downloaded successfully


====================================================
Unpacking /tmp/tflint.XXXXbjnDBi/tflint.zip ...
unzip:  cannot find or open /tmp/tflint.XXXXbjnDBi/tflint.zip, /tmp/tflint.XXXXbjnDBi/tflint.zip.zip or /tmp/tflint.XXXXbjnDBi/tflint.zip.ZIP.
Installing /tmp/tflint.XXXXbjnDBi/tflint to /usr/local/bin/ ...
install: can't stat '/tmp/tflint.XXXXbjnDBi/tflint': No such file or directory
Failed to install tflint

途中で curl: (48) An unknown option was passed in to libcurl というエラーログが出ていることがわかります。
CircleCIが終了したあの謎のエラーコード(48)はcurlが原因で間違いないでしょう。

こうして無事に再現手順を確立しました。

なんで失敗したのか?原因を調べる

素直に読み解くと、libcurlに謎のオプションが渡されているようですが、
先ほどのチームメンバーの報告で直近でtflintのインストールスクリプトは
修正されていないことがわかっています。
したがって、急にcurlのパラメータが変わったわけではなく、別の原因でしょう。

脈はなさそうと思いつつ、一応ChatGPT先生にはcurlのオプションを聞きました。

Q. 下記のcurlコマンドのオプションの意味を教えて
curl --fail -sS -L -o "${download_zip}" "https://github.com/terraform-linters/tflint/releases/download/${version}/tflint_${os}.zip"

A. 
以下は、使用されているcurlコマンドの各オプションの意味です。

-f, --fail: サーバーからHTTPステータスコードがエラーを示す場合に、ダウンロードの実行を中止します。たとえば、HTTP 404エラーが返されると、ダウンロードは失敗し、curlはエラーコードを返します。
-s, --silent: curlがダウンロードの詳細を表示しないようにします。通常、ダウンロードの詳細は標準エラー出力に出力されますが、このオプションを使用すると表示されなくなります。
-S, --show-error: エラーが発生した場合に、エラーメッセージを表示します。
-L, --location: サーバーがリダイレクトを行っている場合に、リダイレクト先にファイルをダウンロードします。
-o, --output: ダウンロードしたデータをファイルに出力します。${download_zip}は、ダウンロードしたデータを保存するための変数です。
"https://github.com/terraform-linters/tflint/releases/download/${version}/tflint_${os}.zip": ダウンロードするファイルのURLです。${version}と${os}は変数であり、それぞれダウンロードするファイルのバージョンとオペレーティングシステムを指定します。

https://curl.se/docs/manpage.html

なるほどね。エラーログを改めてみると、 An unknown option was passed in to libcurl とあります。

...curlとlibcurlって一緒じゃないの?素朴な疑問が湧きました。

everything cURLという公式ドキュメントには、cURLというプロジェクトには2つの目的があると記されています。

  • curl: コマンドラインツール
  • libcurl: CAPIを提供する転送用ライブラリ

いつも実行しているcURLコマンドも、じつは二部構成に分かれており、
転送のコアロジックはlibcurlとして、curl以外でも使えるようにライブラリとして外出しされています。

そのように整理してみると、このcurl/libcurlのIF部分が非常に怪しいですね。

curl 関係のエラーにて先人が同じエラーに遭遇していたので、コマンドを借りてきます。

# 大文字のVパラメータではcurlとlibcurlのバージョンを表示する
/ # curl -V
curl 8.0.1 (x86_64-alpine-linux-musl) libcurl/7.83.1 OpenSSL/1.1.1q zlib/1.2.12 brotli/1.0.9 nghttp2/1.47.0
Release-Date: 2023-03-20
Protocols: dict file ftp ftps gopher gophers http https imap imaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IPv6 Largefile libz NTLM NTLM_WB SSL TLS-SRP UnixSockets
WARNING: curl and libcurl versions do not match. Functionality may be affected.

curlのバージョンが8.0.1に対して、libcurlのバージョンが7.83.1なのでバージョンが食い違っていることがわかりました。

解決するまで

cURLが動かないのはバージョンの食い違いが原因だろうと突き止めたので、
解決策としてはlibcurlのバージョンを合わせれば良いだけです。

どうやってlibcurlを入れればいいのでしょうか?

ここで、ベースイメージが golang:alpine であることを思い出してください。

たとえば、Ubuntuでは apt install libcurl4-openssl-dev コマンドにより、
opensslをTLSライブラリとして使用したlibcurl[1] を入れれば、libcurlが入ります。

しかし、Alpine Linuxでは apt パッケージマネージャーは動きません。

有志の方がAlpine Linux用のパッケージマネージャーAPKと(Ubuntu等で使用される)APTの比較表を出していただいてるので
それを参考に libcurl4-openssl-dev にあたる curl-dev をインストールします。

/ # apk update --no-cache && apk add --no-cache unzip curl bash curl-dev #←最後にこれを追加
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
v3.16.5-42-g1ce1b018120 [https://dl-cdn.alpinelinux.org/alpine/v3.16/main]
v3.16.5-41-geeab6d0b981 [https://dl-cdn.alpinelinux.org/alpine/v3.16/community]
OK: 17046 distinct packages available
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
(1/13) Upgrading libcrypto1.1 (1.1.1q-r0 -> 1.1.1t-r2)
(2/13) Upgrading libssl1.1 (1.1.1q-r0 -> 1.1.1t-r2)
(3/13) Installing readline (8.1.2-r0)
(4/13) Installing bash (5.1.16-r2)
Executing bash-5.1.16-r2.post-install
(5/13) Upgrading libcurl (7.83.1-r3 -> 8.0.1-r0)
(6/13) Installing curl (8.0.1-r0)
(7/13) Installing pkgconf (1.8.1-r0)
(8/13) Installing openssl-dev (1.1.1t-r2)
(9/13) Installing nghttp2-dev (1.47.0-r0)
(10/13) Installing zlib-dev (1.2.12-r3)
(11/13) Installing brotli-dev (1.0.9-r6)
(12/13) Installing curl-dev (8.0.1-r0)
(13/13) Installing unzip (6.0-r9)
Executing busybox-1.35.0-r17.trigger
Executing ca-certificates-20220614-r0.trigger
OK: 31 MiB in 41 packages
/ # curl -V
curl 8.0.1 (x86_64-alpine-linux-musl) libcurl/8.0.1 OpenSSL/1.1.1t zlib/1.2.12 brotli/1.0.9 nghttp2/1.47.0
Release-Date: 2023-03-20
Protocols: dict file ftp ftps gopher gophers http https imap imaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IPv6 Largefile libz NTLM NTLM_WB SSL threadsafe TLS-SRP UnixSockets
/ #

無事解決しました。

障害切り分けと解決策を振り返って

分かってしまえばなんてことない障害ですが、それでも色んなスキルや技術知識がちょっとずつ要求されることに気づかされます。

  • Dockerクライアントのオプション
  • curlの構成
  • APTパッケージマネージャーの違い

こういう積み重ねに自分生きてるなーって謎の肯定感がわくのを感じます。

脚注
  1. 他にも様々な種類のTLSライブラリに対応しています。詳細は https://everything.curl.dev/build/tls を参照してください ↩︎

Discussion

ログインするとコメントできます