Golangはじめて物語(第9話: Go SQL Drivers＋AWS SDK for the GoでAuroraに高速アクセスする)

# はじめに
GolangのDB接続は内部でGoroutineを使って勝手に簡単にコネクションプールを実現してくれるらしい。
なんと素晴らしい。早速試してみようではないか。
せっかくだから、Auroraと組み合わせてみよう。

なお、Auroraへのアクセス方法については、[公式のユーザーガイド](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.Connecting.Go.html)が参考になる。

また、接続にはIAM認証を用いる。AuroraへのIAM認証の設定方法は、[この記事](https://qiita.com/neruneruo/items/359faa76c2c8990439f2)を参考にしていただきたい。

# ファイル構成
今回は、WebサーバのフレームワークからAuroraに接続するサンプルとして、Ginを用いる。
以下のようなファイル構成とする。

```
.
├── Dockerfile
├── docker-compose.yml
└── src
    ├── db
    │   ├── go.mod
    │   └── main.go
    ├── go.mod
    └── main.go
```

# テーブル構成
Auroraのテーブルとユーザは、以下のように定義している前提とする。

```SQL
CREATE USER app_user IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'; 
GRANT SELECT ON COMPANY.* TO app_user;

CREATE TABLE EMPLOYEE (
  id CHAR(5) PRIMARY KEY,
  name CHAR(20) NOT NULL,
  age INTEGER
);
```

また、データベース名は```COMPANY```で設定している前提として以降のコードを読んでいただきたい。

# Golangのソースコード
## mainモジュール
mainは以下のように定義する。

単に動かすだけなら```healthCheck()```関数は不要だが、NLBに組み込んだりする際は必要なので定義しておく。

```golang:src/main.go
package main

import (
	"errors"
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	_ "github.com/go-sql-driver/mysql"

	"local.packages/db"
)

type employee struct {
	ID   string
	Name string
	Age  int
}

func healthCheck() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	}
}

func employeeGet() gin.HandlerFunc {
	return func(c *gin.Context) {
		result, status, err := getProperty(c.Query("id"))
		if err != nil {
			log.Println(err)
		}

		if status == http.StatusOK {
			c.JSON(status, result)
		} else {
			c.JSON(status, gin.H{})
		}
	}
}

func getProperty(id string) (employee, int, error) {
	var (
		e employee
	)

	db := db.DbConn()
	err := db.QueryRow("select * from EMPLOYEE where id = ?", id).Scan(&e.ID, &e.Name, &e.Age)
	if err != nil {
		log.Println(err)
		return employee{}, 500, errors.New("Query Error.")
	}

	return e, 200, nil
}

func initRouter() *gin.Engine {
	router := gin.Default()

	router.GET("/healthcheck", healthCheck())
	router.GET("/employee", employeeGet())

	return router
}

func main() {
	_, err := db.DbInit()
	if err != nil {
		log.Println("db.DbInit() Error.")
		panic(err)
	}

	router := initRouter()
	router.Run(":8080")
}

```

```src/go.mod
module main

go 1.13

require (
	github.com/aws/aws-sdk-go v1.40.12
	github.com/gin-gonic/gin v1.7.2
	github.com/go-sql-driver/mysql v1.6.0
	local.packages/db v0.0.0-00010101000000-000000000000
)

replace local.packages/db => ./db
```

## dbモジュール
dbモジュールは以下のように定義する

```golang:src/db/main.go
package db

import (
	"database/sql"
	"fmt"
	"log"

	_ "github.com/go-sql-driver/mysql"

	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/rds/rdsutils"
)

var db *sql.DB

func DbInit() (*sql.DB, error) {
	var (
		err error
	)

	dbName := "COMPANY"
	dbUser := "app_user"
	dbHost := "Auroraの接続先DNS名"
	dbPort := 3306
	dbEndpoint := fmt.Sprintf("%s:%d", dbHost, dbPort)
	region := "ap-northeast-1"
	sess := session.Must(session.NewSession())
	creds := sess.Config.Credentials
	authToken, err := rdsutils.BuildAuthToken(dbEndpoint, region, dbUser, creds)
	if err != nil {
		log.Println("rdsutils.BuildAuthToken Error.")
		panic(err)
	}
	dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?tls=true&allowCleartextPasswords=true", dbUser, authToken, dbEndpoint, dbName)
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		log.Println("sql.Open Error.")
		panic(err)
	}
	err = db.Ping()
	if err != nil {
		log.Println("db.Ping Error.")
		panic(err)
	}

	db.SetMaxIdleConns(100)
	db.SetMaxOpenConns(100)
	db.SetConnMaxLifetime(0)

	return db, err
}

func DbClose() {
	if db != nil {
		db.Close()
	}
	log.Println("DB Closed.")
}

func DbConn() *sql.DB {
	return db
}
```

```src/db/go.mod
module db

go 1.14

require (
	github.com/aws/aws-sdk-go v1.40.12
	github.com/go-sql-driver/mysql v1.6.0
)
```

## コネクションプールの設定
前述の通り、Golangでは勝手にコネクションプールを実装してくれているが、デフォルト値では心もとない。

[パッケージのドキュメント](https://pkg.go.dev/database/sql)に書かれている通り、SetMaxIdleConnsのデフォルトは2であるため、接続が不足しても2つまでしかコネクションプールしてくれず、都度接続しにいってしまう。
これを変更しておこう。
※実際、この辺の設定をするとしないとで、スループットが何倍も違った。

# コンテナで動かす場合の設定
[参考記事](https://qiita.com/neruneruo/items/359faa76c2c8990439f2)にも書いた通り、BuildAuthTokenで接続する場合はAWSの証明書が必要になるため、以下のようにビルド時にコンテナに込めるようにする。
コンテナを使わずにローカル起動する場合はもちろんローカルにwgetしておけば良い。

```Dockerfile:Dockerfile
FROM golang:1.14-alpine3.12 as build

ENV GOPATH /go

RUN apk add --update --no-cache git

COPY ./src /go/src
WORKDIR /go/src

RUN GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -o aurora-example-app .

FROM alpine:3.12

RUN mkdir /usr/local/share/ca-certificates
RUN wget https://truststore.pki.rds.amazonaws.com/ap-northeast-1/ap-northeast-1-bundle.pem -P /usr/local/share/ca-certificates
RUN apk add --no-cache ca-certificates && \ 
    update-ca-certificates

RUN mkdir /app
WORKDIR /app
COPY --from=build /go/src/aurora-example-app /app/aurora-example-app

CMD ["/app/aurora-example-app"]
```

また、ローカル起動する場合は、環境変数でクレデンシャルの情報が必要になる。
docker-compose.ymlを以下のようにして起動しよう。

```YAML:docker-compose.yml
version: '3'
services:
  aurora-example-app:
    build: .
    image: aurora-example-app
    container_name: aurora-example-app
    environment:
      - AWS_ACCESS_KEY_ID
      - AWS_SECRET_ACCESS_KEY
```

なお、ECS等のAWSサービス上で起動する際にクレデンシャルを設定するのは当然ながら悪手なので、[参考記事に記載の通り](https://qiita.com/neruneruo/items/359faa76c2c8990439f2#iam%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC)IAMポリシーとMySQLユーザを紐づけてIAM認証をするようにしよう。

これで、Golangでセキュアかつ高速にAuroraにアクセスできるようになった！