【AWS Lambda】API Gatewayを一発で作成するスクリプトを作成してみた（Python版）

# はじめに

- 任意のAWS Lambda関数に対するAPI Gatewayを一発の実行で作成できるPythonスクリプトの紹介です。
- スクリプト自体はAWS Lambdaに作成しました。
- クライアントからのリクエスト、Lambdaからのレスポンスのボディを無変換で対向に渡します。
- API Gateway作成と同時にAPIキー認証、スロットリング、クォータの設定をしています。

#スクリプトの作成

ランタイムにPythonを指定したAWS Lambda関数（今回は関数名agdep）を作成します。

以下のコードを記載したlambda_function.pyを作成します。

```lambda_function.py
import boto3
import uuid

agw = boto3.client('apigateway')
lam = boto3.client('lambda')
sts = boto3.client('sts')

def lambda_handler(event, context):

    #Lambda関数存在チェック
    try:
        #Lambda関数ARN取得
        response=lam.get_function(FunctionName=event['functionName'])
        lambdaArn=response['Configuration']['FunctionArn']
    except:
        return('存在しないLambda関数が指定されています。')

    #初期化
    apiName = event['functionName'] + '_api'
    regionName = 'ap-northeast-1'
    accountId = (sts.get_caller_identity())['Account']
    stageName = 'dev'

    #API作成
    agw.create_rest_api(name=apiName)

    #API ID取得
    apis = {}
    for item in (agw.get_rest_apis())['items']:
        apis[item['name']]=item
    apiId = apis[apiName]['id']

    #リソース作成
    agw.create_resource(
        restApiId=apiId,
        parentId=(agw.get_resources(restApiId=apiId))['items'][0]['id'],
        pathPart=apiName+'_rsc'
    )

    #リソースID取得
    resourcePath = '/' + apiName + '_rsc'
    resources = {}
    for item in (agw.get_resources(restApiId=apiId))['items']:
        resources[item['path']]=item
    resourceId = resources[resourcePath]['id']

    #メソッドリクエスト作成
    agw.put_method(
        restApiId=apiId,
        resourceId=resourceId,
        httpMethod='POST',
        authorizationType='None',
        apiKeyRequired=True
    )

    #Lambda側での関数実行許可
    lam.add_permission(
        FunctionName=event['functionName'],
        StatementId=str(uuid.uuid4()),
        Action='lambda:InvokeFunction',
        Principal='apigateway.amazonaws.com',
        SourceArn='arn:aws:execute-api:' + regionName + ':' + accountId + ':' + apiId + '/*/POST/' + apiName + '_rsc'
    )

    #統合リクエスト作成
    agw.put_integration(
        restApiId=apiId,
        resourceId=resourceId,
        httpMethod='POST',
        type='AWS',
        integrationHttpMethod='POST',
        uri='arn:aws:apigateway:' + regionName + ':lambda:path/2015-03-31/functions/' + lambdaArn + '/invocations'
    )

    #メソッドレスポンス作成
    agw.put_method_response(
        restApiId=apiId,
        resourceId=resourceId,
        httpMethod='POST',
        statusCode='200'
    )

    #統合レスポンス作成
    agw.put_integration_response(
        restApiId=apiId,
        resourceId=resourceId,
        httpMethod='POST',
        statusCode='200',
        responseTemplates={
            'application/json': ''
        }
    )

    #APIデプロイ
    agw.create_deployment(
        restApiId=apiId,
        stageName=stageName
    )

    #APIキー作成、キー値取得
    api_key_value = (agw.create_api_key(
        name=apiName+'_key',
        enabled=True,
        stageKeys=[
            {
                'restApiId': apiId,
                'stageName': stageName
            },
        ]
        )
    )['value']

    #Usageプラン作成
    agw.create_usage_plan(
        name=apiName+'_plan',
        apiStages=[
            {
                'apiId': apiId,
                'stage': stageName
            },
        ],
        throttle={
            'burstLimit': 10,
            'rateLimit': 5
        },
        quota={
            'limit': 200,
            'offset': 0,
            'period': 'MONTH'
        }
    )

    #UsageプランID取得
    plans = {}
    for item in (agw.get_usage_plans())['items']:
        plans[item['name']]=item
    usegePlanId = plans[apiName+'_plan']['id']
    
    #APIキーID取得
    keys = {}
    for item in (agw.get_api_keys())['items']:
        keys[item['name']]=item
    keyId = keys[apiName+'_key']['id']

    #Usageプランキー作成
    agw.create_usage_plan_key(
        usagePlanId=usegePlanId,
        keyId=keyId,
        keyType='API_KEY'
    )

    return('https://' + apiId + '.execute-api.' + regionName + '.amazonaws.com/' + stageName + resourcePath,api_key_value)
```

実行ロールは以下のポリシーと同等以上の権限が付与されたロールを使用してください。

```json-doc
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "lambda:GetFunction",
        "lambda:AddPermission",
        "apigateway:PUT",
        "apigateway:POST",
        "apigateway:GET"
      ],
      "Resource": [
        "arn:aws:lambda:<RegionName>:<AccountID>:function:*",
        "arn:aws:apigateway:<RegionName>::*"
      ],
      "Effect": "Allow"
    }
  ]
}
```
#動作確認
APIGatewayを作成したいAWS Lambda関数（今回は関数名test-func）を指定して、先程作成したスクリプトをAWSCLIより実行します。
実行結果には作成されたAPIGatewayのエンドポイントURIとAPIキーが出力されるので控えておきます。

```shell
$ function_name=test-func
$ aws lambda invoke --function-name agdep --payload {\"functionName\":\"${function_name}\"} outfile;cat outfile | jq .;rm outfile
{
    "ExecutedVersion": "$LATEST", 
    "StatusCode": 200
}
[
  "https://5wd******.execute-api.ap-northeast-1.amazonaws.com/dev/test-func_api_rsc",
  "aW7OjaD8****************************"
]
```

先程控えたエンドポイントURIとAPIキーを指定してPOSTリクエストを実行し、想定通りのレスポンスが返ってくればAPIGatewayは利用可能になっています。
お好きなシステムと連携させてお使いください。

```shell
$ endpoint_uri=https://5wd*****.execute-api.ap-northeast-1.amazonaws.com/dev/test-func_api_rsc
$ api_key=aW7OjaD8****************************
$ data='{"key1":"value1","key2":"value2","key3":"value3"}'
$ curl -X POST -H "x-api-key:$api_key" -d $data $endpoint_uri
### test-func のレスポンス ###
```

#おわりに
このスクリプトで作成されるAPIGatewayは、APIキー認証により、クライアントから受けたPOSTリクエストのペイロードをそのままLambda関数へ渡し、Lambda関数からのreturnをペイロードとしたレスポンスをそのままクライアントへ返す、最低限の機能のAPIエンドポイント/HTTPプロキシとなります。

わたしはAWS Lambdaを使ったシステムを構築するときはLambda関数とセットでAPIGatewayを作成して外部連携のためにAPI化することがほとんどなのですが、作成するAPIGatewayはネーミング以外は共通の設定で十分なことが多いため、我ながらとても便利に使っているスクリプトとなります。

ただ、作成されるAPIGatewayはID、キー値、実行するLambda関数以外は共通の設定のため、APIGatewayが実行するLambda関数をリクエスト時に動的に指定できるようになると、作成するAPIGatewayの数をうんと減らせるのですが、いまの仕様だと出来なさそうです。

余裕があればPowerShell版とシェルスクリプト版もそのうち投稿したいと思います。