Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

FUJIWARA Shunichiro

July 04, 2024
Tweet

More Decks by FUJIWARA Shunichiro

Other Decks in Technology

Transcript

  1. 自己紹介 @fujiwara (X/Twitter, GitHub, Bluesky) 面白法人カヤック SREチーム ISUCON 優勝 4回

    / 運営 (出題 )4回 github.com/kayac/ecspresso github.com/fujiwara/lambroll
  2. 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で書き直すのも面倒くさい
  3. 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 )
  4. 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() } }
  5. 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 }}
  6. できたもの 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.
  7. つかいかた awslim [<service> [<method> [<input>]]] 引数がない (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
  8. 今日リリースした v0.3.0で … AWS CLIみたいに引数で Inputのフィールドを指定できるように! $ awslim ecs list-tasks

    --cluster=default ただし現時点では文字列以外のフィールド (数値、真偽値、リストなど )は未サポート (JSON / Jsonnetを併用してください ) $ awslim ecs list-tasks --cluster=default '{MaxResults:10}'
  9. 【課題】バイナリサイズがでかい (500MB〜 ) AWS SDKにある全てのサービス (現時点で 384! )の全てのメソッドを組み込んだ結果 … AWS

    CLIは zip展開後 225MBなので 2倍程度 特定のサービスだけビルドできるようにしてみた # gen.yaml services: ecs: - DescribeClusters # 指定したメソッドのみビルド - DescribeTasks sts: # 指定しなければ全部のメソッド 環境変数でも $ AWSLIM_GEN=ecs,ec2,s3 make 試しに使ったことがある 40サービスでビルドしたら 92MB、これぐらいなら …?
  10. ビルドは 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
  11. パフォーマンス比較 例 : (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
  12. 便利機能いろいろ 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を使って開発しているときにも一発でドキュメントを開けて便利
  13. --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
  14. --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 に書き込まれる どのフィールドに渡されるかは型を見て自動判別 (コード生成時に )
  15. --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
  16. 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で渡せるように )
  17. 設定ファイルで 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にもあります )
  18. ここまでのオプションを組み合わせると … 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 的なことができたり
  19. 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 } ] ]
  20. 【実験】 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
  21. まとめ AWS CLIの限定的な代替品となる Go実装の awslimを作りました 起動速度は 7倍! (ただしサイズは 2倍 )

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