API Gatewayでリソースポリシーを使ったアクセス制御をやってみたい

# はじめに
API Gatewayのアクセス制御にはいくつか方法がある。2018年4月にリリースされた[リソースポリシー](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-resource-policies.html)はその一つであり、IPもしくはIAMによるアクセス制御が行える。

この記事では、リソースポリシーの設定とLambdaからのAPI呼び出しの方法を説明する。Lambda FunctionはPython3.6で作成し、デプロイにはServerless Frameworkを使用した。

# まずは結論を

- リソースポリシーを使えばIAM{ユーザ|グループ|ロール}単位でAPI Gatewayの制御ができる。
  - クロスアカウントのアクセス制御も可能
  - 許可対象のIAMロールが付与されたLambdaからAPIを叩く際には、IAMロールの認証情報を明示的にリクエストに埋め込む必要がある

# やったことと手順
ざっくりと以下を行った。

- API Gatewayのリソースポリシーを使ったアクセス制御設定
- 外部アカウントのLambdaにリソースポリシーで許可したIAMロールを付与し、LambdaからAPIを実行


## 事前準備
- Serverless Framework をインストール ([Serverless Frameworkドキュメント](https://serverless.com/framework/docs/providers/aws/guide/installation/))
- Dockerをインストールし、dockerdを起動しておく (手順はそこら中にあるので省略)

## アカウントを2つ用意
クロスアカウントアクセスを試すためにアカウントを2個準備する。それぞれAccount-A, Account-Bと呼ぶ。Shared Credentialsを用意しておく。

- Account-A: API Gatewayを使ってAPIをデプロイ
- Account-B: APIを叩くLambda Functionをデプロイ

アカウントが1個しかなくてもAccount-BをAccountAに読み替えれば以降の手順を追えるはず(やってない)。

## APIを作成する
Account-Aで[Holidays JP](https://holidays-jp.github.io/)のような休日祝日情報を返すAPIを作成する。作り方は[こちらのページ](http://inokara.hateblo.jp/entry/2018/04/08/230023)を参考にしました。ありがとうございます。今回は簡易なAPIで良いので、S3バケットに以下のJSONファイルを配置し、`api/v1/data.json`メソッドに割り当てる。

``` json:data.json
{
    "2018-01-01": "元日",
    "2018-01-08": "成人の日",
    "2018-02-11": "建国記念の日",
    "2018-02-12": "休日",
    "2018-03-21": "春分の日",
    "2018-04-29": "昭和の日",
    "2018-04-30": "休日",
    "2018-05-03": "憲法記念日",
    "2018-05-04": "みどりの日",
    "2018-05-05": "こどもの日",
    "2018-07-16": "海の日",
    "2018-08-11": "山の日",
    "2018-09-17": "敬老の日",
    "2018-09-23": "秋分の日",
    "2018-09-24": "休日",
    "2018-10-08": "体育の日",
    "2018-11-03": "文化の日",
    "2018-11-23": "勤労感謝の日",
    "2018-12-23": "天皇誕生日",
    "2018-12-24": "休日",
}
```

API Gatewayのマネジメントコンソールはこんな表示になる。
<img width="674" alt="API.png" src="https://qiita-image-store.s3.amazonaws.com/0/236562/07271ed8-4b5e-0e6a-c6e2-0919757d1041.png">


デプロイしたAPIの動作を確認する。以下のようにJSON形式で値が返ってくるはず。

``` sh
$ curl -s https://xxxxxxxxxx.execute-api.{Region}.amazonaws.com/{Stage}/api/v1/data.json
{
    "2018-01-01": "元日",
    "2018-01-08": "成人の日",
      ...(中略)...
    "2018-12-23": "天皇誕生日",
    "2018-12-24": "休日",
}
```

## APIにリソースポリシーを設定
作成したAPIの「リソースポリシー」欄にIAMによるアクセス制御ポリシーを記載する。入力方法は[公式ドキュメントの例](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-resource-policies-examples.html)を参考にしました。
このポリシーでは、Account-BのIAMロールに `execute-api:Invoke` を許可している。(許可対象のIAMロールは後ほどLambda Functionをデプロイした後に確認するので、現時点では設定しなくても良い気がする。)

``` json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::{Account-B}:role/{IAM-Role}"
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:ap-northeast-1:{Account-A}:{API-Method}/{API-Path}"
        }
    ]
}
```

リソースポリシー設定後にAPIを叩くと、こんな感じのエラーが出て怒られる。

``` sh
$ curl -s https://xxxxxxxxxx.execute-api.{Region}.amazonaws.com/{Stage}/api/v1/data.json
{"message":"Missing Authentication Token"}
```


## APIを叩くLambda Functionをデプロイする
Serverless Frameworkを使ってAccount-BにLambda Functionをデプロイする

### Serverless Framework プロジェクトを作成
`sls` コマンドでServerless Frameworkのプロジェクトを作成する

``` sh
# バージョン確認
$ sls version
1.26.1

# slsプロジェクト作成
$ mkdir -p exeApiTest
$ cd ./exeApiTest
$ sls create -name exeApiTest -t "aws-python3"
Serverless: Generating boilerplate...
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.26.1
 -------'

Serverless: Successfully generated boilerplate for template: "aws-python3"
```

exeApiTestディレクトリに`serverless.yml`と`handler.py`が作成される。`serverless.yml`はデプロイ設定。`handler.py`はデフォルトのLambda用コード。

#### Serverless Framework のデプロイ設定
`serverless.yml`にはLambda Functionと一緒にデプロイするリソースを設定できる。例えば、IAMロールやCloudWatch Eventsなど。ここではAccount-Aで作成したAPIの実行権限をIAMロールに付与する。(試してないけど、`Resource: "arn:aws:execute-api:*:*:*"`でいいかもしれない)

``` yml:IAMロールに付与するポリシーの設定例
provider:
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "execute-api:Invoke"
      Resource: "arn:aws:execute-api:ap-northeast-1:{Account-A}:{API-Method}/{API-Path}"
```

その他諸々を書いて、最終的に以下のような`serverless.yml`ができる。

``` yml:serverless.ymlの例
service: exeApiTest
provider:
  name: aws
  runtime: python3.6
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "logs:CreateLogGroup"
        - "logs:CreateLogStream"
        - "logs:PutLogEvents"
      Resource: "arn:aws:logs:*:*:*"
    - Effect: "Allow"
      Action:
        - "execute-api:Invoke"
      Resource: "arn:aws:execute-api:ap-northeast-1:{Account-A}:{API-Method}/{API-Path}"

custom:
  exclude:
    - node_modules/**
    - package.json

functions:
  runner:
    handler: handler.runner
    memorySize: 128
    timeout: 60
```

### APIを叩くLambda Functionを作成する
[こちら](https://dev.classmethod.jp/cloud/aws/botocore-signed-process/)を参考に、APIを叩くLambda Functionを作成する。

``` python:handler.pyの例
import json
import os
import logging
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from botocore.awsrequest import AWSRequest
from botocore.auth import SigV4Auth
from botocore.endpoint import BotocoreHTTPSession
from botocore.credentials import Credentials

# ログレベル設定
logger = logging.getLogger()
logLevel = logging.INFO
logger.setLevel(logLevel)

def runner(event, context):
    url = 'https://xxxxxxxxxx.execute-api.{Region}.amazonaws.com/{Stage}/api/v1/data.json'
    credentials = Credentials(os.environ["AWS_ACCESS_KEY_ID"],
                              os.environ["AWS_SECRET_ACCESS_KEY"],
                              os.environ["AWS_SESSION_TOKEN"])
    request = AWSRequest(method="GET", url=url)
    SigV4Auth(credentials, 'execute-api',
              os.environ["AWS_REGION"]).add_auth(request)
    response = BotocoreHTTPSession().send(request.prepare())
    return response.json()
```

### Lambda Functionをデプロイ
Serverless Frameworkでデプロイする

``` sh
$ pwd
/path-to-project/exeApiTest

# Serverless Frameworkでデプロイ
$ sls deploy --aws-profile={Account-B}

```

デプロイした後にLambdaに振られたIAMロールのARNを確認し、API Gatewayのリソースポリシーに設定しておく。

### Lambda Functionを実行
↑でデプロイしたLambda Functionを叩くと、休日祝日情報が取得できる。(実行も`sls`コマンドでできるので楽。Serverless強い。)

``` sh
$ sls invoke --aws-profile={Account-B} --function=runner
{
    "2018-01-01": "元日",
    "2018-01-08": "成人の日",
      ...(中略)...
    "2018-12-23": "天皇誕生日",
    "2018-12-24": "休日",
}
```

以上。こんな感じでAPIへのアクセスを制御できたけど、もっとうまい方法がある気がする。。。

# 参考情報

### AWS API関連

- [Control Access to an API with Amazon API Gateway Resource Policies](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-resource-policies.html)
  - API Gateway リソースポリシーの公式ドキュメント
- [奥さんがフェイスエステしている間に日本の祝日や休日を JSON で返す Web サービスを API Gateway と Amazon S3 そして Python で作ってみました](http://inokara.hateblo.jp/entry/2018/04/08/230023)
  - API Gatewayの設定方法を参考にしました。
- [[小ネタ] botocoreのAWS APIリクエストの署名プロセスのみを利用する](https://dev.classmethod.jp/cloud/aws/botocore-signed-process/)
  - 認証情報をつけてAPIを呼び出す方法。こちらのコードをほぼ全て使わせていただきました。
  - 上の記事ではbotocoreのメソッドをそのまま使っているので、以下のモジュールを使ったほうが安全かもしれない。
    - [aws-requests-auth](https://github.com/DavidMuller/aws-requests-auth)
    - [requests-aws4auth](https://github.com/sam-washington/requests-aws4auth)

### Serverless Framework
試していく過程で、Lambdaの実行環境にデフォルトで入っていないPythonライブラリ(Requestsとか)を使っていたので、そのようなライブラリをServerless Frameworkでデプロイする方法も紹介する。

- [serverless-python-requirements](https://www.npmjs.com/package/serverless-python-requirements)
  - requestsなど、Lambda実行環境にデフォルトで入っていないライブラリを入れるためのServerless用プラグイン
- [lambci/docker-lambda](https://github.com/lambci/docker-lambda)
  - AWS Lambda の実行環境をエミュレートするDockerイメージ。serverless-python-requirements がPythonのモジュールをコンパイルするために使っている。
  - デプロイするときにあらかじめ `docker pull` しておくと安心できる。
- [How to Handle your Python packaging in Lambda with Serverless plugins](https://serverless.com/blog/serverless-python-packaging/)
  - serverless-python-requirements の解説記事. exeApiTestのserverless.ymlはこれを参考にした。
- [AWS - Packaging](https://serverless.com/framework/docs/providers/aws/guide/packaging/)
  - Serverless Frameworkでファイルを指定してデプロイする方法。Virtualenvを使っている場合はこのやり方でデプロイパッケージを小さくできる。