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などで教えていただけると嬉しいです。

YAPC::Hiroshimaに参加して、登壇して、アフターイベントのキーノートをしてきました #yapcjapan #yayapc

blogを書くまでがYAPCということで、帰路の新幹線で書いています。

2024年2月9,10日に行われた YAPC::Hiroshima 2024 に参加し、応募したトークが採択されたので登壇し、翌11日に行われたYAPCアフターイベント YAYAPC::Hiroshima オフラインだからできる話 でキーノートを依頼していただいたのでスピーカーを務めてきました。

前回の YAPC::Kyoto 2023に参加しました #yapcjapan - 酒日記 はてな支店 ではトークを応募しないで参加したことに後悔したので、今回は応募して本編で40分、更にアフターイベントで40分と沢山発表させて頂きました…ありがとうございます。

YAPC::Japanになってから過去最大の参加者数、スポンサーも過去最高、初参加の人もたくさんということで、2006年のYAPCから参加している古参(老人)としても大変嬉しいです。

聞いたトークの感想

  • Introduce Hono v4!!!!
    • 今やスーパーハッカーという言葉がぴったりなyusukebe氏。中学生がプロジェクトに何人もいるのがとてもよい。サーバーサイドからWeb standardsで攻めていくというのがほんとわくわくしますね
  • キャッシュバスターズ
    • 実のところキャッシュなしでは現代のCPUもOSもまともに動かないわけで、キャッシュは麻薬ではなくパフォーマンスのためには必須の技術、ただ間違った使い方をする人が多いだけ、なのでバスターすべきは間違った使い方のほう
    • 結局あなたの実務の要件を見て適切にやれ、という話になっちゃうのが難しい(断言できない)ところですよね
    • 個人的にはキャッシュは「作る」のは簡単だけど「消す」ほうが100倍難しいから、消し方を考えてから作れ、という話をよくします
  • 入門EOL対応 ~SREが鉄板の流れ全部見せます編~
    • 年中EoL対応をやっている身として興味深く聞きました。「モチベーション高くやる」方法を意識的に実施しているのが素晴らしかったです
  • What You Like May Not Be for Someone オープンソースアクセシビリティ
    • 自分がこれまで関わっていたサービスは正直なところアクセシビリティにことを考えていないものが多かったなあと反省しきりですね。原理や規則に従っているかどうかも大事ですが、実際に支援技術を使って問題があるかないかを確認するのがもっと大事、という話を受け取りました
  • Go to Cloudflare Workers ~ 移行から 0.5 年以上運用する
    • 筋がよいので使いたい、けどまだまだ周辺が未成熟な技術を腕力でどうにかしていく、整備された道だけではなく自分で荒野を切り拓く話は大好きです。高速道路に乗ると快適ですけど、それだけじゃつまらないですからね
  • 経営・意思・エンジニアリング
    • 途中から、最後のほうだけ聞きました。自分の後ろで誰も責任を取ってくれないところでは最後は意思の力、そうですね…
  • 新任エンジニアリングマネージャーのための「ぼうけんのしょ
    • 目標設定が苦手という人が多い話、これは人から設定しろと言われた瞬間にやる気をなくすやつなので、内発的動機をいかに引き出すかなのかなと思って聞いていました。自分の場合、ランニングの目標は誰にいわれるでもないけど自分で作って、それに対する練習プランを立てたりしちゃいます
  • 非同期な開発体制を支えるドキュメント文化
    • ドキュメント、型があるのいいですね。守破離というか、型がないところにドキュメントを書くぞ!とという意気込みだけがあっても、試行錯誤に耐えられなくて爆発四散しがちです
  • 好きな技術《コト》で、生きていく技術
    • 人生の大半をどうせ仕事が占めるのであれば、どうせなら面白くやったほうが得、そのためにはという話。自分の場合、技術選定は実績があるやつを7,80%、新規で面白そうなのを2,30%ぐらいの配分を意識しています。実績は読めるので爆死しづらいし、新規でよいことは次に取り入れて悪いところは捨てて、を繰り返すことで代謝しつつ前に進めるのでは、という持論でやってます

キーノート

1998年に仕事を始めて、会社の先輩に「とほほ」という凄いページがあるからそこで勉強するといいよ、といわれてそこで学びつつ、ラウンジ(掲示板)で数年常連をやったりしていた身としては、とほほさんにはキャリアの最初からお世話になりっぱなしでした。

淡々とやっていることを紹介しているだけなのに超人にしか聞こえない、息をするように、という言葉そのものでしたね…

自分のトーク

speakerdeck.com

作ってから5年(前身のmirageからだと10年近く)、ずっと仕事で便利に使っているミドルウェアの話でした。懇親会で話を聞くと、まだまだ検証環境が固定でn個あるのでそれの奪い合いになってます、みたいな現場も多いみたいですね。そういう環境に、何かのヒントになれば幸いです。

SREやインフラ領域だと既存のソフトウェアをどう使うかという話になりがちですが、使うだけではなく書くことで、ソフトウェアの力で解決できるところはしていきましょう、というメッセージでもあります。

懇親会

5年ぶりの懇親会、大変楽しかったです!

懇親会のYAPCビールと舟盛り

アフターイベント YAYAPC

YAPC本編は前夜祭と本編1日ですが、今回はアフターイベントYAYAPCが企画されました。公開されるイベントというのはどうしても公開可能な事例しか選ばれない(それはそう)なので、広く公開するにはためらわれるけど面白い話、限定のイベントです。そして、そのイベントのキーノートをkobakenさんに依頼して頂いたので、僭越ながら務めさせて頂きました。

当然イベントの内容はここで公開できないんですが、みなさんそれぞれの現場でそれぞれの戦いをしているんですよね。生き様だなあ、と思いました。めちゃくちゃ楽しかったです。またやりたい、というかやるなら個人でもスポンサーしたいです。

キーノートのスライドはこれもまた公開できないのですが、最後の1ページだけ載せておきますね。

YAYAPCキーノートのスライド(最終ページ)

おまけ

広島滞在中の走行距離は合計22.3kmでした

Mackerelと連携する外形監視エージェントmaprobeにOtel metrics送信機能を追加した

この記事はMackerel Advent Calendar 2023 12月19日分の記事です。

Mackerelと連携する外形監視エージェント、maprobeというOSSを5年ほど前に作って、ずっと使っています。今回は maprobe v0.7.0で Otel (OpenTelemetry) metrics を送信する機能を追加したというお話です。

github.com

maprobeについては以下の記事もどうぞ。 sfujiwara.hatenablog.com

3行でまとめ

  • maprobeはMackerelに登録されているホスト情報を取得して、そのホストに対してping, TCP, HTTPによる外形監視とmackerel-pluginの実行によるメトリック取得を定期的に実行するエージェントです
  • maprobe v0.7.0 では外形監視の結果とpluginの実行結果を、Mackerelのホストメトリックとしてだけではなく、OpenTelemetry Metricsとしても送信できるようになりました
  • 既存pluginや自作pluginによるメトリック取得を、シームレスにOpenTelemetry metricsに移行することができます

設定方法

ここでは例として、Mackerelの「service=prd」「role=redis」のホストに対して、mackerel-plugin-redis によるメトリック取得をする例を載せます。

probes:
  - service: prd
    role: redis
    attributes:
      host.name: '{{ Host.Name }}'
      instance.type: '{{ index .Host.Meta.Cloud.MetaData "cache-node-type" }}'
    command:
      command:
        - mackerel-plugin-redis
        - -host={{.Host.CustomIdentifier}}
        - -config-command=

destination:
  mackerel:
    enabled: false
  otel:
    enabled: true
    endpoint: otlp.mackerelio.com:4317

maprobeは service=prd, role=redis のホストをMackerelから取得し、その全てのホストに対してpluginを実行します。pluginの実行対象 (-host=の値) には、Goのtext/template記法でホストの情報を取りだして展開することができます。{{ .Host.CustomIdentifier }} に具体的なRedis(ここではElastiCache Redis)のnode DNS名が入っているため、今回はそれを指定しています。

通常、pluginで取得したメトリックはMackerelの対象ホストのホストメトリックとして送信されますが、v0.7.0 から増えた destination という設定を追加することで、任意の OpenTelemetry Collector に対して gRPC を使って送信することができるようになりました。

ここで指定している otlp.mackerelio.com:4317 は、現在絶賛βテスト中の、Mackerelが開発中のOpenTelemetry metrics endpointです。Mackerelが提供しているendpointだけではなく、任意のendpointを指定できます。

mackerel.io

また、Otel Metricsの特徴としてメトリックに任意の属性(attribute)を付与できる点があります。 maprobeはデフォルトで host.idservice.name という属性をメトリックに付与しますが、追加で任意のattributeを含めることが可能です。

    attributes:
      instance.type: '{{ index .Host.Meta.Cloud.MetaData "cache-node-type" }}'

この設定例では、ホストのメタデータに含まれる cache-node-type(いわゆるインスタンスタイプ)を追加しています。

取得した結果

ということで、Mackerelに送信されたOtel metricsをクエリグラフ機能によって描画したものがこちらです。

クエリグラフ

maprobeで指定した属性がメトリックに付与されていることが分かります。この属性を利用して、特定の属性を持つメトリックだけ描画したり、特定の属性のメトリックを合計/平均などの演算をして柔軟にグラフを描画できますね。

どのように使うのか

Otel metricsは、特定の監視ツールによらない汎用的なメトリック取得を可能にしてくれます。また、Mackerelの従来の機能では実現できないような、柔軟なグラフ描画も可能になります。

しかし、従来からMackerelをご利用中の皆様は、既存pluginや自作pluginによって現在のモニタリングを構築していることでしょう。それらを全て別の手段によって取得したOtel metricsに置き換えるのは大変ですし、取得方法や項目が変われば監視自体の継続性にも問題が出てきます。

maprobeによって既存pluiginによって取得した値をOtel metricsとして送信することで、既存の監視項目やノウハウを引き継ぎつつ、新たにOtel metricsによる柔軟なグラフ描画(や、現在はまだありませんがアラート)に移行できるかと思います。どうぞご利用ください。

Mackerel OpenTelemetry metric機能の正式公開が待ち遠しいですね!

unbufferでAmazon ECS Execを端末以外から実行する

検索で引っかかるように書いておきます。

ECS Exec (ecspresso exec, ecsta exec, aws ecs execute-commandなど)を端末以外の環境 (例えばJenkinsやGitHub ActionsなどのCI/CD環境) から実行すると、session-manager-pluginが "Cannot perform start session: EOF" でエラーになります。

これは session-manager-plugin が端末(tty)を割り当てられていることを期待しているためです。

expect パッケージに含まれる unbuffer コマンドでwrapして実行して疑似端末を与えると、端末以外からも実行できるようになります。

$ unbuffer aws ecs excute-command ...

github.com

ecspresso MeetUpを開催していただきました

自分が開発しているAmazon ECSデプロイツール ecspresso のmeet upを、JAWS-UGコンテナ支部のイベントとして開催していただきました。

参加された皆様、発表して頂いた8名の皆様、企画、運営してくださったJAWS-UGコンテナ支部の皆様、本当にありがとうございました!

皆様のecspresso愛を感じて、作者冥利に尽きるイベントでした。ecspresso共々、今後ともよろしくお願いします。

jawsug-container.connpass.com

経緯とか

AWS Dev Day 2023 Tokyoの発表 でecspressoをご利用の皆様を紹介したくて名前を出してもいいよという会社さんを募ったところ、思いもよらず多くの反応をもらいました。これだけ多くの人に使ってもらえてるならmeet upぐらいできるんじゃないかな? と思って気軽につぶやいたのですが、それをみていたJAWS-UGコンテナ支部のかたが企画をしてくださった……という経緯でした。

自分のプロダクト(OSS)で単独のイベントを開いてもらった上に8人もの人に話してもらえるという、これはなかなかできない経験でしたね…

ところで当日は48名の現地参加者のうち懇親会参加が29名という大宴会になってしまい、懇親会は当日まで何も決まっていなかったので「居酒屋北海道に断られる」という実績を解除してしまいました。受け入れてもらった ひものや(目黒店)様もありがとうございました。

発表資料

公開されているものを当日の発表順に並べておきます。ecspresso自体の話はいっぱいしてもらえると思ったので、自分はecspressoそのものというよりはそういうソフトウェアを作るときになにをどう考えているか、という話をしました。

speakerdeck.com

junkyard.song.mu

speakerdeck.com

speakerdeck.com

speakerdeck.com

www.docswell.com

speakerdeck.com

speakerdeck.com

speakerdeck.com

録画アーカイブ

www.youtube.com

Go実装のAWS Lambda関数をCLIで動かせるライブラリを書いた

最初に3行でまとめ

  • GoでAWS Lambdaのハンドラを実装した場合に、手元から同じ処理を実行したり開発中の動作確認のため、CLIコマンドとしても実行できると便利です
  • そのための、とてもシンプルなwrapperライブラリを書きました lamblocal といいます
  • 環境変数を読み込めるCLI flag parserと組み合わせるといいかんじです

github.com

Lambdaを実装する言語

最近、自分はAWS Lambdaの関数をGoで書くことがほとんどです。

個別のランタイムがある言語で書くとランタイムのバージョンアップやEoL対応が必要になって面倒だったりしますが、Goで書いてシングルバイナリをbootstrapという名前でzipに含めてカスタムランタイム(provided.al2) で動かすとそういう煩わしさがありません。ARM対応もビルド時に環境変数を指定するだけなので極めて簡単です。

GoでLambda関数を書くためには、aws/aws-lambda-goを使って以下のようにします。

package main

import (
    "github.com/aws/aws-lambda-go/lambda"
)

func hello() (string, error) {
    return "Hello λ!", nil
}

func main() {
    // Make the handler available for Remote Procedure Call by AWS Lambda
    lambda.Start(hello)
}

これを go build -o bootstrap main.go としてLinux向けにビルドしてやると 1、Lambdaのハンドラとして動作します。アーキテクチャはLambdaの設定により、GOARCH=amd64またはarm64です。

Lambda関数を手元でも動かしたい

デバッグや開発中の動作確認のため、手元でこのハンドラを実行したいことがあります。また、lambdaとして便利な関数は、手元でも単体のコマンドとして実行できると便利な場面が結構あったりします。

公式には、AWS SAM CLIを使うことでローカル実行ができたりするようですが、ここでは触れません。

Goで書かれたLambda関数ハンドラをLambdaでもそれ以外の環境でも実行できるコマンド(バイナリ)にするためには、以下のようにすればよいのです。

  1. 実行時に環境がLambda上かどうかを判別する
  2. Lambda上であれば lambda.Start() にハンドラを渡して実行する
  3. そうでない場合、ハンドラの関数に適当な入出力を与えて実行する

実行時の環境がLambda上であることは、環境変数から次のようにして判別できます。2

  • AWS_EXECUTION_ENVAWS_Lambdaで始まっていること (言語別ランタイムの場合)

または

  • AWS_LAMBDA_RUNTIME_API が設定されていること (カスタムランタイムの場合)

Lambdaでは入出力はランタイム側で処理されますが、それ以外の環境では標準入出力を使えばよいでしょう。

簡単にやるためのライブラリを書いた

github.com

ということで、これを簡単にやるためのlamblocalというライブラリを書きました。実装を見ると分かりますが、本当にシンプルなものです。使用例は次の通りです。lambda.Start()の代わりにlamblocal.Run()を呼ぶだけです。

これで作ったバイナリは、Lambda上ではLambda関数として、そうでない環境では単体コマンドとして実行できます。

package main

import (
        "context"

        "github.com/aws/aws-lambda-go/events"
        "github.com/fujiwara/lamblocal"
)

func handler(ctx context.Context, payload events.CloudWatchEvent) (string, error) {
        return payload.ID, nil
}

func main() {
        lamblocal.Run(context.TODO(), handler)
}

handlerの第2引数(paload)は標準入力から受け付けた内容をJSONとして解釈して任意の型で渡せるようになっているので、型に応じた適当なJSONを喰わせてください。関数の出力は、任意の型をJSONとしてencodeして標準出力に出力されます。

$ echo '{"id":"xxx"}' | go run main.go
"xxx"

payloadがない関数(第2引数を _ にする)の場合は実行開始後、標準入力を閉じてください(端末からCtrl-Dを入力)。

// payloadがない関数
func handler(ctx context.Context, _ interface{}) (string, error) {
        return "OK", nil
}

なおlamblocal.Run()ジェネリクスを使っていて、func Run[T any, U any](ctx context.Context, fn func(context.Context, T) (U, error)) という型になっています。payloadと戻り値には任意の型(any)を扱えますが、jsonとしてUnmarshal/Marshalできる型であることを前提としています。

ハンドラとして渡せる関数のインターフェースは func(context.Context, any) (any, error) のみです。

lambda.Start()ではあらゆる型(interface{})を引数に取って実行時に型チェックを行う作りになっていて、ビルドはできていても実行できない関数を作ってしまうことが稀にありました。lamblocalでは型を絞ることでその可能性を減らしています。

設定値などの管理方法

Lambdaでは、環境変数で設定値などを渡すのが前提です。CLIでも同様に環境変数を使ってもよいのですが、コマンドライン引数として渡せると便利ですよね。

ここでは応用例として、alecthomas/kong というコマンドラインパーサーと組み合わせる例を紹介します。

kongでは、コマンドライン引数をstructとして定義し、structのタグでデフォルト値やどの環境変数から値を読み取るかを定義できます。

type CLI struct {
        Foo     string `help:"This is Foo." default:"foo" env:"FOO"`
}

このように定義すると、CLI.Fooの値は デフォルト値が foo環境変数 FOO=bar が設定されていれば barコマンドライン引数 --foo=baz が与えられた場合には baz、となります。つまり適当なデフォルト値を設定した上で、Lambda上で実行される場合は環境変数から、CLIとして実行する場合は引数から上書きできますし、その値が何かという説明も明示的にhelpとして記述できます。

環境変数のみで処理しようとすると得てしてコード内にos.Getenv()が散在したりしますが、このようにまとめておくことで設定値を適切に管理できますね。

具体的な例はリポジトリexamples/kongにありますが、次のようになります。

package main

import (
        "context"

        "github.com/alecthomas/kong"
        "github.com/fujiwara/lamblocal"
)

type CLI struct {
        Foo     string `help:"Foo." default:"foo" env:"FOO"`
}

func (c *CLI) Handler(ctx context.Context, _ interface{}) (string, error) {
         // c.Foo はデフォルト値、環境変数、コマンドライン引数から設定された状態になっている
        return "OK", nil
}

func main() {
        var c CLI
        kong.Parse(&c)
        lamblocal.Run(context.TODO(), c.Handler)
}

まとめ

GoでAWS Lambdaのハンドラを実装した場合に、手元から同じ処理を実行したり開発中の動作確認のため、CLIコマンドとしても実行できる、とてもシンプルなwrapperライブラリを書きました。

小物ですが便利なので、どうぞご利用ください。


  1. 確実にシングルバイナリにするためにCGO_ENABLED=0 も指定しましょう。ビルド環境がLinuxの場合、指定しないとlibcへの依存が発生するため、Lambdaに持っていったときにglibcのバージョンの差異で動かないことがあります。
  2. AWS Lambda 環境変数の使用 - AWS Lambda