BETA

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

投稿日:2020-08-12
最終更新:2020-08-13

概要

この記事ではgolangで書かれたLambdaでRDSに接続するための方法を説明する。
LambdaとRDSを接続するというのはサーバーレスのアンチパターンの代表例のような話であったが、RDS Proxyが発表され、最近公式から以下のような記事も提出されている。
オンラインセミナー「RDS+Lambda が始まる。過去のアンチパターンはどう変わるのか」 資料および QA 公開
LambdaからRDSヘ接続する実装も、これからさらに一般化してくる可能性がある。
今回はGo言語で書かれたLambdaからRDSヘどのように接続し、どのようにデータを取得するかの方法を記述する。

前提

  • serverless-frameworkを使用する
  • 接続先のRDBMSはMySQLとし、DBへ接続するドライバとしてdo-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:[email protected](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カラムを取得することとする。
クエリを発行し、予め定義していた構造体に合わせてデータを取得する。

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:[email protected](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"  
}  

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

最後に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)  
}  

おしまい

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@editechの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう