AWS LambdaでKMSを使ってセキュアにRDSへ接続する(実践編)〜ChatWorkで通知しよう〜

この記事は[WHITEPLUS Advent Calendar](http://qiita.com/advent-calendar/2016/whiteplus "")  2016 19日目になります。

こんにちは。株式会社ホワイトプラス、エンジニア(マネージャー)の @hachihiro224です。

[前回](http://qiita.com/hachihiro224/items/6d778156c4ef2757cb6a)はRDSに接続できるLambdaの環境構築までしましたが、
今回はその環境を利用して、RDSからデータを取得し、Chatworkへ通知するアプリケーションを作成してみます。

## 前回のおさらい
前回の環境でKMSで接続情報を暗号化し、VPC内のRDSに接続できる状態になりました。
VPC内リソースのアクセスとインターネットへの接続が可能になっています。

## ライブラリ化
このまま一つのファイルに機能を盛り込んでも良いのですが、DBの接続や復号は共通な処理なのでライブラリとしてファイルを分けていきます。

``` ディレクトリ構成
sample_function
├── create-function.sh
├── require.txt
├── src
│   ├── kms_client.py
│   ├── mysql_client.py
│   ├── rds_config.py
│   └── sample_function.py
└── update-function.sh
```
``` kms_client.py
# -*- coding: utf-8 -*-
import base64
import boto3

kms = boto3.client('kms')


def decrypt(encrypted):
    return kms.decrypt(CiphertextBlob=base64.b64decode(encrypted))['Plaintext']
```
``` rds_config.py
db_host = '暗号化したdb_host'
db_username = '暗号化したdb_user'
db_password = '暗号化したpassword'
db_name = 'db_name'
```
``` mysql_client.py
# -*- coding: utf-8 -*-
import pymysql
import rds_config
from kms_client import decrypt

# rds settings
rds_host = decrypt(rds_config.db_host)
name = decrypt(rds_config.db_username)
password = decrypt(rds_config.db_password)
db_name = rds_config.db_name


def connect():
    return pymysql.connect(rds_host, user=name, passwd=password, db=db_name, connect_timeout=5, charset='utf8')


def fetch(conn, sql, param=None):
    with conn.cursor() as cur:
        num = cur.execute(sql, param)
        return num, cur
```
``` sample_function.py
# -*- coding: utf-8 -*-

import sys
import logging
from mysql_client import connect, fetch

logger = logging.getLogger()
logger.setLevel(logging.INFO)

try:
    conn = connect()
except:
    logger.error("ERROR: Unexpected error: Could not connect to MySql instance.")
    sys.exit()


def lambda_handler(event, context):
    num, cur = fetch(conn, 'SELECT NOW()')
    for row in cur:
        logger.info('接続確認 %s' % row[0])

    return "Got %d items from RDS MySQL table" % num
```

登録したLambda Functionを更新するために、shellを作成します。

``` update_function.sh
#!/bin/bash

# TODO: update function_name
FUNCTION_NAME=sample_function

cd "$(dirname "$0")"/src
mkdir bundle
cp *.py bundle/
cd bundle

pip freeze > require.txt
[ -s require.txt ] && pip install -r require.txt -t .

zip -r upload.zip .

aws lambda update-function-code \
    --function-name ${FUNCTION_NAME} \
    --zip-file fileb://upload.zip

cd .. && rm -rf bundle
```

Lambda Functionを更新して結果が同じであれば、ライブラリ化出来ました。

## ChatWorkへ通知する
ChatWorkへ通知するためのライブラリを作成します。

``` chatwork_config.py
BASE_URL = 'https://api.chatwork.com/v1/rooms/{}/messages'
DEFAULT_TOKEN = '暗号化されたtoken'
ROOM = {
    'test': 'room_id',
    'prod': 'room_id'
}
```
``` chatwork.py
# -*- coding: utf-8 -*-
import urllib
import logging
from urllib2 import Request, urlopen, URLError, HTTPError
from chatwork_config import BASE_URL, DEFAULT_TOKEN
from kms_client import decrypt

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def post(room_id, message):
    logger.info("Message: " + str(message))

    url = BASE_URL.format(room_id)
    token = decrypt(DEFAULT_TOKEN)
    headers = {
        'X-ChatWorkToken': token
    }
    payload = {
        'body': message
    }

    req = Request(url, urllib.urlencode(payload), headers)
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", payload['body'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)
```
これを使ってChatworkへ通知するようにsample_function.pyを書き換えてみます。

``` sample_function.py
# -*- coding: utf-8 -*-

import sys
import logging
import chatwork
from chatwork_config import ROOM
from mysql_client import connect, fetch

logger = logging.getLogger()
logger.setLevel(logging.INFO)

try:
    conn = connect()
except:
    logger.error("ERROR: Unexpected error: Could not connect to MySql instance.")
    sys.exit()


def lambda_handler(event, context):
    num, cur = fetch(conn, 'SELECT NOW()')
    for row in cur:
        logger.info('接続確認 %s' % row[0])
        chatwork.post(ROOM['prod'], '接続確認 %s' % row[0])

    return "Got %d items from RDS MySQL table" % num
```
chatworkで通知する部屋にトークンを取得したユーザーを追加するのをお忘れなく。

## まとめ
今回は前回作った基盤の上でChatworkへと通知出来るようにLambda Functionを作りました。
この要領でライブラリを増やすことで、Slackへの通知やElasticsearch Serviceへの登録など用途は広がっていきます。
今回作成したものは以下に公開していますので、ご自由に利用ください。(ただし、自己責任でお願いします。)
https://github.com/WHITEPLUS/lambda_vpc_rds

明日は弊社エンジニア @kai-zoa の「続・Goでリモートデバッグ」です。

#ホワイトプラスではエンジニアを募集しています
ホワイトプラスでは、[新しい技術にどんどん挑戦したい！という技術で事業に貢献したいエンジニアを募集](https://www.wantedly.com/projects/69257)しております。