Slide 1

Slide 1 text

awslim Goで実装された高速な AWS CLIの代替品を作った 2024.07.04 layerx.go #1 @fujiwara

Slide 2

Slide 2 text

自己紹介 @fujiwara (X/Twitter, GitHub, Bluesky) 面白法人カヤック SREチーム ISUCON 優勝 4回 / 運営 (出題 )4回 github.com/kayac/ecspresso github.com/fujiwara/lambroll

Slide 3

Slide 3 text

AWS CLIの起動は重い $ /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.58user 0.06system 0:00.65elapsed 100%CPU $ /usr/bin/time aws help > /dev/null 0.82user 0.09system 0:00.90elapsed 100%CPU 起動するだけで 1コア CPUを 100%使って 1秒弱 手元でたまに実行するならいいけど … shell scriptで処理を自動化してループでまわしたり 0.25vCPUの ECSや小さい Lambdaで実行したり (実時間で 3秒とか ) やりたい処理を Go+AWS SDKで書けば解決 …だが APIを一個呼ぶだけの処理をいちいち全部 Goで書き直すのも面倒くさい

Slide 4

Slide 4 text

「 AWS CLIを Goで実装して (シングルバイナリにして )ほしい」 AWSと Goを使っている人類なら全員 100回ぐらい思っているはず

Slide 5

Slide 5 text

aws-sdk-go-v2 の *Client ってみんな同じ見た目してるな ? cfg, _ := config.LoadDefaultConfig(ctx) svc := s3.NewFromConfig(cfg) out, err := svc.GetObject(ctx, &s3.GetObjectInput{ ... }) // ... svc := ssm.NewFromConfig(cfg) out, err := svc.GetParameter(ctx, &ssm.GetParameterInput{ ... }) foo サービスの Bar APIを呼ぶのは全部これ func (*foo.Client) Bar( context.Context, *foo.BarInput, // Bar APIの入力 optFns ...func(*foo.Options) // オプション ) ( *foo.BarOutput, // Bar APIの出力 error )

Slide 6

Slide 6 text

自動生成できるのでは? github.com/aws/aws-sdk-go-v2/service/* の全 packageに対して *Client 型のメソッド全てに対して同一形式で呼び出せるので メソッド一覧を取れば全サービス全ての APIを呼ぶコードを生成できる

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

package main import ( "fmt" "reflect" "strings" "github.com/aws/aws-sdk-go-v2/service/s3" ) func main() { clientType := reflect.TypeOf(s3.New(s3.Options{})) for i := 0; i < clientType.NumMethod(); i++ { method := clientType.Method(i) fmt.Println("Function Name:", method.Name) fmt.Println("Function Type:", method.Type) // メソッドのパラメータとリターンタイプを抽出 params := make([]string, 0) for j := 0; j < method.Type.NumIn(); j++ { params = append(params, method.Type.In(j).Name()) } returns := make([]string, 0) for k := 0; k < method.Type.NumOut(); k++ { returns = append(returns, method.Type.Out(k).Name()) } fmt.Println("Parameters:", strings.Join(params, ", ")) fmt.Println("Returns:", strings.Join(returns, ", ")) fmt.Println() } }

Slide 9

Slide 9 text

Input/Output は全部 JSONで こんなテンプレートでコードを生成しまくる {{ range .Methods }} func {{ .Service }}_{{ .Method }}(ctx context.Context, awsCfg aws.Config, b []byte) ([]byte, error) { svc := {{ .Service }}.NewFromConfig(awsCfg) var in {{ .Service }}.{{ .Method }}Input if err := json.Unmarshal(b, &in); err != nil { return nil, err } out, err := {{ .Service }}.{{ .Method }}(ctx, &in) if err != nil { return nil, err } return json.Marshal(out) } {{ end }} // 動的dispatchのためにmapにfuncを入れる処理 {{ range .Methods }} func init() { clientMethods["{{ .Service }}_{{ .Method }}"] = {{ .Service }}_{{ .Method }} } {{ end }}

Slide 10

Slide 10 text

できたもの awslim github.com/fujiwara/awslim awslim is a CLI for AWS services by Go This CLI is auto generated from the AWS SDK Go v2 service client. Motivation While the AWS CLI is very useful, it can be resource intensive to boot up. awslim offers a simpler and faster alternative for limited use cases.

Slide 11

Slide 11 text

つかいかた awslim [ [ []]] 引数がない (Inputが {} でいい )やつはこれだけ $ awslim sts GetCallerIdentity メソッド名は kebab-case でも OK (AWS CLIぽい! ) $ awslim sts get-caller-identity Inputが必要なやつは JSON/Jsonnet文字列で渡す $ awslim ecs list-tasks '{"Cluster":"default"}' # JSON $ awslim ecs list-tasks '{Cluster:"default"}' # Jsonnet

Slide 12

Slide 12 text

今日リリースした v0.3.0で … AWS CLIみたいに引数で Inputのフィールドを指定できるように! $ awslim ecs list-tasks --cluster=default ただし現時点では文字列以外のフィールド (数値、真偽値、リストなど )は未サポート (JSON / Jsonnetを併用してください ) $ awslim ecs list-tasks --cluster=default '{MaxResults:10}'

Slide 13

Slide 13 text

【課題】バイナリサイズがでかい (500MB〜 ) AWS SDKにある全てのサービス (現時点で 384! )の全てのメソッドを組み込んだ結果 … AWS CLIは zip展開後 225MBなので 2倍程度 特定のサービスだけビルドできるようにしてみた # gen.yaml services: ecs: - DescribeClusters # 指定したメソッドのみビルド - DescribeTasks sts: # 指定しなければ全部のメソッド 環境変数でも $ AWSLIM_GEN=ecs,ec2,s3 make 試しに使ったことがある 40サービスでビルドしたら 92MB、これぐらいなら …?

Slide 14

Slide 14 text

ビルドは Docker がおすすめ AWSLIM_GEN で指定したサービスだけビルドできます $ docker run -it -e AWSLIM_GEN=ecs ghcr.io/fujiwara/awslim:builder ... Completed. Please extract /app/awslim from this container! For example, run the following command: docker cp $(docker ps -lq):/app/awslim . マルチステージでも FROM ghcr.io/fujiwara/awslim:builder AS builder ENV AWSLIM_GEN=ecs,firehose,s3 ENV GIT_REF=v0.3.0 RUN ./build-in-docker.sh FROM debian:bookworm-slim COPY --from=builder /app/awslim /usr/local/bin/awslim

Slide 15

Slide 15 text

パフォーマンス比較 例 : (aws|awslim) sts get-caller-identity 0.25 vCPU の Fargate(AMD64) で /usr/bin/time -v の出力を元に比較 command CPU time(user, sys) Elapsed time(s) Max memory(KB) aws 0.70 + 0.06 = 0.76 3.02 65,868 awslim 0.08 + 0.03 = 0.11 0.40 111,616 awslim(40) 0.03 + 0.00 = 0.03 0.09 35,728 全部入りでも 7倍速 (ただしメモリは 2倍 )、 40サービスだけビルドしたら数十倍速 !! aws-cli/2.17.7 Python/3.11.8, awslim 0.2.0

Slide 16

Slide 16 text

便利機能いろいろ helpは SDKのドキュメント URLに丸投げ $ awslim ecs describe-clusters help See https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ecs#Client.DescribeClusters Goで SDKを使って開発しているときにも一発でドキュメントを開けて便利

Slide 17

Slide 17 text

--query (-q) : JMESPathで結果をクエリできる (AWS CLI同様 ) --raw (-r) : jq -r と同じ $ awslim sts get-caller-identity { "Account": "012345678901", "Arn": "arn:aws:sts::012345678901:role/xxxx", "UserId": "xxxxxx", } $ awslim sts get-caller-identity -q Account "012345678901" $ ACCOUNT_ID=$(awslim sts get-caller-identity -q Account -r) $ echo $ACCOUNT_ID 012345678901

Slide 18

Slide 18 text

--input-stream (-i), --output-stream(-o) 入力構造体の io.Reader 、出力構造体の io.ReadCloser フィールドとファイルを結びつける機能 $ awslim s3 put-object '{Bucket:"my-bucket",Key:"my.jpg",ContentType:"image/jpeg"}' \ --input-stream my.jpg s3.PubObjectInput#Body (io.Reader) に my.jpg の中身が渡される $ awslim s3 get-object '{Bucket:"my-bucket",Key:"my.jpg"}' \ --output-stream my.jpg s3.GetObjectOutput#Body (io.ReadCloser) の中身が my.jpg に書き込まれる どのフィールドに渡されるかは型を見て自動判別 (コード生成時に )

Slide 19

Slide 19 text

--follow-next (-f) ページングがある APIで {Outputのフィールド名}={Inputのフィールド名} を指定すると自動的に次を呼んでくれる機能 $ awslim s3 list-objects-v2 '{Bucket: "my-bucket",Delimiter:"/"}' \ --follow-next NextContinuationToken=ContinuationToken AWS CLIでは shell scriptでループしてコマンド起動し直しが必要 awslimは内部でループするので高速 入出力で同じフィールド名を使う APIの場合は名前のみで OK $ awslim ecs list-tasks --cluster=default -f NextToken

Slide 20

Slide 20 text

Jsonnet関数 _(n) n番目の引数を返す env(n, d) 環境変数 nがあればその値、 dを返す must_env(n) 環境変数 nがあればその値、なければエラー $ awslim ecs describe-clusters '{Clusters:[_(0)]}' foo $ CLUSTER=foo awslim ecs describe-clusters '{Clusters:[env("CLUSTER","default")]}' $ CLUSTER=foo awslim ecs describe-clusters '{Clusters:[must_env("CLUSTER")]}' Inputを Json/Jsonnetで構築するのが面倒な時に便利 (単純な文字列は v0.3.0で flagで渡せるように )

Slide 21

Slide 21 text

設定ファイルで URLを開いたり Aliasを定義したり ~/.config/awslim/config.(json|jsonnet|yaml|yml) open: /usr/bin/open # URLを開くコマンド aliases: whoami: sts get-caller-identity regions: ec2 describe-regions --query Regions[].RegionName logs: cloudwatchlogs # sdkとcliでコマンドが違うやつがある ce: costexplorer $ awslim whoami # awslim sts-get-caller-identity と同じ (alias機能は AWS CLIにもあります )

Slide 22

Slide 22 text

ここまでのオプションを組み合わせると … aliases: s3put: s3 put-object '{Bucket:_(0),Key:_(1)}' s3get: s3 get-object '{Bucket:_(0),Key:_(1)}' $ awslim s3put mybucket foo/upload.txt -i upload.txt $ awslim s3get mybucket foo/download.txt -o download.txt aws s3 cp 的なことができたり

Slide 23

Slide 23 text

aliases: s3ls: | s3 list-objects-v2 '{Bucket:_(0),Delimiter:"/",Prefix:_(1)}' -q '[CommonPrefixes[], Contents[].{Key:Key,Size:Size,LastModified:LastModified}]' -f NextContinuationToken=ContinuationToken $ awslim s3ls mybucket foo/ [ [ { "Prefix": "bar/" } ], [ { "Key": "foo/upload.txt", "LastModified": "2024-06-26T04:26:52Z", "Size": 81 } ] ]

Slide 24

Slide 24 text

【実験】 UPX (upx.github.io) を使ってバイナリを圧縮 コマンド サイズ 起動時間 awslim 526MB 90ms awslim(upx) 101MB 1500ms awslim(40) 92MB 20ms awslim(40, upx) 19MB 350ms サイズは 1/5、起動時間は 16倍、うまくやればあるいは … 適当なサービス単位でまとめてバイナリを作る (awslim-a, b, c...) 軽量な起動コマンド (awslim)からサービスに対応したバイナリを exec

Slide 25

Slide 25 text

まとめ AWS CLIの限定的な代替品となる Go実装の awslimを作りました 起動速度は 7倍! (ただしサイズは 2倍 ) 必要なサービスだけ自分でビルドするともっと小さく速くなります 手元でも Dockerでも簡単にビルドできます サーバー用途で使うにはこちらがおすすめです v0.3.0で文字列フラグ指定がサポートされてかなり便利に 便利機能を組み合わせて自分だけの最強コマンドを構築しよう! (公式が Goか Rustで実装してくれるのを待ってます )