ちょっと話題の記事

「ALB + Lambda + Aurora Serverless」でWebサイトを作ってみよう #reinvent

ALBのターゲットにLambdaを指定できるようになりました。いっそのことAurora Serverlessと組み合わせてサーバーレスWebサイトを作ってしまえ!という内容です。遅いんじゃないの〜とか気になってる人はちょっと開いてみませんか?
2018.12.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、菅野です。
re:Invent で発表されたアップデートにより ALB の ターゲットとして Lambda が指定できるようになりました。
今回はこの機能を使ったサーバレス Web サイトを作ってみようと思います。

構成

構成図は以下となります。
ご覧の通り静的なものは S3 から、それ以外は ALB 経由の Lambda が応答を返します。

構成について

共通

  • 各 AWS リソースにはデフォルトのセキュリティグループを付与する

VPC

  • プライベートサブネットとパブリックサブネットを 2 AZ 分用意する

ALB

  • パブリックサブネットに作成する
  • ターゲットは Lambda

Aurora Serverless(以降 DB)

  • プライベートサブネットに作成する
  • デフォルトセキュリティグループからの接続を許可する
  • データの登録用 Lambda 関数は今回作成しないので 作業用 EC2 からデータを用意する

Lambda

  • DB へ接続するために VPC 内のプライベートサブネットで起動させる
  • DB への接続に必要な情報はパラメータストアに置く
  • パブリックネットワークへ接続できないので、パラメータストア接続用の VPC エンドポイントを用意する
  • DB への接続は「PyMySQL」を利用する

S3

  • CSSファイルと画像ファイル(静的なもの)はここに置く
  • CloudFront 以外の外部からの GetObject は許可しない

VPC 関連の作成

  • VPC の CIDR は 10.0.0.0/16
  • サブネットは以下のように作成(2AZ分)

  • インターネットゲートウェイ(以降 IGW)を作成

  • パブリックサブネット用のルートテーブルを作成
    • このルートテーブルには以下のルートを追加
      • Destination:0.0.0.0/0
      • Target:先ほど作成した IGW の ID
    • パブリックサブネットのルートテーブルをこれに変更
  • エンドポイントを以下の条件で作成
    • サービスカテゴリ:AWS サービス
    • サービス名:com.amazonaws.ap-northeast-1.ssm
    • サブネット:先ほど作成した 2AZ のプライベートサブネットを指定

Aurora Serverless の作成

サブネットグループを用意

  • 指定するのは作成した 2AZ のプライベートサブネットを指定

セキュリティグループ

  • デフォルトセキュリティグループを持つ AWS リソースから 3306 ポートへの接続を許可

Aurora Serverless を作成

  • Aurora Serverless を使う場合、エディションは「MySQL 5.6 との互換性」を選択

  • エディションの選択が合っていれば「Capacity Type」で Serverless が選択可能

  • 先ほど作成したサブネットグループとセキュリティグループを指定します

    • セキュリティグループは以下の2個アタッチ
      • 先ほど作成したセキュリティグループ
      • デフォルトセキュリティグループ

データを用意

  • データを登録するために、作業用 EC2 インスタンスを作成します
  • 作成するのはパブリックサブネットです
  • EC2 インスタンスには以下のセキュリティグループをアタッチします
    • デフォルトセキュリティグループ
    • SSH を許可するセキュリティグループ
  • MySQL クライアントのインストール
    • 今回は AmazonLinux2 で用意しましたので、以下のコマンドでインストールされるのは「mariadb」になります
sudo yum install mysql
  • データ作成に必要なコマンドは以下となります
    • DB へ接続
    • database の作成
    • database の文字コードの設定
    • テーブルの作成
    • テーブルの文字コードの設定
    • データの登録
$ mysql -h aurora-serverless.cluster-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -u ユーザー名 -p
MySQL [(none)]> create database serverless;
MySQL [(none)]> alter database serverless default character set utf8;
MySQL [(none)]> use serverless;
MySQL [serverless]> create table schedule (
    -> id INT NOT NULL AUTO_INCREMENT,
    -> event_title char(200) NOT NULL,
    -> url char(200) NOT NULL,
    -> PRIMARY KEY (id)
    -> );
MySQL [serverless]> alter table schedule convert to character set utf8;
MySQL [serverless]> insert into schedule (event_title, url) values
    -> ("re:Growth 2018 東京", "https://dev.classmethod.jp/news/181205-re-growth/"),
    -> ("re:Growth 2018 大阪", "https://classmethod.connpass.com/event/109623/"),
    -> ("re:Growth 2018 福岡", "https://classmethod.connpass.com/event/109895/");

Lambda が DB へ接続するためのパラメータの作成

  • 以下の3つを SecureString で作成します(名前は自由)
    • db-endpoint-serverless・・・DB の「Database endpoint」を入力
    • username-serverless・・・DB 作成時に指定したユーザー名
    • username-serverless・・・DB 作成時に指定したパスワード

Lambda 関数の作成

ローカルでファイルとライブラリを用意する

  • Lambda 関数のメインファイルを「lambda_function.py」という名前で作成します
  • 内容は以下
from __future__ import print_function
import sys
import boto3
import pymysql
import textwrap
import time

# 開始時間を保存
start_time = time.time()
# 経過時間記録用テキストファイルを初期化
time_text = ''

# 経過時間をテキストに記録する
def show_time( message ):
    global time_text
    # 現在時刻を取得
    now_time = time.time()
    # 開始時間からの差分を取得
    interval = str( now_time - start_time )
    # 差分を記録
    time_text += '<div class="left">{}秒:{}</div>\n'.format( interval, message )  

# パラメータ取得準備
ssm = boto3.client( 'ssm' )

# パラメータを格納する配列を準備
db_params = {}

# パラメータの名前から復号化したパラメータを取得
ssm_response = ssm.get_parameters(
    Names = [
        'db-endpoint-serverless',
        'username-serverless',
        'password-serverless'
    ],
    WithDecryption = True
)

# 復号化したパラメータを配列に格納
for tmp_param in ssm_response[ 'Parameters' ]:
    db_params[ tmp_param[ 'Name' ] ] = tmp_param[ 'Value' ]

# 時間を記録
show_time( 'DB接続パラメータ取得' )

# パフォーマンス向上のためにハンドラー外で DB へ接続
try:
    connection = pymysql.connect(
        db_params['db-endpoint-serverless'],
        user=db_params['username-serverless'],
        passwd=db_params['password-serverless'],
        db="serverless",
        charset='utf8mb4',
        cursorclass=pymysql.cursors.DictCursor,
        connect_timeout=5
    )
except:
    print( 'データベースへの接続に失敗')
    sys.exit( 'ERROR' )

# 時間を記録
show_time( 'DB接続完了' )

def lambda_handler(event, context):
    global time_text

    # データを取得するためのカーソルを用意
    cursor = connection.cursor()
    # SQL 文を作成
    sql = 'select id, event_title, url from schedule order by id'
    # SQL 文を実行
    cursor.execute( sql )
    # 全データを辞書型で取得
    sql_result = cursor.fetchall()

    # 時間を記録
    show_time( 'データ取得完了' )

    tmp_body = ''
    # 1レコードずつ取得
    for row in sql_result:
        tmp_body += '<div class="event"><a href="{0[url]}" target="_blank">{0[event_title]}</a></div>\n'.format( row )

    # http レスポンスを準備
    response = {
        "statusCode": 200,
        "statusDescription": "200 OK",
        "isBase64Encoded": False,
        "headers": {
            "Content-Type": "text/html; charset=utf-8"
        }
    }

    # body を作成
    response[ 'body' ] = textwrap.dedent( '''\
    <html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>re:Growth</title>
        <link rel="stylesheet" type="text/css" href="/css/index.css">
    </head>
       <body>
          <p><img src="/img/bnr_regrowth.jpg"></p>
          ''' + tmp_body + '<div class="left">経過時間</div>{}'.format( time_text ) + '''\
      </body>
    </html>
    ''')

    time_text = ''
    return response
  • lambda_function.py と同じディレクトリで以下のコマンドを実行します
pip install PyMySQL -t .
zip -r ../lambda-serverless.zip *

IAM ポリシーを用意する

  • Lambda 関数がパラメータストアを利用できるように以下の内容で作成します
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole",
                "ssm:GetParameters"
            ],
            "Resource": "*"
        }
    ]
}

IAM ロールを用意する

  • Lambda 関数の実行ロールを作成し、以下のポリシーを追加します
    • 先ほど作った IAM ポリシー
      • DB への接続に必要な情報をパラメータストアから取得するのに必要
    • AWSLambdaVPCAccessExecutionRole
      • VPC 内で Lambda 関数を実行するのに必要

セキュリティグループを用意する

  • デフォルトセキュリティグループを持つ AWS リソースから 80 ポートへの接続を許可

Lambda 関数を作成する

  • 以下のように作成します   

  • 先ほどローカルで作成した「lambda-serverless.zip」をアップロードします   

  • ネットワークの設定をします

    • VPCを選択
    • 2AZ のプライベートサブネット
    • セキュリティグループを指定(2個)
      • 先ほど作成したセキュリティグループ
      • デフォルトセキュリティグループ   
  • タイムアウトは15秒くらいにしておきましょう

ALB の作成

セキュリティグループを用意する

  • 0.0.0.0/0 からポート 80 への接続を許可するよう作成

ターゲットグループを用意する

  • やっと今回の目玉まで辿り着きました。
  • ターゲットの種類:Lambda 関数
  • Lambda 関数:先ほど作成した Lambda 関数
  • ヘルスチェック:付けたら無駄な利用料が・・・   

ALB を作成する

  • スキーム:インターネット向けを選択
  • リスナー:テストなので HTTP で
  • VPC:最初に作成した VPCを選択
  • アベイラビリティーゾーン:先ほど作成した 2AZ のパブリックサブネットを指定
  • セキュリティグループ:先ほど用意したセキュリティグループを指定
  • ターゲットグループ:先ほど用意したターゲットグループを指定

静的ファイルの作成

  • S3バケットを作成して以下のファイルを配置してください
  • css/index.css
html, body {
    margin: 0;
    padding: 0;
    text-align: center;
}

.left {
    text-align: left;
}
  • img/bnr_regrowth.jpg
    • ここからもらってきました

Cloud Front の作成

  • マルチオリジン(ALBとS3の二つをオリジンとする)の Cloud Front の作成方法はこちらのバイブルを参考にしてください
  • 今回特有の設定としては以下があります
    • ALB へ振り分けるアクセスはキャッシュさせないため、以下のように「All」を選択しました。Lambda 関数の変更をすぐに反映させるためです。
    • 本番運用ではこの部分の設定を検討してください   

完成

完成しました。さっそくアクセスしてみます。
  

処理時間について

左下にLambda関数の実行開始から各処理が終わるまでの経過時間を表示させています。
処理完了まで1秒以内というのは、想像していたよりもいい結果でした。

  • 0.8180656433105469秒:DB接続パラメータ取得
  • 0.9344112873077393秒:DB接続完了
  • 0.9461565017700195秒:データ取得完了

ちなみに、2回目以降は「データ取得完了」の行しか表示されませんし、経過時間がすごい時間になっています。

  • 177.42943167686462秒:データ取得完了

Lambda 関数のソースを見ていただけるとわかるのですが、以下の処理はハンドラー(メイン処理)の外に書いてあります。

  • パラメータストアから DB への接続に必要な情報を取得
  • DB へ接続

こうすることで2回目以降のアクセスは DB への接続が不要となり、ハンドラーの内部の処理だけとなりますのでパフォーマンスが向上するのです。
私が何度か試した中で、一番いい結果は以下でした。

  • 0.16818475723266602秒:DB接続パラメータ取得
  • 0.32499241828918457秒:DB接続完了
  • 0.345470666885376秒:データ取得完了

さいごに

サーバーレスのWebサイト構築、いかがでしたでしょうか。
各 AWS リソースの細かい作成方法は省いていますが、大まかな流れや考え方は理解いただけたかと思います。

API Gateway との比較 ですが、ALB は作成しただけで常に料金が掛かっていますので API Gataway を使うよりもコスト面で不利になります。
不利なのはわかっているのですが、使い易さや慣れがあるので ALB 経由で Lambda 関数が実行できる今回のアップデートは個人的に喜んでいます。

CloudFront の部分を省けばもっとお手軽ですので、興味がある方はこの構成を楽しんでみてください。

参考ページ

以下のページを参考にさせていただきました。
ありがとうございました。
アプリケーションロードバランサー(ALB)のターゲットにAWS Lambdaが選択可能になりました | Amazon Web Services ブログ
ALBのバックエンドにLambdaを選択してみた! #reinvent | DevelopersIO
Aurora Serverlessが一般利用可能になったので試してみた | DevelopersIO RDSに作成したMySqlのDatabaseに日本語が登録出来ない問題 - Qiita
PyMySQL · PyPI
ミリ秒・マイクロ秒単位で処理時間を計測するには | hydroculのメモ
AWS Lambda
AmazonLinux2にMariaDBをインストールする | SaintSouth.NET