less is more

心のフルスタックエンジニア👨‍💻バイブスでコードを書いています🤘

Lambdaの関数をランタイムごとに数える

完成イメージ

$ abc lambda stats
|         RUNTIME          | COUNT |
|--------------------------|-------|
| nodejs12.x               |     2 |
| nodejs8.10(Deprecated)    |     1 |
| python3.6                |     2 |
| ruby2.5                  |     8 |
| ruby2.7                  |     2 |

--verboseオプションで関数名も表示

$ abc lambda stats --verbose
|         RUNTIME          | COUNT |           FUNCTIONS           |
|--------------------------|-------|-------------------------------|
| nodejs12.x               |     2 | sample-nodejs-12-function-1   |
|                          |       | sample-nodejs-12-function-2   |
| nodejs8.10(Deprecated)    |     1 | sample-nodejs-8-function      |
| python3.6                |     2 | sample-python-36-function-1,  |
|                          |       | sample-pytohn-36-function-2   |
| ruby2.5                  |     8 | sample-ruby-25-function-1,    |
|                          |       | sample-ruby-25-function-2,    |
|                          |       | sample-ruby-25-function-3,    |
|                          |       | sample-ruby-25-function-4,    |
|                          |       | sample-ruby-25-function-5,    |
|                          |       | sample-ruby-25-function-6,    |
|                          |       | sample-ruby-25-function-7,    |
|                          |       | sample-ruby-25-function-8     |
| ruby2.7                  |     2 | sample-ruby-27-function-1,    |
|                          |       | sample-ruby-27-function-2     |

--formatオプションでJSONフォーマットで出力

$ abc lambda stats --format json | jq "."
[
  {
    "runtime": "nodejs12.x",
    "count": 2,
    "deprecated": false
  },
  {
    "runtime": "nodejs8.10",
    "count": 1,
    "deprecated": true
  },
  {
    "runtime": "python3.6",
    "count": 2,
    "deprecated": false
  },
  {
    "runtime": "ruby2.5",
    "count": 8,
    "deprecated": false
  },
  {
    "runtime": "ruby2.7",
    "count": 2,
    "deprecated": false
  }
]

CLIツール

もうブログで何回も紹介しているgo製のCLIツールabcのサブコマンドとして実装する。

github.com

ランタイム一覧

docs.aws.amazon.com

現在サポートされているランタイムは以下。

  • nodejs12.x
  • nodejs10.x
  • python3.8
  • python3.7
  • python3.6
  • python2.7
  • ruby2.7
  • ruby2.5
  • java11
  • java8
  • go1.x
  • dotnetcore3.1
  • dotnetcore2.1
  • provided(カスタムランタイム)

ランタイムサポートポリシー

docs.aws.amazon.com

Lambdaのランタイムにはランタイムサポートポリシーというものが設定されていて、要はセキュリティパッチなどちゃんと実行環境がメンテナンスされているかを表すもの。

最新でないバージョンの言語のランタイムは随時廃止されて行く模様。
廃止は段階的に行われる。

まずはそのランタイムで新しい関数が作成できなくなり、続いて既存の関数の更新ができなくなる。 その後は、既存の関数は廃止されることなくそのまま使い続けることはできるが、実行環境のメンテナンスは行われないので移行が推奨される。

すでに廃止されているランタイムは以下。

  • dotnetcore1.0
  • dotnetcore2.0
  • nodejs(Node.js 0.10)
  • nodejs4.3
  • nodejs4.3-edge
  • nodejs6.10
  • nodejs8.10

現在(2020/06/08)時点で廃止予定のランタイムはないが、Python2.7まわりはざわざわしている。 2020/12/31までセキュリティパッチは当てられるようだが、いつ廃止されるという明記はない。

aws.amazon.com

実装

lambda:ListFunctionsAPIで関数が列挙できる。
再帰的に取得して結果を返す部分の抜粋。

func listFunctions() ([]*lambda.FunctionConfiguration, error) {
    var result []*lambda.FunctionConfiguration
    var nextMarker *string
    for {
        params := &lambda.ListFunctionsInput{
            MaxItems: aws.Int64(1000),
            Marker:   nextMarker,
        }
        resp, err := LambdaClient.ListFunctions(params)
        if err != nil {
            return nil, err
        }
        result = append(result, resp.Functions...)
        if resp.NextMarker == nil {
            break
        }
        nextMarker = resp.NextMarker
    }

    return result, nil
}

パラメータは、なるべく1回で取得するためにMaxItemsを大きめにとっている。次ページがある場合はMarkerを指定して再帰的に取得する。他のAPIではNextTokenという実装をよく見るがMarkerは初めて見た🤔

他にもバージョンの指定やLambda@Edge用にリージョンの指定もできるのだが今回は必要ないので割愛。

集計はランタイムごとにmapに突っ込んでいて、ソートされたキー順に出力するのだが、一点微妙なところがある。Node.jsのようにバージョンが2桁になっているランタイムでは、リリース順にきれいに並ばないみたいなことが起きる。

(例) 昇順にソートしているが、一番上に最新のnodejs12.xがきてしまう

RUNTIME COUNT
nodejs12.x 2
nodejs10.x 2
nodejs8.10(Deprecated) 1
python3.6 1
python3.7 1
python3.8 2
ruby2.5 8
ruby2.7 2

これを考慮したソートを実装するのはちょっと骨が折れるので今回はやらなかった。

出力のフォーマットはマークダウンのtableかJSONを選択できるようにした。
これまではJSON縛りで作ることが多かったが、今回のユースケースではマークダウンにペッと貼り付けたいこともあるかなと思ったので。

func Output(count map[string][]string) (string, error) {
    if format == "json" {
        return jsonOutput(count)
    } else if format == "table" {
        return tableOutput(count), nil
    }
    return "", errors.New("invalid format.")
}

JSONは代わり映えしないので省略するとして、テーブル形式でのフォーマットの実装に関して。

最初は標準のtext/tabwriterを試した。
最近リリースされたAWS謹製のec2-instance-selectorではこれを使ってテーブル出力を実装している。

github.com

使い方もわかりやすく標準で賄えるのはいいのだが、凝ったことをしようとするとなかなか厳しい。それに、色をつけようとするとタブ幅が崩れるという問題があった。

代わりに以下のライブラリを導入した。

github.com

こちらではマークダウン形式のテーブルがサポートされていて、表崩れもなかったのでめでたく採用。

柔軟な色付けもできるっぽい。
(柔軟と言いつつ今回は実装が煩雑になるので自前でカラーコードを埋め込んだが)

func tableOutput(count map[string][]string) string {
    keys := sortKey(count)
    tableString := &strings.Builder{}
    table := tablewriter.NewWriter(tableString)
    table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
    table.SetCenterSeparator("|")
    if verbose {
        verboseTableOutput(keys, table, count)
    } else {
        normalTableOutput(keys, table, count)
    }
    return tableString.String()
}

func normalTableOutput(keys []string, table *tablewriter.Table, count map[string][]string) {
    table.SetHeader([]string{"Runtime", "Count"})
    for _, k := range keys {
        runtime := k
        if isDeprecatedRuntime(runtime) {
            runtime = fmt.Sprintf("\x1b[91m%s(Deprecated)\x1b[0m", runtime)
        }
        table.Append([]string{runtime, strconv.Itoa(len(count[k]))})
    }
    table.Render()
}

廃止済みのランタイムを使っている場合は赤くして警告する。

f:id:bluepixel:20200610130002p:plain

v0.5.0としてリリース

Release v0.5.0🎉 · Blue-Pix/abc · GitHub