CloudFormationからLambdaを実行してテーブル情報の入ったRDSを自動作成

## はじめに
CloudFormationはAWSサービスの外側を作るものですが、
最初からテーブルの入ったRDSを作れないかと考え以下の方法で作成しました。

① CloudFormationでRDSの作成
② CloudFormationからLambdaの実行
③ LambdaでRDSにテーブル情報の投入
<img width="456" alt="012.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/95ab9c02-b5af-c14a-2f42-73a63be3edc2.png">



## CloudFormation用yamlファイルの中身と解説
まず、CloudFormationで実行されるyamlファイルについてですが、
ファイルの中身は以下のようになっております。

```
Resources:
  DBInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: "RDSName" #この名前を使ってLambdaはエンドポイント名を取得します
      DBName: jimbot3rds
      Engine: MySQL
      MasterUsername: jimbot3
      DBInstanceClass: db.t2.micro 
      AllocatedStorage: '20'
      MasterUserPassword: xxxxxxxx
      VPCSecurityGroups:
      - !GetAtt DBSecurityGroup.GroupId

  Mylambda: 
    Type: "Custom::ExcuteLambda"
    Version: "1.0"
    Properties: 
      ServiceToken: "arn:aws:lambda:ap-northeast-1:XXXX~~~~" #ここにLambdaのARNを入力します
      RequestType: Create
    DependsOn: DBInstance

  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 22only
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: '22'
        ToPort: '22'
        CidrIp: "0.0.0.0/0"

  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Open database for access
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: '3306'
        ToPort: '3306'
        CidrIp: 0.0.0.0/0
```

**解説1**
DBInstanceIdentifier:の箇所で作成するRDSの名前を決めます。
この名前から後に記載しているLambdaでRDSのエンドポイントを拾います。

**解説2**
Mylambda: ~の箇所がカスタムリソースの定義で、Lambdaの実行を定義しています。
また、RDSが作成された後にLambdaを実行する必要があるためDependsOn: DBInstanceとしました。

※ファイルは[こちら](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/quickref-rds.html)のAmazon RDS テンプレートスニペットを参考にしています。

## Lambdaの事前準備
今回のLambdaは、pymysqlがインストール状態である環境でなければ実行できないため、
以下の手順でCloud9上でpymysqlをインストールし、そこからLambdaのコンソールに環境をデプロイしました。
※インストールをしていないデフォルトのLmbdaではpymysqlが利用できないエラーが以下のように出力されます。

```
 "errorMessage": "Unable to import module 'lambda_function': No module named 'pymysql'",
```

1. Cloud9で「λ」のボタンを押下
<img width="1186" alt="001.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/44c5298c-22cf-076b-57d3-51624003c2b2.png">

2. Lambda名を入力しNext
<img width="448" alt="002.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/e5807c73-f026-b447-e50c-a79fbee9502f.png">

3.runtimeでPython3.6とテンプレート無しのemptyを選択しNext
<img width="452" alt="003.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/2c9d5c02-ae33-e9be-e2b9-4c02b65c678d.png">

4.以降はNextとFinish
<img width="454" alt="004.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/65be9b24-122e-db42-8b00-775f4ddceb13.png">

5.以下のコマンドを入力しpymysqlのインストール

```
$ cd rdslambda
$ pip install pymysql -t ./
```

6.ディレクトリ構成が反映されていることを確認
<img width="1180" alt="005.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/22adf90e-41de-f1d2-2e68-67ad95e2c34c.png">

7.Deployボタンを押下
<<img width="371" alt="006.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/58bf84b4-4bed-fde1-cdac-f3b89662d36e.png">

8.Lambdaコンソールに同じディレクトリ構成でデプロイされたことを確認
これでLambdaからRDSに繋ぐための"pymysql.connect~"などが使えます
<img width="1049" alt="007.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/c8eeebb8-93e9-89b2-c4bd-ac11b2dfb230.png">

## Lambdaの中身と解説

↑で準備したLambdaに以下のソースを記載します。

```python
import boto3
import pymysql.cursors
from botocore.vendored import requests
import json

SUCCESS = "SUCCESS"
FAILED = "FAILED"

def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False):
    responseUrl = event['ResponseURL']

    print(responseUrl)

    responseBody = {}
    responseBody['Status'] = responseStatus
    responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name
    responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name
    responseBody['StackId'] = event['StackId']
    responseBody['RequestId'] = event['RequestId']
    responseBody['LogicalResourceId'] = event['LogicalResourceId']
    responseBody['NoEcho'] = noEcho
    responseBody['Data'] = responseData

    json_responseBody = json.dumps(responseBody)

    print("Response body:\n" + json_responseBody)

    headers = {
        'content-type' : '',
        'content-length' : str(len(json_responseBody))
    }

    try:
        response = requests.put(responseUrl,
                                data=json_responseBody,
                                headers=headers)
        print("Status code: " + response.reason)
    except Exception as e:
        print("send(..) failed executing requests.put(..): " + str(e))


def lambda_handler(event, context):
    dbinstance = 'RDSName' #↑のCloudFormation用のyamlファイルに記載したDBInstanceIdentifierを入力
    rds = boto3.client('rds')
    myendpoint = rds.describe_db_instances(DBInstanceIdentifier=dbinstance)

    rds_host  = myendpoint['DBInstances'][0]['Endpoint']['Address']
    db_user = "jimbot3"
    password = "xxxxxxxx"
    db_name = "jimbot3rds"

    conn = pymysql.connect(host=rds_host, user=db_user, passwd=password, db=db_name, connect_timeout=5)
    with conn.cursor() as cur:
        cur.execute("create table jimbots (id int, name varchar(10),PRIMARY KEY (id))")
        cur.execute('insert into jimbots (id, name) values(1, "jimbot1")')
        cur.execute('insert into jimbots (id, name) values(2, "jimbot2")')
        conn.commit()
        cur.execute("select * from jimbots")
        for row in cur:
            print(row)
    send(event, context, SUCCESS, {})
    
```
**解説1**
from botocore.vendored import requests から　print("send(..) failed executing requests.put(..): " + str(e)) までと、
1番最後にある　send(event, context, SUCCESS, {})　は、CloudFormationからLambdaがたたかれた場合に、
CloudFormationにLambdaからレスポンスを返すためのcfn-responseモジュールのソースコードです。
これが無ければCloudFormationにレスポンスが返されないためCloudFormationのスタックの作成が終わりません。
cfn-responseモジュールはLambdaのコンソール画面上に"import cfnresponse"と記載しても使えないためソースコードの記載です。
※cfn-responseモジュールのソースコードは[こちら](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html)


**解説2**
myendpoint = rds.describe_db_instances(DBInstanceIdentifier=dbinstance)
はRDSのエンドポイントの取得を行っています。
CloudFormationでRDSを作るときにRDSの名前は指定できますがRDSのエンドポイントは指定できないため、
こちらで指定した名前からエンドポイントを取得し、そのまま、そのエンドポイントを使用してRDSに繋ぎに行けるようにしています。

**その他**
・LambdaにはRDSFullAccessのロールが必要です。
・selectとprint(row)はデータが投入されたことの確認用で本来は不要です。

## 実行結果
① CloudFormationの実行ログです。すべて成功しているし、ちゃんとRDSができた後にLambdaが実行されています。
<img width="613" alt="009.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/486bcbe1-81cd-d1f1-f84b-8c78a1787ceb.png">

② Lambdaの実行結果のログです。(こっちは+09:00が日本時間です)
　赤枠の"Status": "SUCCESS"の箇所がLambdaが成功したことをCloudFormationに返しています。
<img width="329" alt="010.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/337e7e70-0a8c-8b94-fc86-107a4699d5d7.png">

③ RDSに繋ぎ中にデータが入っていることの確認です。
　ちゃんとJimbotsテーブルがあります。
<img width="459" alt="011.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322881/0394c297-cce8-6453-6c23-2d0b9dc4dfbd.png">


## 終わりに
LambdaにSQL文書いてるし結局、自動化っていうよりコード化というのかなぁという感じ。。。
残念ながらあまり利用できるケースが思い浮かばないので、普通に作成してデータ入れたほうが良いと思う。←
良かった点は「どうやったらできるんだろう?」と考えるのが楽しかったところくらいかな。