20200720のGoに関する記事は2件です。

祝GA‼︎【Go】Lambda + RDS 接続にRDS Proxyを使ってみた

はじめに

2019年末に行われたre:Invent 2019で発表されたRDS Proxyが先日正式リリースになりました!API Gateway + Lambda + RDSを使ってWebアプリケーションを作っていて、そこでRDS Proxyを使ってみたので、備忘録です。

RDS Proxyって何?

公式サイトによると、次のように説明されています。

Amazon RDS プロキシは、Amazon Relational Database Service (RDS) 向けの高可用性フルマネージド型データベースプロキシで、アプリケーションのスケーラビリティやデータベース障害に対する回復力と安全性を高めます。

詳細は公式サイトをご確認ください!

RDS Proxyで何をするのか

RDS Proxyでデータベースへのコネクションプールを確立、管理することで、アプリケーションからのデータベース接続数を少なく抑えることができるというので、今回使ってみました!

Lambda関数は、呼び出すごとに新しいコネクションを作成する必要があります。しかし、LambdaからRDSへの同時接続数には上限があり、これまではコネクション数が上限に達しないようにする必要がありました。これを解決してくれるのがこのRDS Proxyです。RDS Proxyを利用することで、既存のコネクションを再利用することができ、コネクション数を抑えることができます。

つまり、Lambda + RDSの構成が避けられていた原因の1つの同時接続数問題がRDS Proxyで解決できるのです!

料金

料金は有効になっているデータベースインスタンスの vCPU あたり0.018USD/時間(東京リージョン)となります。ただし、最低料金として、2つのvCPU分の料金がかかるので、1つのvCPUの場合でも0.018 × 2 = 0.036USD/時間かかります。
参考:https://aws.amazon.com/jp/rds/proxy/pricing/

結論

とても記事が長くなってしまったので、先に結論をまとめます。

  • RDS Proxyを介してRDSに接続できることが確認できた
  • VPC内にLambdaを設置したときのコールドスタートが改善されていることがわかった
    → IAM認証接続ではパブリックサブネットに置いていたRDSをプライベートサブネットに配置できるようになった
  • VPCLambda + RDSで構築していたものはエンドポイントをRDS Proxyに向けるだけでOK(コードの修正が不要)



それでは、本題に入っていきます!

構成図

このような構成で作成しました!本記事では、Lambda、RDS Proxy、RDS、踏み台のEC2にフォーカスしています。
RDS Proxy構成図.png

手順

下記の流れで進めていきます。

  1. VPC、サブネットの作成
  2. セキュリティグループの作成
  3. RDSの構築
  4. 踏み台EC2の構築
  5. テーブル作成
  6. DBユーザの作成
  7. RDS Proxyの構築
  8. Lambda関数の作成

やってみる

1. VPC、サブネットの作成

事前準備として、VPCを作成し、作成したVPCの中にプライベートサブネット、パブリックサブネットを作成します。特別な設定は不要なので作成方法は省略します。

2. セキュリティグループの作成

事前準備として、各リソースのセキュリティグループの作成を行います。

Lambda

セキュリティグループ name : sg-lambda
インバウンド 

タイプ ポート ソース

特にどこからも許可していなくてもAPI Gatewayからは叩くことができます!

EC2

セキュリティグループ name : sg-ec2-bastion
インバウンド

タイプ ポート ソース
SSH 22 許可したいIPアドレス

RDS Proxy

セキュリティグループ name : sg-rdsproxy
インバウンド

タイプ ポート ソース
MySQL/Aurora 3306 sg-lambda

RDS

セキュリティグループ name : sg-rds
インバウンド

タイプ ポート ソース
MySQL/Aurora 3306 sg-ec2-bastion
MySQL/Aurora 3306 sg-rdsproxy

3. RDSの構築

プライベートサブネットにRDSを立てます。
今回使用したMySQLのバージョン : MySQL 5.7.22
セキュリティグループは2で作成したsg-rdsを選択してください。

その他、特に特別な設定は不要なので省略します。

4. 踏み台EC2の構築

RDSに接続して、ユーザやテーブルを作成するための踏み台EC2をたてます。
今回使用したOS : Amazon Linux 2
セキュリティグループは2で作成したsg-ec2-bastionを選択してください。

こちらも、特に特別な設定は不要なので省略します。
ただ、IPv4パブリックIPを使ってsshで接続するため、自動割り当てパブリックIPは有効に設定します。(これをせずに、プライベートIPで接続を試みましたが、できませんでした。)

※上記の方法(自動割り当てパブリックIP)では、EC2を再起動するごとにパブリックIPアドレスが変更になるのでお気をつけてください。

スクリーンショット 2020-03-11 19.38.45.png

5. テーブル作成

RDS内にテーブルを作成します。
テーブル内のデータはcsvファイルから取り込むようにしたかったので、今回はcsvファイルを準備しましたが、ただデータをRDSに保存するだけです。

csvファイルのアップロード

ローカルから踏み台のEC2にcsvファイルを移動させました。

$ scp -i [キーペア名].pem [ファイル名].csv ec2-user@[パブリックIP]:/home/ec2-user/(EC2内の保存したいディレクトリを指定)

EC2にssh接続

$ ssh -i [キーペア名].pem ec2-user@[IPv4パブリックIP]

MySQLのインストール

$ sudo yum update
$ sudo yum install mysql

RDSへ接続

$ mysql --local-infile=1 -h [RDSエンドポイント] -u [マスタユーザ名] -p

テーブル内のデータはcsvファイルから読み込むために、--local-infile=1のオプションをつけました。

テーブルの作成

まず、テーブルの枠を作成します。

> CREATE TABLE [テーブル名] ([カラムの指定]);

# 例
> CREATE TABLE m1_champion (id INT(2) AUTO_INCREMENT NOT NULL PRIMARY KEY, name VARCHAR(30) NOT NULL, champion YEAR NOT NULL, formed YEAR NOT NULL, note VARCHAR(30));

次に、下記のようなcsvファイルをインポートします。

スクリーンショット 2020-03-12 18.23.55.png

> LOAD DATA LOCAL INFILE "[ファイルパス]/[ファイル名].csv" INTO TABLE [テーブル名] FIELDS TERMINATED BY ',' LINES TERMINATED BY '\r\n';

これでテーブルが完成しました。

id name champion formed note
1 ミルクボーイ 2019 2007 コーンフレーク
2 霜降り明星 2018 2013 NULL
3 とろサーモン 2017 2002 NULL

6. DBユーザの作成

上記の手順通り進めば、現在DBにログインしているので、このままLambdaから接続したときに使うユーザを作成します。

> CREATE USER '[ユーザ名]'@'%' IDENTIFIED BY '[パスワード]';
> GRANT SELECT, INSERT, UPDATE, DELETE ON [対象のDB].[対象のテーブル] TO '[ユーザー名]'@'%';

上記はSELECT, INSERT, UPDATE, DELETEの権限を許可しています。また、ホスト名には%=ワイルドカードを使用し、どこからのアクセスも受け入れるように設定しています。ちなみに、全てのDBとテーブルが対象の場合は*.*とします。

7. RDS Proxyの構築

いよいよRDS Proxyの構築に入ります。

Secrets Manager シークレットの作成

先ほど作成したDBのユーザ名とパスワードを入力し、作成します。
下記URLのAWSの公式ブログに沿って作成してください。
参考:AWS LambdaでAmazon RDS Proxyを使用する

IAMロールの作成

ユースケースはRDS - Add Role to Databaseを選択します。

スクリーンショット 2020-03-11 19.32.53.png

そして、必要なポリシーをアタッチします。

Secrets Managerへのアクセス権限のポリシー
{
    "Version": "2012-10-17",
    "Statement": [

        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetResourcePolicy",
                "secretsmanager:GetSecretValue",
                "secretsmanager:DescribeSecret",
                "secretsmanager:ListSecretVersionIds"
            ],
            "Resource": "[シークレットARN]"
        }
    ]
}

拡張ログを取得したい場合はCloud Watch Logsへのアクセス権限も必要です。ただ、以下のログに関しては、Cloud Watch Logsへのアクセス権限がなくてもロググループに出力されます。

  • RDS Proxyの起動終了
  • DBへの接続開始終了
  • 警告

RDS Proxyの作成

今回はRDS Proxyのコンソールから作成します。

ちなみに、Lambda関数のコンソール上からも作成でき、IAM認証で接続する場合はLambdaと紐付ける手間を省くことができます。ただ、今回のようにDBのユーザ名とパスワードを用いて接続する場合は紐付けは必要ないようなので、RDS Proxyのコンソールから作成しました。

スクリーンショット 2020-07-05 18.53.25.png

プロキシ識別子(名前)を入力します。
エンジンの互換性ではMySQLとPostgreSQLが選べるようになっていました。ここでプロキシが接続できるDBのタイプを設定します。今回はMySQLなので、MySQLを選択しました。

スクリーンショット 2020-07-05 19.04.08.png

先ほど作成したRDSを選択します。

スクリーンショット 2020-03-16 12.25.36.png

先ほど作成したSecrets ManagerシークレットとIAMロールを選択します。
サブネットはRDSと同じプライベートサブネットを選択します。
セキュリティグループは2で作成したsg-rdsproxyを選択してください。

スクリーンショット 2020-07-05 19.09.34.png

Cloud Watch Logsでデバッグログを取得したい場合は拡張されたログ記録を有効にするにチェックを入れてください。(Cloud Watch Logsへのアクセス権限も必要です。)

8. Lambda関数の作成

IAMロール

LambdaはVPC内にあるのでENI生成用のポリシーを作成してアタッチします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface",
                "ec2:CreateNetworkInterface"
            ],
            "Resource": "*"
        }
    ]
}

ログを取得したい場合はCloud Watch Logsへのアクセス権限も必要です。

VPC

Lambda関数の編集画面で設定できます。

スクリーンショット 2020-03-16 12.26.00.png

カスタムVPCを選択し、RDSとRDS Proxyと同様のVPCとパブリックサブネットを選択します。
セキュリティグループは2で作成したsg-lambdaを選択してください。

ソースコード

package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    "github.com/aws/aws-lambda-go/lambda"
    _ "github.com/go-sql-driver/mysql"
    "os"
)

type Response struct {
    ID       int            `json:"id"`
    Name     string         `json:"name"`
    Champion string         `json:"champion"`
    Formed   string         `json:"formed"`
    Note     sql.NullString `json:"note"`
}

// os.Getenv()でLambdaの環境変数を取得
var dbUser = os.Getenv("dbUser")         // DBに作成したユーザ名
var dbPass = os.Getenv("dbPass")         // パスワード
var dbEndpoint = os.Getenv("dbEndpoint") // RDS Proxyのプロキシエンドポイント
var dbName = os.Getenv("dbName")         // テーブルを作ったDB名

func RDSConnect() (*sql.DB, error) {
    connectStr := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?charset=%s",
        dbUser,
        dbPass,
        dbEndpoint,
        "3306",
        dbName,
        "utf8",
    )
    db, err := sql.Open("mysql", connectStr)
    if err != nil {
        panic(err.Error())
    }
    return db, nil
}

func RDSProcessing(db *sql.DB) (interface{}, error) {

    var id int
    var name string
    var champion string
    var formed string
    var note sql.NullString

    responses := []Response{}
    responseMap := Response{}

    getData, err := db.Query("SELECT * FROM m1_champion")
    defer getData.Close()
    if err != nil {
        return nil, err
    }

    for getData.Next() {
        if err := getData.Scan(&id, &name, &champion, &formed, &note); err != nil {
            return nil, err
        }
        fmt.Println(id, name, champion, formed, note)
        responseMap.ID = id
        responseMap.Name = name
        responseMap.Champion = champion
        responseMap.Formed = formed
        responseMap.Note = note
        responses = append(responses, responseMap)
    }

    params, _ := json.Marshal(responses)
    fmt.Println(string(params))

    defer db.Close()
    return string(params), nil
}

func run() (interface{}, error) {
    fmt.Println("RDS接続 start!")
    db, err := RDSConnect()
    if err != nil {
        panic(err.Error())
    }
    fmt.Println("RDS接続 end!")
    fmt.Println("RDS処理 start!")
    response, err := RDSProcessing(db)
    if err != nil {
        panic(err.Error())
    }
    fmt.Println("RDS処理 end!")
    return response, nil
}

/**************************
   メイン
**************************/
func main() {
    lambda.Start(run)
}

実行結果

上記の手順でRDS Proxyを用いての接続は完了です。(あれ意外と簡単)
Lambdaでの実行結果がこちらです。

スクリーンショット 2020-07-05 19.38.47.png

RDSから値が取得できました!

びっくりするのは応答時間!!!コールドスタートで接続に10秒程度かかるのでご法度とされていたVPCLambdaですが、こんなにはやくなっているんです。

参考:[発表] Lambda 関数が VPC 環境で改善されます

これは使わない手はない!

おわりに

無事、LambdaからRDS Proxyを介してRDSに接続が可能になりました!これで同時接続数問題も気にしなくていい!!また、VPC内にLambdaを設置したときのコールドスタートが改善されたので、Lambdaを非VPCに設置しなくてもいけるし、RDSがプライベートサブネットに置ける!!素晴らしい!

また、RDSのセキュリティグループにLambdaのセキュリティグループを付加し、コード中のエンドポイントの向きをRDS ProxyからRDSに変更するだけで、このコードのままRDSに接続できることが確認できました。つまり、Lambda + RDSで構築していたものは簡単にRDS Proxyを経由することができるんです!
わたしはこれまでIAM認証でのLambda + RDS接続しか経験がなく、コードの実装に手間取ってしまいましたが、上手く接続できないときの問題の切り分け方などとても勉強になりました!

GAになったので、これから活用できる場所が増えるのではないかなと思います!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerとGoで簡単にapiサーバーを立ててみる

はじめる前に

こんにちはRIN1208です
この記事はDockerとGoを少し触れている程度の学生が書いていますので分からない点、間違っている点等がございましたらコメントして頂けると幸いです。
今回の書いたDockerfileなどはこちらに置いてあります。

この記事の対象

  • Dockerについてなんとなく知っている人
  • Goを知っている人
  • とりあえず両方触ってみたい人

この記事で触れない事

  • Dockerがどのように動いているか
  • Dockerの環境構築について
  • Docker Hubの詳細について
  • Dockerとは何か
  • Goのコードの詳細について
  • マルチステージビルド

開発環境

  • macOS Catalina
  • Go(ver 1.14.4)こちらはなくても問題ありません
  • Docker(後半でDocker-composeも使用します)
  • VSCode(お好みのエディターを使用して下さい) 上記の環境で説明していきます

さわってみる

今回やる事

  • コンテナの起動などの基本動作
  • Dockerfileを書く
  • Goで簡単なWebサーバーを立てる

今回触れない事

  • Docker Networkを使用したコンテナ間通信

Nginxをdockerで起動してみる

下記のコマンドを実行してみましょう

docker run --name hoge -d -p 8080:80 nginx

起動後にhttp://localhost:8080/に接続するとWelcome to nginx!と表示されます。

今入力したコマンドの説明

docker run: コンテナを起動するコマンドです
--name: 起動するコンテナに名前をつけられます。今回はhogeという名前です
-d: このオプションを使用するとバックグラウンドで起動することができます
-p: ポートのオプションですホストのポート:コンテナのポートで指定できます。今回はホストが8080、コンテナが80で起動しています。

起動したコンテナの確認

下記のコマンドを実行してみましょう

docker ps

すると以下のように表示されると思います

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
63940a31eae8        nginx               "/docker-entrypoint.…"   3 seconds ago       Up 2 seconds        0.0.0.0:8080->80/tcp   hoge

表示項目の説明

CONTAINER ID: コンテナのIDです。コンテナを指定する際に使用します
IMAGE: 使用したDockerimageです。今回はnginxを使用したのでnginxになっています
COMMAND: コンテナが起動する際に与らたコマンドです
CREATED: コンテナを作成してからの時間です
STATUS: 起動、停止してからの経過時間です
PORTS: コンテナ及びホストの使用ポート
NAMES: コンテナの名前です。こちらの名前を使用してコンテナを指定することもできます

起動したコンテナの停止

下記のコマンドを実行してみましょう

docker stop hoge

実行するとコンテナが停止しました。docker psで確認してみましょう

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

とだけ表示されます。こちらの項目に先ほど確認したコンテナが存在しなければコンテナの停止ができています。
またdocker psコマンドは起動しているコンテナを確認するコマンドなので他にも起動しているコンテナがある場合は表示されます

Dockerfileを書いてコンテナを作成する

次はDockerfileを書いてGoのapiサーバーを立ち上げてみましょう。
今回はGoのwebフレームワークであるGinを使用します。

今回触れない事

  • マルチステージビルド
  • Go Modules

Dockerfileとmain.goを作成する

適当な同じディレクトリに内にDockerfileとmain.goを作成しましょう。

main.go
package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/", hoge)
    r.Run() // デフォルトが8080ポートなので今回は変更しません
}

func hoge(c *gin.Context) {
    c.JSON(200, "hogeeeeeeee")
}

dockerfile

FROM golang:1.14

RUN  go get -u github.com/gin-gonic/gin

WORKDIR /app 
COPY . /app

ENV CGO_ENABLED=0 \
    GOOS=linux \
    GOARCH=amd64

EXPOSE 8080

CMD ["go", "run", "main.go"]

設定項目の説明

FROM : 実行する環境を指定します。DockerHubにあるimageをしますので使用する際はDockerHubにあるかどうか確認してください。バージョン指定がない場合は最新バージョン(latest)が使用されます。

RUN : コマンドを実行します

WORKDIR : 作業ディレクトリを設定します

COPY : コンテナにディレクトリ、ファイルるをコピーします。

ENV : コンテナに永続的な環境変数を定義します。コピーする対象 コンテナ内のコピー先で指定できます

EXPOSE : コンテナの使用ポートを指定します

CMD : 実行するコンテナにコマンドと引数を提供します

Dockerfileからimageを作成し起動する。

以下のコマンドでbuildしimageを作成します。Dockerfileのあるディレクトリ内で実行しましょう。
今回はmy-imageという名前のimageを作成し起動します。

docker build -t my-image .

buildが完了しましたら下記のコマンドを実行しREPOSITORYの項目にmy-imageという名前があるのを確認しましょう。

docker images

下記のようにREPOSITORYの項目にmy-imageとあれば問題ありません。

REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
my-image                         latest              b498e18e4087        6 hours ago         956MB

確認することができましたら以下のコマンドでコンテナを起動させましょう。
今回は先ほど作成したimageのmy-imageを使用しmy-apiという名前のコンテナを作成しています。

docker run -it --name my-api  my-image 

今回は-dオプションをつけていないのでバックグラウンドで動作させていませんが-dオプションを指定すればバックグラウンドで動かすことができます。

docker run  -d -it --name my-api  my-image

上記のコマンドでapiサーバーが起動できたのを確認できます。
今回はhttp://localhost:8080/にて立ち上がっています。

docker-composeを使ってみる

次は先ほど立ち上げたコンテナをdocker-composeで立ち上げます。
Mysql等との接続は記事が長くなるのに加え、少しややこしいので今回は触りません。

docker-compose.yml

version: "3" 
services:
  api: # コンテナの名前
    build: . # dockerfileの場所を指定できます。
    ports:
      - "8080:8080"
    tty: true
コンテナのビルド

下記のコマンドでビルドします。
ymlファイルを書き換えた際はbuildしないと反映されません。ちょくちょく忘れてやらかします

docker-compose build
コンテナの起動

下記のコマンドでコンテナを立ち上げます。 今回は -d オプションを使用しバックグラウンドにて立ち上げています

docker-compose up -d 

起動できましたらhttp://localhost:8080/にて確認してみましょう。

起動したコンテナの確認

立ち上がっているコンテナは下記のコマンドにて確認することができます。

docker-compose ps

すると以下のように表示されると思います。

      Name             Command       State           Ports
-------------------------------------------------------------------
docker-test_api_1   go run main.go   Up      0.0.0.0:8080->8080/tcp

Name : コンテナの名前
Command : コンテナが起動する際に与らたコマンドです
State : コンテナのステータスです
Ports : コンテナ及びホストの使用ポートです

起動したコンテナの停止

下記のコマンドでコンテナを停止させることができます。

docker-compose down

終わりに

ここまで読んでくださりありがとうございます。
この記事は友人のDockerの勉強のためになればと思い書きました。また初めてDocker触る方の手助けになれば幸いです。
間違っている点などがございましたらコメントなどで指摘していただけると助かります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む