Ginを用いたサーバーレスアプリケーションをAWS CDKでデプロイする方法

 ## はじめに
Glolang WebアプリケーションGinを用いたサーバーレスアプリケーションをAWS CDKを使ってAWS Lambdaにデプロイします

今回用いたコードは[GitHub](https://github.com/tomoish/gin-aws-lambda)で公開しています。

## 環境
Golang: 1.20.4
AWS CDK: 2.96.0

## ディレクトリ構成
```
├── gin
│   ├── bin
│   │   ├── bootstrap
│   │   └── myFunction.zip
│   ├── main.go
└── infra
```

## Gin 
### Gin Tutorial
今回は[Gin Tutorial](https://go.dev/doc/tutorial/web-service-gin)で作れるRESTful APIをデプロイしようと思います。

<details><summary>このTutorialを終えた後のmain.goは以下の通りです。</summary>
<div>

```Golang
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// album represents data about a record album.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// albums slice to seed record album data.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
    var newAlbum album

    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID locates the album whose ID value matches the id
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}
```
</div></details>


### AWS Lambdaにデプロイするために書き換える
1. 以下のパッケージをimportします
    ```Golang
    import (
    	"context"
    	"net/http"
    
    	"github.com/aws/aws-lambda-go/events"
    	"github.com/aws/aws-lambda-go/lambda"
    	"github.com/awslabs/aws-lambda-go-api-proxy/gin"
    	"github.com/gin-gonic/gin"
    )
    ```
    その後、以下のコマンドを実行します
    ```Shell
    go get .
    ```
2. GinLambdaを宣言
    ```Golang
    var ginLambda *ginadapter.GinLambda
    ```
3.  func mainをfunc initに書き換え、以下のように変更する
    ```Golang
    func init() {
    	router := gin.Default()
    	router.GET("/albums", getAlbums)
    	router.POST("/albums", postAlbums)
    	router.GET("/albums/:id", getAlbumByID)
    
    	ginLambda = ginadapter.New(router)
    }
    ```
4. Handler, main関数を用意する
    ```Golang
    func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    	return ginLambda.ProxyWithContext(ctx, req)
    }
    
    func main() {
    	lambda.Start(Handler)
    }
    ```


<details><summary>最終的にmain.goはこのようになります。</summary>
<div>

```Golang
package main

import (
	"context"
	"net/http"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/awslabs/aws-lambda-go-api-proxy/gin"
	"github.com/gin-gonic/gin"
)

// album represents data about a record album.
type album struct {
	ID     string  `json:"id"`
	Title  string  `json:"title"`
	Artist string  `json:"artist"`
	Price  float64 `json:"price"`
}

// albums slice to seed record album data.
var albums = []album{
	{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
	{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
	{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

var ginLambda *ginadapter.GinLambda

func init() {
	router := gin.Default()
	router.GET("/albums", getAlbums)
	router.POST("/albums", postAlbums)
	router.GET("/albums/:id", getAlbumByID)

	ginLambda = ginadapter.New(router)
}

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
	c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
	var newAlbum album

	if err := c.BindJSON(&newAlbum); err != nil {
		return
	}

	albums = append(albums, newAlbum)
	c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID locates the album whose ID value matches the id
func getAlbumByID(c *gin.Context) {
	id := c.Param("id")

	for _, a := range albums {
		if a.ID == id {
			c.IndentedJSON(http.StatusOK, a)
			return
		}
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	return ginLambda.ProxyWithContext(ctx, req)
}

func main() {
	lambda.Start(Handler)
}

```
</div></details>

## AWS CDK

### AWS CDKが初めての方
こちらの[AWS Cloud Development Kit(AWS CDK)v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html)の、Getting startedを実際に行ってみるとわかりやすいです。

今回の実装ではTypeScriptを用います。

### AWS Lambda, API Gateway
今回の以下のようなサーバーレスアーキテクチャーをAWS CDKで構築します。

![ AWS_Architecture.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3507134/ecac728b-acf1-1d99-ed1e-69204c1bfc6b.jpeg)




### Gin側での準備

今回は、.zipファイルアーカイブを使用してGo Lambda関数をデプロイします。
[AWS公式](https://docs.aws.amazon.com/lambda/latest/dg/golang-package.html)を参考に、以下を実行します。(maxOS/Linuxを想定しています。Windowsの方は左のAWS公式を参照してください。)

1. main.goをコンパイルする
    x86_64 architectureの場合
    ```Shell:/gin
    $ GOOS=linux GOARCH=amd64 go build -tags lambda.norpc -o bin/bootstrap main.go
    ```

2. zipファイルを作成する
    ```Shell:/gin
    $ zip bin/myFunction.zip bin/bootstrap
    ```



### AWS CDK
1. init
    ```Shell:/infra
    $ cdk init app --language typescript
    ```
2. infra/lib/gin-lambda-stack.tsを以下のように変更し、それに合わせて、infra/bin/infra.ts内のstack名も変更します。
    <details><summary>gin-lambda-stack.ts</summary>
    <div>
    
    
    ```TypeScript:/infra/lib/gin-lambda-stack.ts
    import {
      aws_s3_assets as assets,
      aws_lambda as lambda,
      aws_apigateway as gateway,
      Stack,
      StackProps,
      Duration,
    } from "aws-cdk-lib";
    import * as path from "path";
    import { Construct } from "constructs";
    
    export class GinLambdaStack extends Stack {
      constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props);
    
        const lambdaAsset = new assets.Asset(this, "GinServerAsset", {
          path: path.join(__dirname, "../../gin/bin"),
        });
    
        const lambdaFn = new lambda.Function(this, "GinServer", {
          code: lambda.Code.fromBucket(lambdaAsset.bucket, lambdaAsset.s3ObjectKey),
          timeout: Duration.minutes(5),
          runtime: lambda.Runtime.PROVIDED_AL2,
          handler: "main",
        });
    
        new gateway.LambdaRestApi(
          this,
          "GinServerLambdaEndpoint",
          {
            handler: lambdaFn,
          }
        )
      }
    }
    
    ```
    </div></details>

3. デプロイ
    ```Shell:/infra
    $ cdk bootstrap
    ```
    
    デプロイ後にAPI endpointが表示されます
    
    ```Shell:/infra
    $ cdk deploy
    ```
    ```
     ✅  InfraStack
    
    ✨  Deployment time: 32.11s
    
    Outputs:
    InfraStack.GinServerLambdaEndpoint******** = https://**********.execute-api.<region>.amazonaws.com/prod/
    ```
    
    実際にAPIを叩くと、Gin Tutorialと同じレスポンスが返ってきます
    ```Shell:任意の場所
    $ curl https://**********.execute-api.<region>.amazonaws.com/prod/albums
    ```
    ```JSON
    [
        {
            "id": "1",
            "title": "Blue Train",
            "artist": "John Coltrane",
            "price": 56.99
        },
        {
            "id": "2",
            "title": "Jeru",
            "artist": "Gerry Mulligan",
            "price": 17.99
        },
        {
            "id": "3",
            "title": "Sarah Vaughan and Clifford Brown",
            "artist": "Sarah Vaughan",
            "price": 39.99
        }
    ]
    
    ```



## 参考文献
* https://blog.dennisokeeffe.com/blog/2020-07-06-deploying-a-serverless-go-api
    * この記事を参考にしつつ、現在のAWS CDKのバージョンに適したコードに変更しました。