Go API(Gin)をCloud Functionsにデプロイする方法(CLI, Terraform)

 ## はじめに
Glolang WebアプリケーションGinを用いたサーバーレスアプリケーションをgcloud CLI, Terraformそれぞれを用いてCloud Functionsにデプロイします

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

## 環境
Golang: 1.20.4
Terraform v1.6.6

## ディレクトリ構成
```
.
├── gin
│   ├── cmd
│   │   └── main.go
│   ├── function-source.zip
│   ├── gin_http.go
│   ├── go.mod
│   └── go.sum
└── infra
    ├── main.tf
    └── terraform.tfvars
```

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

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

```Golang:./gin/gin_http.go
package http

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>


### Cloud Functionsにデプロイするために書き換える
1. "github.com/GoogleCloudPlatform/functions-framework-go/functions"をimport
    ```Golang
    import (
    	"net/http"

    	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
    	"github.com/gin-gonic/gin"
    )
    ```
    
2.  func mainをfunc ginHTTPに書き換え、以下のように変更する
    ```Golang
    func ginHTTP(w http.ResponseWriter, r *http.Request) {
        router := gin.Default()
        router.GET("/albums", getAlbums)
        router.GET("/albums/:id", getAlbumByID)
        router.POST("/albums", postAlbums)
    
    	router.ServeHTTP(w, r)
    }
    ```
    
4. init関数を用意する
    ```Golang
    func init() {
    	functions.HTTP("ginHTTP", ginHTTP)
    }
    ```
    
5. go.modファイルの作成
    以下のコマンドを実行する
    ```Shell:./gin
    $ go mod init example.com/gin
    $ go mod tidy
    ```

6. cmd/main.goを作成
    cmdディレクトリを作成し、その中に以下のmain.goを作成する
    ```Golang:./gin/cmd/main.go
    package main
    
    import (
    	"log"
    
    	_ "example.com/gin"
    
    	"github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
    )
    
    func main() {
    	port := "8080"
      if err := funcframework.Start(port); err != nil {
        log.Fatalf("funcframework.Start: %v\n", err)
      }
    }

    ```

6. ローカルでテスト
    以下のコマンドでローカルサーバーが立ち上がることが確認できる
    ```Shell:./gin
    $ export FUNCTION_TARGET=ginHTTP
    $ go run cmd/main.go
    $ curl "http://localhost:8080/albums"
    [
        {
            "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
        }
    ]
    ```

## CLIを用いてGCPにデプロイ

### 前準備
[Create and deploy an HTTP Cloud Function with Go](https://cloud.google.com/functions/docs/create-deploy-http-go)にある、Before you begin章に従ってgcloud cliの設定を行う

### デプロイ
```Shell:./gin
$ gcloud functions deploy go-http-function \
    --gen2 \
    --runtime=go121 \
    --region=<前準備で設定したregion> \
    --source=. \
    --entry-point=ginHTTP \
    --trigger-http \
    --allow-unauthenticated
```

デプロイ後に表示されるurlを用いてアクセスすると、Gin Tutorialと同じレスポンスが返ってくる

```Shell:任意の場所
$ curl https://<region>.***.cloudfunctions.net/go-http-function/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
    }
]

```


## Terraformを用いてGCPにデプロイ

### 前準備
1. [Get Started - Google Cloud](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started)Install Terraformまでの章に従ってterafformをinstallする。

2. [Terraform Tutorial (2nd gen)](https://cloud.google.com/functions/docs/tutorials/terraform)のBefore you beginに従って設定を行う。

3. 以下のコマンドを用いてgolangコードのzipファイルを生成する。
    ```Shell:./gin
    zip -r function-source.zip .
    ```

### terraform

main.tfの最初に、GCPで用いるための一般部分を記述する。

```terraform: ./infra/main.tf
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "4.51.0"
    }
  }
}

variable "CREDENTIALS" {
    type = string
}

variable "PROJECT" {
    type = string
}

provider "google" {
  credentials = file(var.CREDENTIALS)

  project = var.PROJECT
  region  = "<YOUR REGION>"
  zone    = "<YOUR ZONE>"
}

resource "random_id" "default" {
  byte_length = 8
}

resource "google_storage_bucket" "default" {
  name                        = "${random_id.default.hex}-gcf-source"
  location                    = "ASIA"
  uniform_bucket_level_access = true
}
```

上においてCREDENTIALSとPROJECTは環境変数を用いているが、これはterraform.tfvarsファイルを作って以下のように記述し、main.tfでvariableを用いることで使用できる。

```terraform:./infra/terraform.tfvars
CREDENTIALS="<YOUR CREDENTIALS>"
PROJECT="<YOUR PROJECT>"
```

続いて、この下にCloud Functionsに関する部分を記述する。
```terraform:./infra/main.tf
data "archive_file" "default" {
  type        = "zip"
  output_path = "../gin/function-source.zip"
  source_dir  = "../gin"
}

resource "google_storage_bucket_object" "object" {
  name   = "function-source.zip"
  bucket = google_storage_bucket.default.name
  source = data.archive_file.default.output_path
}

resource "google_cloudfunctions2_function" "default" {
  name        = "gin-cloud-functions"
  location    = "<YOUR REGION>"
  description = "Gin server"

  build_config {
    runtime     = "go121"
    entry_point = "ginHTTP"
    source {
      storage_source {
        bucket = google_storage_bucket.default.name
        object = google_storage_bucket_object.object.name
      }
    }
  }

  service_config {
    max_instance_count = 1
    available_memory   = "256M"
    timeout_seconds    = 60
  }
}

resource "google_cloud_run_service_iam_member" "member" {
  location = google_cloudfunctions2_function.default.location
  service  = google_cloudfunctions2_function.default.name
  role     = "roles/run.invoker"
  member   = "allUsers"
}

output "function_uri" {
  value = google_cloudfunctions2_function.default.service_config[0].uri
}


```


### デプロイ
1. terraformの初期化
    ```Shell:./infra
    terraform init
    ```
2. デプロイ
    ```Shell:./infra
    terraform apply
    ```
    
    デプロイ後にAPI endpointが表示されます
    ```
    Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
    
    Outputs:
    
    function_uri = "https://gin-cloud-functions-***.a.run.app"
    ```
    
    実際にAPIを叩くと、Gin Tutorialと同じレスポンスが返ってきます
    ```Shell:任意の場所
    $ curl "https://gin-cloud-functions-***.a.run.app/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
        }
    ]
        
    ```