たった10分で始める！AWS における最速のアプリ開発方法

## はじめに
こちらは AWS for Games Advent Calendar 2023 18 日目の記事となります。

迅速なアイデアの実装と検証は、あらゆるソフトウェア開発において重要です。このブログでは、AWS における最速の開発手法を [lambda-container-deploy](https://github.com/axot/lambda-container-deploy
) を利用し解説します。あなたのアイデアを速やかに実現しましょう！

このブログで学べること：

1. Go言語を用いた hello world のウェブアプリの作り方
2. 10 分以内で aws にホスティングする手法
3. IP 制限の実装
4. AWS における分散トレーシング、ロギングの実現
5. ローカル環境の構築

## Go ウェブアプリケーションの準備

最初に、Go言語で hello world のプログラムを書きます。ポート 8000 番で Listen します。

```go:main.go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func topHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Println("hello world")
	io.WriteString(w, "hello world")
}

func main() {
	http.Handle("/", http.HandlerFunc(topHandler))
	http.ListenAndServe(":8000", nil)
}
```
## Dockerfile の作成
マルチステージビルドを利用します。今回は Lambda Container にホスティングするため、[aws-lambda-adapter](https://github.com/awslabs/aws-lambda-web-adapter) を使って実現しています。

```dockerfile:Dockerfile
FROM golang:1.21 as builder

ENV GOPROXY=direct

WORKDIR /app

COPY go.* ./
RUN go mod download

COPY . ./

RUN go build -v -o myapp

FROM debian:stable-slim

WORKDIR /app

# tell lambda-adapter which port is listening
ENV PORT=8000

# handle lambda runtime api, convert requests to localhost:8000
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1 /lambda-adapter /opt/extensions/lambda-adapter
COPY --from=builder /app/myapp /app/myapp

CMD ["/app/myapp"]
```

## Lambdaにデプロイ

Lamdba に必要なリソースの作成、更新、デプロイは [lambda-container-deploy](https://github.com/axot/lambda-container-deploy
) を利用します。`lambda.sh deploy` 一つのコマンドでビルドからデプロイまでを行います。入力項目としては、AWSのリージョンだけとなります。また、サービス名とリージョンは、`.lambda/config.ini` に自動保存されるため、2回目以降は入力なしで実行が可能です。

```bash
$ ./lambda.sh deploy
Deploying Lambda function...
Enter service name [lambda-container-blog]: 
Enter AWS region: ap-northeast-1
[+] Building 10.7s (18/18) FINISHED                                                                                                                  docker:orbstack
 => [internal] load .dockerignore                                                                                                                               0.0s
 => => transferring context: 2B                                                                                                                                 0.0s
 => [internal] load build definition from Dockerfile                                                                                                            0.0s
 => => transferring dockerfile: 542B                                                                                                                            0.0s
 => [internal] load metadata for public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1                                                                                2.3s
 => [internal] load metadata for docker.io/library/golang:1.21                                                                                                  2.7s
 => [internal] load metadata for docker.io/library/debian:stable-slim                                                                                           2.5s
 => [builder 1/6] FROM docker.io/library/golang:1.21@sha256:2ff79bcdaff74368a9fdcb06f6599e54a71caf520fd2357a55feddd504bcaffb                                    0.0s
 => => resolve docker.io/library/golang:1.21@sha256:2ff79bcdaff74368a9fdcb06f6599e54a71caf520fd2357a55feddd504bcaffb                                            0.0s
 => FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1@sha256:97c8f81e19e64841df0882d3b3c943db964c554992c1bac26100f1d6c41ea0bb                                0.0s
 => [stage-1 1/4] FROM docker.io/library/debian:stable-slim@sha256:375fb84f3c64691a1b9a9ff5ff3905173dcd0c5e11bc2aebd5c3472a139fa2b4                             0.0s
 => [internal] load build context                                                                                                                               0.0s
 => => transferring context: 17.80kB                                                                                                                            0.0s
 => CACHED [builder 2/6] WORKDIR /app                                                                                                                           0.0s
 => CACHED [builder 3/6] COPY go.* ./                                                                                                                           0.0s
 => CACHED [builder 4/6] RUN go mod download                                                                                                                    0.0s
 => [builder 5/6] COPY . ./                                                                                                                                     0.0s
 => [builder 6/6] RUN go build -v -o myapp                                                                                                                      7.8s
 => CACHED [stage-1 2/4] WORKDIR /app                                                                                                                           0.0s
 => CACHED [stage-1 3/4] COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1 /lambda-adapter /opt/extensions/lambda-adapter                             0.0s
 => [stage-1 4/4] COPY --from=builder /app/myapp /app/myapp                                                                                                     0.0s
 => exporting to image                                                                                                                                          0.0s
 => => exporting layers                                                                                                                                         0.0s
 => => writing image sha256:6c2f79b6321f0cd9b4e7693763d1fb56208c85cc54edf4c2ea1ce9ea25ab4733                                                                    0.0s
 => => naming to docker.io/library/lambda-container-blog:1afe4a5                                                                                                0.0s
Login Succeeded
The push refers to repository [xxx.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-container-blog]
9bec1d67c6e9: Pushed 
a809e9675065: Pushed 
e7f40a6b4e08: Pushed 
03459226eed7: Pushed 
1afe4a5: digest: sha256:9affcfcb65c0846891960046a8c25aadf5d3ac24a0117eebc39349688a9b3d61 size: 1157
Lambda function lambda-container-blog does not exist. Creating function...
Function URL for lambda-container-blog does not exist. Creating Function URL...
Public Lambda Function URL: https://xxx.lambda-url.ap-northeast-1.on.aws/, you can change auth-type to AWS_IAM
```
あっという間に、外部からアクセス可能な URL が表示されました。`https://xxx.lambda-url.ap-northeast-1.on.aws/` の部分です。

### IP 制限機能
ここまでは、外部からだれもアプリケーションをアクセスすることができますが、IP 制限をかけたい場合も多いと思います。Golang で IP 制限機能を入れてみましょうー

```go:main.go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"github.com/jpillora/ipfilter"
)

func topHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Println("hello world")
	io.WriteString(w, "hello world")
}

func addWhiteListIPs(f *ipfilter.IPFilter) {
	// Get the WHITE_IP_LIST environment variable
	whiteIPList := os.Getenv("WHITE_IP_LIST")

	// Check if the environment variable is set
	if whiteIPList == "" {
		fmt.Println("WHITE_IP_LIST environment variable is not set")
		return
	}

	// Split the WHITE_IP_LIST into separate IP addresses/CIDR ranges
	ipList := strings.Split(whiteIPList, ",")

	// Iterate over each IP/CIDR in the list
	for _, ip := range ipList {
		fmt.Println("IP/CIDR:", ip)
		f.AllowIP(ip)
	}
}

func main() {
	f := ipfilter.New(ipfilter.Options{
		AllowedIPs:     []string{"192.168.0.0/16", "10.0.0.0/8", "172.16.0.0/12"},
		BlockByDefault: true,
		TrustProxy:     true,
	})

	addWhiteListIPs(f)
	http.Handle("/", f.Wrap(http.HandlerFunc(topHandler)))
	http.ListenAndServe(":8000", nil)
}
```
環境変数 `WHITE_IP_LIST` に自分の IP アドレスを入れて、デプロイします。

```bash
# .env に自分の IP を設定します。
$ cat << EOF > .env
WHITE_IP_LIST=$(curl --silent https://checkip.amazonaws.com)/32
EOF

$ cat .env
WHITE_IP_LIST=xx.xx.xx.xx/32

# --env-vars-file を利用し、Lambda の環境変数を設定し、デプロイします。
$ ./lambda.sh deploy --env-vars-file .env
```
これで、IP 制限を実現できました！

## 分散トレーシング、ロギング
開発はトラブルシューティングが一番時間かかっていること、皆さんはよくご存知かと思います。X-Ray のような分散トレーシングの機能を利用すれば、どの関数がエラー出ていたとか、その時関数が使用した時間、関連するログなどをワン画面で表示することができます。Lambda では、手軽くに利用することができます。ここで Go アプリからの利用方法をご紹介します。

トレーシングに関しては、`xray.Handler()`および`xray.NewFixedSegmentNamer()`を入れるだけで完了です。
```go
http.Handle("/", xray.Handler(xray.NewFixedSegmentNamer("myApp"), f.Wrap(http.HandlerFunc(topHandler))))
```

たとえば、必要なデバッグ情報を入れたりもできます。ここでは、`xray.AddAnnotation()`や`xray.AddMetadata()`を使って、必要な情報を入れたり、`xraylogger`の方は、ログとトレーシングを結びつけることができます。
```go:main.go
func topHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	log := xraylogger.New(ctx)

	// Add annotation and metadata
	xray.AddAnnotation(ctx, "User", "example-user2")
	xray.AddMetadata(ctx, "debug-info", "sample metadata2")

	log.Info("hello world")
	io.WriteString(w, "hello world")
}
```

Console の方はこのように表示されます。
![xray.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29819/4b266e09-06c7-d5b8-52b4-bfee09e659a4.jpeg)

また、ローカルでログを確認するケースもありますので、こちらのコマンドを利用すると便利です。

```bash
# 直近1分以内のログを live tail しながら表示します
$ aws logs tail --follow --since 1m /aws/lambda/関数名
```

## ローカルでのテスト
コンテナを利用するメリットの一つは、ローカルだけで確認したい場合でも、同じ Dockerfile を使うことができ、。`docker-compose.yaml` のサンプルコードはこちらとなります。

```docker:compose.yaml
version: '3.8'
services:
  my-lambda-app:
    build: .
    ports:
      - "8000:8000"
```

ローカル環境を起動
```bash
$ docker-compose build
$ docker-compose up -d
```
http://localhost:8000/ にアクセスすれば、hello world が表示されています。

## まとめ

AWS Lambda コンテナを使用したアプローチは、開発者が迅速にアイデアを実装し、手軽に検証するための強力なソリューションです。より簡単に利用できるように `lambda-container-deploy` を開発し、設定やデプロイメントの手間を減らし、開発に集中できる環境を提供しました。開発がある程度進んでいたら、忘れずに IaC を利用して、環境を構築していきましょう。では楽しい開発を是非体験してください。

(免責) 本記事の内容はあくまでも個人の意見であり、所属する企業や団体は関係ございません。