Conao3 Notes

awsrを本家AWS CLIの新しいバージョンに毎日追従させる

前の記事で紹介した awsr は、AWS CLI v2とバイト単位で同じ出力を返すRust製のCLIです。 同じ出力を返し続けるには、本家の新しいバージョンが出るたびに awsr も同じバージョンへ上げる必要があります。 本家は2026-09-30から2026-11-05までの36日間に20回リリースしていて、平日はほぼ毎日新しいバージョンが出ます。 そこで、本家の新しいバージョンへの追従をGitHub Actionsで毎日回すようにしました。 新しいバージョンが出ると、workflowがバージョンを上げる変更を作って本家と比べ、結果を載せたPRを作ります。

次の画像は、2.39.5が出たときにworkflowが作ったPRです。 移植する前の比較の結果が、表に並んでいます。

img

バージョンを上げると変わるもの

awsr は本家の決まった1つのバージョンに合わせて作っていて、そのバージョンを models/VERSION に書いています。 flake.nix は、同じバージョンの本家のソースと公式のLinux向けのインストーラをnixで取り込みます。 本家のバージョンを上げると、次の3つが変わります。

  • botocoreのモデル: awsr がバイナリに埋め込む、サービスと操作の定義です。
  • テストの期待値: コマンドの表、引数の表のハッシュ、helpの文書、waiterの説明は、本家のモデルとソースから作っています。
  • 本家のコードの変更: 独自の処理を持つコマンドや、認証の処理の変更です。本家のchangelogでは api-change 以外の項目 (enhancement や bugfix) にあたります。

モデルと期待値は本家から機械で作れますが、本家のコードの変更はRustへ移植する必要があります。

バージョンを上げるスクリプト

機械で作れる部分はscripts/update-reference.shで作り直します。

$ scripts/update-reference.sh 2.39.5

このスクリプトは次の順に作業します。

  1. nix-prefetch-url で本家のソースとインストーラのハッシュを取り、 flake.nix のバージョンとハッシュを書き換える。
  2. 新しいバージョンのdevShellに入り、 scripts/vendor-models.sh でモデルを取り込み直す。モデルを持つ3つのデータcrateのバージョンも、ここで本家のバージョンにそろう。
  3. コマンドの表、引数の表のハッシュ、helpの文書、waiterの説明を、本家のソースから作り直す。
  4. nix fmt で整形する。

期待値を作るスクリプトは、2.39.3のソースで動かすとcommit済みの期待値と同じ中身になることを確かめてから使っています。

毎日回すworkflow

Reference のworkflowは、毎日12:00 UTCに本家の最新のタグを調べます。 models/VERSION より新しいバージョンがあれば、上のスクリプトでバージョンを上げてから、次の4つで本家と比べます。

  • make check (fmt、clippy、テスト)
  • 差分テスト: モデルが変わったサービスの全操作を awsr と本家の両方で動かし、出力とリクエストを比べる。
  • helpの比較: 同じサービスのhelpページを、バイト単位で比べる。
  • シナリオと認証の比較: 手で書いたシナリオ、 s3 コマンドのシナリオ、 configure や sso login などの認証まわりのコマンドを比べる。

比べ終わると、 reference/<バージョン> のブランチをpushし、結果の表を載せたPRを作ります。 PRの本文には本家のchangelogのうち api-change 以外の項目も並べます。 比較が1つでも失敗するか移植の要る項目があれば、PRをdraftにします。

img

変わったサービスだけを比べる

差分テストを全件回すと、23万件を超える組み合わせを本家と比べることになります。 本家の実行結果のキャッシュがないと、手元の14コアのPCで約100分かかりました。 公開リポジトリで使うGitHub Actionsの ubuntu-latest は4コアなので、全件を毎日回すのは現実的ではありません。

そこで、毎日の比較はモデルが変わったサービスに絞ります。 2.39.3から2.39.5では34サービスのモデルが変わっていて、その全操作の組み合わせは48,504件でした。 このバージョンに上げたときのrunは全体で約71分かかりました。 そのうち差分テストがreleaseビルドを含めて約44分、helpの4,189ページの比較が約18分です。

2.39.5で移植したもの

2.39.3から2.39.5に上げたPRはconao3/rust-awscli#6です。 2.39.4と2.39.5のchangelogには、 api-change 以外の項目が9つありました。 そのうち awsr に移植したのは、次の4つです。

  • Bearer認証のトークンを、HTTPS以外のendpointへ送らない。
  • s3 cp 、 s3 mv 、 s3 sync で、手元からS3への転送に --tags を付けられる。
  • 転送が PreconditionFailed で失敗したとき、 --no-overwrite を付けた転送だけを、すでにあるファイルのskipとして扱う。
  • deploy install が書く設定ファイルの権限を600にする。

残りの5つは、CRTの転送クライアントとWindowsでの aws update の項目です。 awsr はclassicの転送クライアントだけを実装していて、 update のコマンドもまだ実装していないので、この5つは移植の対象になりません。

Bearer認証の変更は、2.39.5で増えた account-access-runtime のサービスで見つかりました。 差分テストはこのサービスの24件で一致せず、本家は Bearer authorization requires an HTTPS endpoint. で止まり、 awsr はリクエストを送っていました。 この変更を移植すると、こんどは2.39.3の本家と比べたときにBearer認証を使う別のサービスが一致しなくなります。 そのため、本家のバージョンに依存する移植は、バージョンを上げるPRの中で行います。 PRのブランチにmasterをmergeし、その上に移植のcommitを積んで、手元ですべての比較が通ってからmergeしました。

--tags は既存のシナリオでは比べられないので、 s3 コマンドのシナリオを6つ足しました。 足したシナリオがタグを確かに比べていることは、 awsr のタグを付ける処理を一時的に外し、uploadの4つが失敗することで確かめています。

runnerの上でだけ見つかった差分

2.39.5のPRを作ったrunでは、 deploy install と deploy uninstall のシナリオが3つ失敗しました。 同じシナリオは手元のPCでは一致します。

deploy install は、EC2のインスタンスの上では動かないように作られていて、 169.254.169.254 のメタデータのサービスに問い合わせてEC2の上かを調べます。 本家はPythonの urlopen でHTTPのGETを送り、エラーの応答を受けたときはEC2ではないと判断します。 awsr はTCPでつながるかだけを見ていたので、このアドレスに何かのサービスが応答する環境ではEC2の上だと判断していました。 GitHub ActionsのrunnerはAzureかAWSの上で動き、Azureのメタデータのサービスも同じアドレスを使います。 Azureのメタデータのサービスは、 Metadata: true のヘッダーがない要求を拒みます。

awsr もHTTPのGETを送り、応答のステータスが400未満のときだけEC2の上と判断するように直しました。 手元では、 unshare -rnm で作ったuser namespaceの中に 169.254.169.254 で400か200を返すサーバーを立て、 /etc/lsb-release をUbuntuのものに差し替えて、本家と awsr の出力が同じになることを確かめています。