Lambdaでko!

## これは何？

これは [MEGAZONE 株式会社](https://www.megazonejapan.jp/) のテック陣「MEGAZONEのゆかいな仲間たち」がおくる、[Megazone Japan Advent Calendar 2023](https://qiita.com/advent-calendar/2023/megazonejapan) の一日目のエントリーです。

## はじめに

いやー、あっという間に12月ですね。今年は秋っぽい空気を楽しむチャンスがほとんどない天候のまま冬がやってきましたが、皆さんも「え？もう Advent Calendar の季節？どうしようエントリーしたけどまだ書いてないよ？まじやばいよー」なんて慌ててませんか？えぇ、私がまさにその状態です。

まともに Advent Calendar を書くなんて何年ぶりだろう？と記憶を掘り起してみると、実に6年ぶりぐらいのエントリーとなります。Private Cloud 界隈では 2012 年ごろから 2017 年ぐらいにかけて Advent Calendar が盛んでしたが、その後は Private Cloud を構築するための基盤ソフトウェアが OpenStack 一強となりまして、私のまわりでは Advent Calendar が下火になっていきましたが、一方で世の中(Qiita)の Advent Calendar は以下のように推移してきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42575/146b5268-ebf0-f012-208e-26f571d57ae7.png)

ご覧頂くとわかるように、2014年ではカレンダー数が200程度だったのが、2022年では900に到達する勢いです。また一番特徴的なのが、2015年からカテゴリが設定できるようになり、Organizations のカレンダー数が年々増加しています。

そう! 時代は組織・団体による Advent Calendar 戦国時代! 乗るしかない、このビッグウェーブに! (表現が古くて、ほんとすまん)

前置きが長くなりましたが、上記のような背景があり、また当社は未だ国内では無名に等しいクラウドリセラーなため、なんとしても露出の機会を増やしたい! 少しでも露出したい! ポロ〜ン♪
ということで、社内のテック陣を騙し…唆して Advent Calendar をやってみることにしました。

そろそろ前置きが長すぎて「ポエム書くなら他所でやれ」とか言われかねないので、本題に入ります。(過去の自分の Advent Calendar のエントリーに比べると、これでも前置きが 1/10 ぐらいの短かさなんですよ…)

## Lambda で ko

やっと本題です。あやうく前置きだけで 12月1日 が終わってしまうところでした。

みなさん、Golang はもう日常的にお使いになっていると思いますし、当然のことながら AWS Lambda も日々の業務はもちろん、朝な夕な使っていることでしょう。

そんな中、2023年12月31日を以って、AWS Lambda の Go1.x ランタイムが終了し、今後はカスタムランタイムの Amazon Linux 2 を使うよう[発表](https://aws.amazon.com/jp/blogs/compute/migrating-aws-lambda-functions-from-the-go1-x-runtime-to-the-custom-runtime-on-amazon-linux-2/)がありました。

世の中はこの発表を受けて、「カスタムランタイムに移行するほうがより良い未来が待っている」とか「ランタイムに Golang が挙げられてないのは Golang が標準サポートされないと捉えられ兼ない」など様々な意見が飛び交ったとか飛び交ってないとか色々とちょっとした騒動になったわけです。(ほんと？)

で、私としては「もうランタイムの提供具合で右往左往するのは嫌だ」と思い、今迄は「AWS Lambdaのコンテナイメージサポートって言っても、コンテナイメージの中のコードは AWS Lambda で動くコードを書かないといけないから、ちょっと思ってたのと違うんだよなー」とか生意気なことを考えていたのですが、今回この考えを改め、「そうか、コンテナイメージならランタイムの提供状況に右往左往することも無くなる!」と思うことにしました。

一方でコンテナイメージは、うっかり盛り盛りで作るとすぐ肥大化してしまいます。そうまるで私のお腹のように…。

そこで、今回このエントリーで取り上げたいのが [ko](https://github.com/ko-build/ko) です。ko 自体の紹介は日本語でも色々な方達がブログや X (旧:Twitter) でその便利さなどを紹介されているので、先達の情報を読んで頂くのが一番良いとは思いますが、このエントリーでは ko でビルドしたコンテナイメージを Lambda で使う道程について書かせて頂きます。

### 前提条件

* ECR のプライベートリポジトリを作成しておく
    * Dockerhub を使ってもいいんですが、折角 AWS を利用するので、ここでは ECR のプライベートリポジトリを利用することを前提とします。
* ここに挙げる各種コマンドは Linux で実行することを前提として書いています。基本的に他の OS でも動くとは思いますが、私は日常業務も Linux をメインで利用しているため、Windows や macOS については素人同然なため、もしお手元に Linux 環境が無いという奇特な方は AWS Cloud9 を利用することをお勧めします。
* 当然ながら Golang が利用できることを前提としますので、Golang のインストール方法とかは割愛します。

### ko のインストール

```bash
$ go install github.com/google/ko@latest
$ export PATH=$HOME/go/bin/:$PATH
## お使いの Shell に合わせて下記の1行は適宜読み替えてください
$ echo 'export PATH="$HOME/go/bin/:$PATH"' >> ~/.bashrc 
$ ko version
v0.15.1  ← のように出力されればインストール完了です
```

### サンプルコード

AWS のドキュメントにある [Lambda で実行する Golang のサンプルコード](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/golang-handler.html) をベースに、以下のようなコードを書きます。

```go:main.go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

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

type MyEvent struct {
	Name string `json:"name"`
}

func SayHello(name string) string {
	return fmt.Sprintf("Hello %s!", name)
}

func HandleRequest(ctx context.Context, event *MyEvent) (*string, error) {
	if event == nil {
		return nil, fmt.Errorf("received nil event")
	}
	message := SayHello(event.Name)
	return &message, nil
}

func main() {
	// en: If the environment variable AWS_LAMBDA_FUNCTION_NAME is not set, it is executed as a local function.
	// ja: 環境変数AWS_LAMBDA_FUNCTION_NAMEが設定されていない場合は、ローカル関数として実行されます。
	if "" == os.Getenv("AWS_LAMBDA_FUNCTION_NAME") {
		event := &MyEvent{Name: "World"}
		response, err := HandleRequest(context.Background(), event)
		if err != nil {
			log.Fatal(err)
		}
		log.Println(*response)
	} else {
		lambda.Start(HandleRequest)
	}
}
```

```go:main_test.go
package main

import (
	"context"
	"testing"
)

type MockContext struct {
	context.Context
}

func TestHandlerRequest(t *testing.T) {
	var ctx MockContext
	event := &MyEvent{Name: "World"}
	response, err := HandleRequest(ctx, event)
	if err != nil {
		t.Error(err)
	}
	expected := "Hello World!"
	if *response != expected {
		t.Errorf("expected %s, but got %s", expected, *response)
	}
}
```

### Go モジュールの初期化

```bash
$ go mod init example.com/lambda.go-hello
$ go mod tidy
```

### (好みで) テスト

```bash
$ go test -v
=== RUN   TestHandlerRequest
--- PASS: TestHandlerRequest (0.00s)
PASS
ok      example.com/lambda.go-hello     0.005s
```

### コンテナイメージの作成と登録

ECR リポジトリの URL やパスおよび各種パラメータはご自身の環境に合わせて読み替えてください

```bash
$ export AWS_ID=000000000000
$ export AWS_REGION=ap-northeast-1
$ aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin ${AWS_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
$ KO_DEFAULTPLATFORMS=linux/arm64 ko build -L main.go
2023/12/01 04:50:02 Using base cgr.dev/chainguard/static:latest@sha256:e0f163b457130b20176fb5ffd3a5528ee69c314ed02a0e9b5a4047806214f832 for main.go
2023/12/01 04:50:03 Building main.go for linux/arm64
2023/12/01 04:50:13 Loading ko.local/main.go-7ddfb3e035b42cd70649cc33393fe32c:2fda24f855d0a9dccdd734a86e8ae7e7ca76703854f2b607cfe9c8fe031ff21a
2023/12/01 04:50:15 Loaded ko.local/main.go-7ddfb3e035b42cd70649cc33393fe32c:2fda24f855d0a9dccdd734a86e8ae7e7ca76703854f2b607cfe9c8fe031ff21a
2023/12/01 04:50:15 Adding tag latest
2023/12/01 04:50:15 Added tag latest
ko.local/main.go-7ddfb3e035b42cd70649cc33393fe32c:2fda24f855d0a9dccdd734a86e8ae7e7ca76703854f2b607cfe9c8fe031ff21a
```

作成したイメージを確認

```bash
$ docker image ls ko.local/main.go-7ddfb3e035b42cd70649cc33393fe32c:2fda24f855d0a9dccdd734a86e8ae7e7ca76703854f2b607cfe9c8fe031ff21a
REPOSITORY                                          TAG                                                                IMAGE ID       CREATED       SIZE
ko.local/main.go-7ddfb3e035b42cd70649cc33393fe32c   2fda24f855d0a9dccdd734a86e8ae7e7ca76703854f2b607cfe9c8fe031ff21a   e1eebf71448c   11 days ago   10.9MB

```

作成したイメージを ECR のリポジトリに push

```bash
$ docker tag ko.local/main.go-7ddfb3e035b42cd70649cc33393fe32c:2fda24f855d0a9dccdd734a86e8ae7e7ca76703854f2b607cfe9c8fe031ff21a ${AWS_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/ko-lambda:latest
$ docker push ${AWS_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/ko-lambda:latest
The push refers to repository [000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/ko-lambda]
1b760825c805: Pushed 
ffe56a1c5f38: Pushed 
535c61268717: Pushed 
latest: digest: sha256:c933910f3fb2e78502cb5249f8c0bbc95bc8c1e0653f29f26c3ee6a082a419f7 size: 944
```

### Lambda Function の作成

以下の設定で Lambda を設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42575/6d1181d8-8906-37a9-10e7-fcbfd29858ce.png)

以下の設定でテストのデータを作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42575/01714cb3-8974-3b2b-88c2-415a7745f157.png)

### Lambda Function のテスト実行

テストタブにあるテストボタンをクリックしテスト実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42575/2685f577-07f9-6142-733f-1acb5f7f8af7.png)

以下のように "Hello World!!" と出力されていれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42575/9280ff9b-bbd4-affc-7a95-33c95e5bf609.png)

### おわりに

同僚を騙…唆す際に「普段の[テックブログ](https://zenn.dev/megazone_japan)や[MEGAZONEブログ](https://www.hyperbilling.jp/blog/)と違って、気軽にライトな分量で書けばいいですよ。もともとは『5分で考えて書く』というのが Advent Calendar の流儀？だったみたいですよ〜」と言った手前、一日目から難易度が高かったり、分量が多いものを書いたりして他の同僚を尻込みさせてしまうと良くないと思い、一日目は本当に手軽な内容にしました。(そのわりには私が遅筆なせいで、5分で書き上げられなかったのは反省です (反省しているとは言ってない))

それでは、二日目は同僚が書いてくれるはずです。(さすがに一人で25日全部書くとか狂気じみたことはやらないですので、御安心ください)