LoginSignup
2
0

More than 1 year has passed since last update.

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

Posted at

はじめに

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

なお、Auroraへのアクセス方法については、公式のユーザーガイドが参考になる。

また、接続にはIAM認証を用いる。AuroraへのIAM認証の設定方法は、この記事を参考にしていただきたい。

ファイル構成

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

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

テーブル構成

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

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に組み込んだりする際は必要なので定義しておく。

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モジュールは以下のように定義する

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では勝手にコネクションプールを実装してくれているが、デフォルト値では心もとない。

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

コンテナで動かす場合の設定

参考記事にも書いた通り、BuildAuthTokenで接続する場合はAWSの証明書が必要になるため、以下のようにビルド時にコンテナに込めるようにする。
コンテナを使わずにローカル起動する場合はもちろんローカルにwgetしておけば良い。

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を以下のようにして起動しよう。

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サービス上で起動する際にクレデンシャルを設定するのは当然ながら悪手なので、参考記事に記載の通りIAMポリシーとMySQLユーザを紐づけてIAM認証をするようにしよう。

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0