見出し画像

Golangで書かれたAWS LambdaでRDS(MySQL)へ接続しレコードを取得・操作する方法

この記事ではgolangで書かれたLambdaでRDSに接続するための方法を説明する。

LambdaとRDSを接続するというのはサーバーレスのアンチパターンの代表例のような話であったが、RDS Proxyが発表され、最近公式から以下のような記事も提出されている。

LambdaからRDSヘ接続する実装も、これからさらに一般化してくる可能性がある。
今回はGo言語で書かれたLambdaからRDSヘどのように接続し、どのようにデータを取得するかの方法を記述する。

前提

・serverless-frameworkを使用する
・接続先のRDBMSはMySQLとし、DBへ接続するドライバとして[do-sql-driver/mysql](https://github.com/go-sql-driver/mysql)を使用する。
・接続先のRDS及びRDSとLambdaが所属するVPC・Subnetなどは既に作成済みのものとする。

結論

下記ファイルでRDSに接続+データのクエリ+golangの構造体として取得までができるLambdaが生成される。

serverless.yml

service: go-connect-rds
frameworkVersion: '>=1.28.0 <2.0.0'
provider:
 name: aws
 runtime: go1.x
 stage: dev
 region: ap-northeast-1
 iamRoleStatements:
   - Effect: "Allow"
     Action:
       - "rds:Describe*"
       - "rds:ListTagsForResource"
     Resource:
       - "*"
 vpc:
   securityGroupIds:
     - sg-yoursgid
   subnetIds:
     - subnet-yoursubentid1
     - subnet-yoursubnetid2
 environment: ${ssm:/aws/reference/secretsmanager/go-connect-rds-params~true}
package:
 exclude:
   - ./**
 include:
   - ./bin/**
functions:
 go-connect-rds:
   handler: bin/main

main.go(lambda)

package main
import (
	"context"
	"database/sql"
	"fmt"
	"os"
	_ "github.com/go-sql-driver/mysql"
	"github.com/aws/aws-lambda-go/lambda"
)
func Handler(ctx context.Context) {
	DBMS := "mysql"
	USER := os.Getenv("user")
	PASS := os.Getenv("password")
	PROTOCOL := os.Getenv("protocol")
	DBNAME := os.Getenv("dbname")
	CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo"
	conn, err := sql.Open(DBMS,  CONNECT)
	defer conn.Close()
	if err != nil {
		fmt.Println("Fail to connect db" + err.Error())
	}
	// 接続確認
	err = conn.Ping()
	if err != nil {
		fmt.Println("Failed to connect rds : %s", err.Error())
	} else {
		fmt.Println("Success to connect rds")
	}
	// 取得するレコード一行のデータ形式を構造体で定義する
	type UserData struct {
		UserID int
		FirstName string
		LastName string
		Email string
	}
	// DBからレコードを抽出
	rows, err := conn.Query("select user_id, first_name, last_name, email from user;")
	if err != nil {
		fmt.Println("Fail to query from db " + err.Error())
	}
	// データを構造体へ変換
	var UserDatas []UserData
	for rows.Next() {
		var tmpUserData UserData
		err := rows.Scan(&tmpUserData.UserID, &tmpUserData.FirstName, &tmpUserData.LastName, &tmpUserData.Email)
		if err != nil {
			fmt.Println("Fail to scan records " + err.Error())
		}
		UserDatas = append(UserDatas, UserData{
			UserID:    tmpUserData.UserID,
			FirstName: tmpUserData.FirstName,
			LastName:  tmpUserData.LastName,
			Email:     tmpUserData.Email,
		})
	}
	// 確認のための出力
	for _, userData := range UserDatas {
		fmt.Printf("%#v\n", userData)
	}
}
func main() {
	lambda.Start(Handler)
}

開発手順

Step1. セットアップ〜RDS接続まで

以下のような配置でファイルを作成し、Makefileがある階層で`make deploy`を打てば、RDSに接続しにいくLambdaが生成される。
なお、接続情報は以下と仮定する。

Mysqlユーザー名 == user
DataBase名 == dbname
RDSのhost(=エンドポイント) == rdshost
DataBaseパスワード == password

ファイルの配置

# 1.1 以下の形にファイルを配置
go-connect-rds
├── Makefile
├── bin
│   └── main
├── main.go
└── serverless.yml
# 1.2 下記の内容でserverless.ymlとmain.go、Makefileの中身を追記する
# 1.3 go-connect-rds直下にて下記コマンド実行 => AWSコンソールでLambdaが出現していることを確認
$ make deploy

serverless.yml

service: go-connect-rds
frameworkVersion: '>=1.28.0 <2.0.0'
provider:
 name: aws
 runtime: go1.x
 stage: dev
 region: ap-northeast-1
 iamRoleStatements:
   - Effect: "Allow"
     Action:
       - "rds:Describe*"
       - "rds:ListTagsForResource"
     Resource:
       - "*"
 vpc:
   securityGroupIds:
     - sg-yoursgid
   subnetIds:
     - subnet-yoursubentid1
     - subnet-yoursubentid2
package:
 exclude:
   - ./**
 include:
   - ./bin/**
functions:
 go-connect-rds:
   handler: bin/main

Makefile

.PHONY: build clean deploy
build:
	env GOOS=linux go build -ldflags="-s -w" -o bin/main main.go
clean:
	rm -rf ./bin
deploy: clean build
	sls deploy --verbose

main.go(Lambdaの中身)

package main
import (
	"context"
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	//"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)
func Handler(ctx context.Context) {
	// テキストで接続情報を一つなぎに規定し、sql.Openの第二引数に与える
	conn, err := sql.Open("mysql", "user:password@tcp(rdshost:3306)/dbname?charset=utf8&parseTime=true&loc=Asia%2FTokyo")
	defer conn.Close()
	if err != nil {
		fmt.Println(err.Error())
	}
	// 接続確認
	err = conn.Ping()
	if err != nil {
		fmt.Println("Failed to connect rds : %s", err.Error())
	} else {
		fmt.Println("Success to connect rds")
	}
}
func main() {
	lambda.Start(Handler)
}

Step2. テーブルの内容を取得して構造体として格納する

今回は"user"という名前のテーブルがあると仮定し、user, first_name, last_name, emailカラムを取得することとする。
クエリを発行し、予め定義していた構造体に合わせてデータを取得する。

main.go

package main
import (
	"context"
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	//"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)
func Handler(ctx context.Context) {
	conn, err := sql.Open("mysql", "user:password@tcp(rdshost:3306)/dbname?charset=utf8&parseTime=true&loc=Asia%2FTokyo")
	defer conn.Close()
	if err != nil {
		fmt.Println("Fail to connect db" + err.Error())
	}
	// 接続確認
	err = conn.Ping()
	if err != nil {
		fmt.Println("Failed to connect rds : %s", err.Error())
	} else {
		fmt.Println("Success to connect rds")
	}
	// =================================================
	// 【ここから追記】テーブルの内容を取得して構造体として格納
	// =================================================
	// 取得するレコード一行のデータ形式を構造体で定義する
	type UserData struct {
		UserID int
		FirstName string
		LastName string
		Email string
	}
	// DBからレコードを抽出
	rows, err := conn.Query("select user_id, first_name, last_name, email from user")
	if err != nil {
		fmt.Println("Fail to query from db " + err.Error())
	}
	// データを構造体へ変換
	var UserDatas []UserData
	for rows.Next() {
		var tmpUserData UserData
		err := rows.Scan(&tmpUserData.UserID, &tmpUserData.FirstName, &tmpUserData.LastName, &tmpUserData.Email)
		if err != nil {
			fmt.Println("Fail to scan records " + err.Error())
		}
		UserDatas = append(UserDatas, UserData{
			UserID:    tmpUserData.UserID,
			FirstName: tmpUserData.FirstName,
			LastName:  tmpUserData.LastName,
			Email:     tmpUserData.Email,
		})
	}
	// 確認のための出力
	for _, userData := range UserDatas {
		fmt.Printf("%#v\n", userData)
	}
	// =================================================
	// ↑↑↑↑↑【ここまで追記】↑↑↑↑↑↑
	// =================================================
}
func main() {
	lambda.Start(Handler)
}

Step3. 環境変数へ接続情報を隠蔽

このままでは接続情報が剥き出しでセキュリティ上褒められたものではない。AWSで秘匿情報を格納+利用できるサービスであるシークレットマネージャーを利用して、接続情報を隠蔽する。

今回はserverless-frameworkを使用しているため、Deployの際にシークレットマネージャーを参照し、Lambdaの環境変数に埋め込む設計とする。
メリットとしてはデプロイ時のみリードが走るのでシークレットマネージャーの読み取りオーバーヘッドがなくなる点がある。
デメリットとしてはLambdaのコンソール画面が見れる開発者はDBの接続情報が丸わかりであるところ。

# AWS CLIを使って以下のコマンドを実行
$ aws secretsmanager create-secret --name go-connect-rds-params  --description "[Test]This parameter is for lambda go-connect-rds-params"
# すると以下のような表示が返ってくるはず。
{
   "ARN": "arn:aws:secretsmanager:ap-northeast-1:?????????????:secret:go-connect-rds-params-???????",
   "Name": "go-connect-rds-params"
}

シークレットマネージャーのコンソール画面にいき、赤枠のボタンから値を入力する。(一部画面上のデータをマスキングしている)

画像1

画像2

最後にserverless.ymlにシークレットマネージャーから読み取った値を環境変数にセットする記述を追記する。
※1:値のsufixに`~true`をつけることで、暗号化されているパラメータを復号して取得することができる。
※2:指し先は一つのシークレットマネージャーの値であるが、ここに登録している値全てがLambdaの環境変数に全部登録される。

serverless.yml

service: go-connect-rds
frameworkVersion: '>=1.28.0 <2.0.0'
provider:
 name: aws
 runtime: go1.x
 stage: dev
 profile: famm
 region: ap-northeast-1
 iamRoleStatements:
   - Effect: "Allow"
     Action:
       - "rds:Describe*"
       - "rds:ListTagsForResource"
     Resource:
       - "*"
 vpc:
   securityGroupIds:
     - sg-yoursgid
   subnetIds:
     - subnet-yoursubnetid1
     - subnet-yoursubnetid2
 environment: ${ssm:/aws/reference/secretsmanager/go-connect-rds-params~true} # <= ここを追記!!!
package:
 exclude:
   - ./**
 include:
   - ./bin/**
functions:
 go-connect-rds:
   handler: bin/main

次にLambda内のソースを修正し、環境変数から値を読み取るように変更する。

main.go

package main
import (
	"context"
	"database/sql"
	"fmt"
	"os"
	_ "github.com/go-sql-driver/mysql"
	"github.com/aws/aws-lambda-go/lambda"
)
func Handler(ctx context.Context) {

	// =================================================
	// 【ここから追記】接続情報を隠蔽
	// =================================================
	DBMS := "mysql"
	USER := os.Getenv("user")
	PASS := os.Getenv("password")
	PROTOCOL := os.Getenv("protocol")
	DBNAME := os.Getenv("dbname")
	CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo"
	conn, err := sql.Open(DBMS,  CONNECT)
	defer conn.Close()
	// =================================================
	// ↑↑↑↑↑【ここまで追記】↑↑↑↑↑↑
	// =================================================
	if err != nil {
		fmt.Println("Fail to connect db" + err.Error())
	}
	// 接続確認
	err = conn.Ping()
	if err != nil {
		fmt.Println("Failed to connect rds : %s", err.Error())
	} else {
		fmt.Println("Success to connect rds")
	}
	// 取得するレコード一行のデータ形式を構造体で定義する
	type UserData struct {
		UserID int
		FirstName string
		LastName string
		Email string
	}
	// DBからレコードを抽出
	rows, err := conn.Query("select user_id, first_name, last_name, email from user;")
	if err != nil {
		fmt.Println("Fail to query from db " + err.Error())
	}
	// データを構造体へ変換
	var UserDatas []UserData
	for rows.Next() {
		var tmpUserData UserData
		err := rows.Scan(&tmpUserData.UserID, &tmpUserData.FirstName, &tmpUserData.LastName, &tmpUserData.Email)
		if err != nil {
			fmt.Println("Fail to scan records " + err.Error())
		}
		UserDatas = append(UserDatas, UserData{
			UserID:    tmpUserData.UserID,
			FirstName: tmpUserData.FirstName,
			LastName:  tmpUserData.LastName,
			Email:     tmpUserData.Email,
		})
	}
	// 確認のための出力
	for _, userData := range UserDatas {
		fmt.Printf("%#v\n", userData)
	}
}
func main() {
	lambda.Start(Handler)
}

おしまい

この記事が気に入ったらサポートをしてみませんか?