IAM認証RDS・非VPC Lambda・API Gateway・CloudFront・WAFで接続元IP制限付きAPIを作成する

主に以下2例に対して情報を少し追加するだけの記事です。

* [IAM認証によるRDS接続を試してみた](http://dev.classmethod.jp/cloud/aws/iam-auth-rds/)
* [【全世界待望】Public AccessのRDSへIAM認証(+ SSL)で安全にLambda Pythonから接続する](http://blog.serverworks.co.jp/tech/2017/04/27/rds-iam-auth-lambda-python/)

API GatewayのバックをVPC Lambdaにしている場合、APIのくせに返答に時間がかかる場合がある、という問題があります。なので、

* 非VPC Lambdaから安全にPublic AccessibleなRDSに接続したい
* かつ、API Gatewayに接続元IP制限をつけたい

というのがやりたいことです。

## RDS接続認証でIAMが使えるようになった

* [AWS IAM で RDS for MySQL と Amazon Aurora データベースへのアクセスを管理](https://aws.amazon.com/jp/about-aws/whats-new/2017/04/manage-access-to-your-rds-for-mysql-and-amazon-aurora-databases-using-aws-iam/)
* [IAM Database Authentication for MySQL and Amazon Aurora](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html)

執筆時点で公式日本語ドキュメントは無し。

### つまりどういうこと？
MySQLの認証プラグインを作ったからRDSイメージにいれておいたよー、という話らしいので見てみる。

```
select * from plugin;
+-------------------------+-------------+
| name                    | dl          |
+-------------------------+-------------+
| AWSAuthenticationPlugin | aws_auth.so |
+-------------------------+-------------+
1 row in set (0.01 sec)
```

同じものをPostgreSQLでも作ってくれればPostgreSQLでもIAM認証が使える用になるんですよね？ (よく知らないまま希望)

### それのなにがうれしいの？

MySQLのuser/password認証がAWS_ACCESS_KEY_IDとAWS_ACCESS_SECRET_KEYに置き換わったような感じです。

……って言っちゃうと「それはそう」案件になるんですが、**特定のIAMロールを持つ非VPC Lambdaからのみのアクセスを許可するようなRDS MySQLを作ることができる**、というのが特にうれしい点です。

MySQL組み込みの認証機構よりは比較的安全な方法で、RDSをPublic Accessibleにできるようになる、とも言います。RDSがPublic Accessibleなら、Lambdaも非VPCでよくなります。しかもそのLambdaも特定のIAMロールを持つものだけに制限できます。安心だ。

## やりたいこと

```
RDS (IAM & SSL) | AWS Lambda (IAM) | API Gateway (https) | CloudFront (https) | Web Application Firewall
```

![rds-iam.png](https://qiita-image-store.s3.amazonaws.com/0/15526/2ffad1ec-10b1-c795-551b-61be1317b233.png)

の構成を作りたい。社内APIとかですな。なんか登場人物が多いですが、以下の事情があります

* ソースIP制限をつけたいが、API Gateway自体にソースIP制限機能がない
* WAFにはあるが、WAFはALBかCloudFrontにしか使えない

よって、

* CloudFrontをAPI Gatewayの前段に置く
* WAFをCloudFrontの前段に置く

ことでアクセスをコントロールします。

API Gateway自体に関しては、API Tokenを必須にすることでアクセスを制限します。もちろんCloudFrontからはアクセスできないといけないので、Origin設定でAPI Tokenをヘッダに追加する必要があります。ややこしいなおい。

ちなみに、API Gatewayの認証をIAMにすることもできるんですが、これだとAPIユーザーにAWS v4 Signature Authを実装させることになるので心苦しいです。

* [Authenticating Requests (AWS Signature Version 4)](http://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sig-v4-authenticating-requests.html)
* [AWS REST リクエストの署名についてメモ](https://siguniang.wordpress.com/2014/11/23/evolution-of-aws-signing-signatures/)

うっわめんどくさっ。

## やってみよう

### RDSをつくる

[IAM Database Authentication for MySQL and Amazon Aurora](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html)

IAM認証はdb.m1.smallより大きいインスタンスのみでしか使えないので注意。ユーザーの作り方もドキュメントのままですが、

```sql
CREATE USER jane_doe IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS' REQUIRE SSL;
```

DBとLambda間はSSLにしないといけないので、`REQUIRE SSL`オプションを付けてSSLを強制します。

`Lambdaのレンジ:3306`のInをAllowしたSecGroupを作る、つってもLambdaのレンジってめっちゃ広そう？
https://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-ip-ranges.html

### Lambdaをつくる
試しに作ったLambdaは

```py
from __future__ import print_function

import os

import boto3
import mysql.connector

def lambda_handler(event, context):
    print(event, context)
    token = boto3.client('rds').generate_db_auth_token(
        DBHostname=os.environ['RDS_HOST'],
        Port=3306,
        DBUsername=os.environ['RDS_USER']
    )
    conn = mysql.connector.connect(
        host=os.environ['RDS_HOST'],
        user=os.environ['RDS_USER'],
        password=token,
        database=os.environ['RDS_DATABASE'],
        ssl_verify_cert=True,
        ssl_ca='rds-combined-ca-bundle.pem'
    )
    cursor = conn.cursor()
    cursor.execute('SELECT user FROM mysql.user')
    rows = cursor.fetchall()
    result = [str(x[0]) for x in rows]
    return {'users': result}
```

DBのユーザーを`SELECT`して返すだけのものです。

Lambda実行時のIAMロールにIAM Database Authポリシーが必要になります。

[Attaching an IAM Policy Account to an IAM User or Role](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html#UsingWithRDS.IAMDBAuth.Attaching)

```
arn:aws:rds-db:region:account-id:dbuser:dbi-resource-id/database-user-name
```

ここでMySQLの`GRANT`の`ON`と`TO`を指定しているみたいなものと考える。

別のIAMロールを付けてLambdaを実行してみると、みごとにDB接続エラーが発生する。

### Lambdaをzipでまとめる
`Dockerfile`を作ってzipを生成するナウでヤングな最先端のイカしたアレだぜ。デプロイ時に非VPCを選択。

```
FROM amazonlinux

RUN yum install -y python27-devel python27-pip zip
RUN pip install --upgrade pip
RUN mkdir /opt/rds-iam-auth /opt/build
COPY ./ /opt/rds-iam-auth/
WORKDIR /opt/rds-iam-auth
RUN pip install wheel
RUN pip install -r requirements.txt -t .
RUN zip -r rds-iam-auth.zip *

CMD cp rds-iam-auth.zip /opt/build
```

悲しいかなLambdaのamazonlinuxに入っているboto3がIAM RDS authに未対応 (執筆時) だったので`requirement.txt`に追記。近々AMIもアップデートされるだろう。

### その他をつくる

コンソールからぽちぽちやる作業がたくさんあります。特にここで追記することはないので、先人の記事を参照したいところだ。

* [API GatewayにCloud Frontを紐づけ、WAFでIP制限をかけてみる](https://recipe.kc-cloud.jp/archives/9234)
* [AWS WAFでCloudFront経由でのアクセスにIPアドレス制限を設定する](http://qiita.com/yoheiit/items/45fbd94801682dc5fe63)
* [Amazon CloudFrontでAPI Gatewayの痒いところに手を届ける](http://dev.classmethod.jp/cloud/cache-api-gateway-by-cloudfront/)

注意点まとめ

* APIエンドポイントを直接使用されないように、API Tokenを必須にしておくこと。
* WAFはGlobalリージョンで作成しないとCloudFrontのコンソールから選べないので注意。
* CloudFrontのOrigin設定時に、`x-api-token`ヘッダ転送設定を追加すること。
* CloudFrontの設定反映には時間がかかるの辛いコーヒー飲むしかない。

## ためす
WAFで社内のGlobal IPのみアクセスを許可。

```
$ curl https://hoge.cloudfront.net/
{"users": ["helloworld", "iamuser1", "mysql.sys", "rdsadmin"]}
```

WiFi変えたりとかして、Global IPを変えてみると

```
$ curl https://hoge.cloudfront.net/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
Request blocked.
<BR clear="all">
<HR noshade size="1px">
<PRE>
Generated by cloudfront (CloudFront)
Request ID: jIG5TSJUn9rPZYI0JwUr9wZHFHFa_LRyVmwGC302sHjBgv0sLoPubA==
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>
```

Forbiddenされる。

## まとめ

AWSよくできてんなー。