AWS Chalice で API Gateway の API Key 認証と Cognito 認証を使い分ける

概要、実施したいこと

実施したいこととしては以下になります。

  1. AWS Chalice で API Gateway + Lambda をデプロイ
  2. ビルド・デプロイは CodePipeline で実行
  3. 1つの API Gateway で、API Key 認証と Cognito 認証を使い分けたい

1, 2点目は、開発効率の点を考慮したよくある構成かと思います。

3点目については、
アプリケーションユーザーからのリクエストに対しては Cognito 認証を、
外部システムからのリクエストに対しては API Key 認証を使いたい
…といった場合を想定した構成です。

上記3点を実装した際のポイントを紹介します。

ソースコードの内容

ソースコード (app.py) については以下のように実装しました。

import os

from chalice import Chalice, Response, CognitoUserPoolAuthorizer
from chalicelib import sample

app = Chalice(app_name='chalice_api')

cognito_authorizer = CognitoUserPoolAuthorizer(
    'ChaliceUserPool', provider_arns=[
        os.environ['COGNITO_USERPOOL_ARN']
    ]
)

@app.route('/app-user', authorizer=cognito_authorizer)
def function():
    return sample.hello_world()

@app.route('/ext-system', api_key_required=True)
def function():
    return sample.hello_world()

API Key 認証と Cognito 認証を使い分けるために URL パスを分けました。
アプリケーションユーザー用パス(/app-user)には authorizer=cognito_authorizer で Cognito 認証を設定し、
外部システム用パス(/ext-system)には api_key_required=True で API Key 認証を設定しています。

ビルドの設定

CodePipeline の設定としては以下のようになっています。
Build ステージで CloudFormation テンプレートを作成し Deploy ステージに渡す必要があります。

そのため Build ステージで chalice package を実行しテンプレートを生成しています。
以下のような buildspec を使いました。

chalice package の実行時にソースコード(app.py)が参照されるため
ビルド時点で、ソースコードに定義された環境変数(COGNITO_USERPOOL_ARN)が利用できる必要があります。

このような環境変数はプロジェクトディレクトリの .chalice/config.json に記載する必要があります。
Configuration File – environment_variables

ですが、ソースコード内に環境固有の値(COGNITO_USERPOOL_ARN)を記載することに抵抗があったので、
ソースコード外から COGNITO_USERPOOL_ARN を挿入する仕組みにしました。
それが上記 buildspec 内で実施している build.py です。

プロジェクトディレクトリルートに config.json の元になる chalice-config.json を配置し、
build.py がそこに COGNITO_USERPOOL_ARN (CodeBuild の環境変数) を追加し .chalice/config.json として保存しています。

build.py の内容は以下の通りです。

import argparse
import os
import json

ROOT_PATH = os.path.dirname(os.path.abspath(__file__))

class Build():
    def __init__(self, args):
        self.cognito_userpool_arn = args.cognito_userpool_arn

    def main(self) -> None:
        self.config = self.load_base_config()
        self.generate_config()
        self.output_config()
    
    def load_base_config(self) -> dict:
        with open(f'{ROOT_PATH}/chalice-config.json', 'r') as file:
            base_config = json.load(file)
        return base_config

    def generate_config(self) -> None:
        self.config['environment_variables'] = {
            'COGNITO_USERPOOL_ARN': self.cognito_userpool_arn
        }
        
    def output_config(self) -> None:
        with open(f'{ROOT_PATH}/.chalice/config.json', 'w') as file:
            file.write(json.dumps(self.config, indent=2))

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--cognito-userpool-arn', required=True)
    args = parser.parse_args()
    return Build(args).main()

main()

本当はこのような独自のスクリプトを作りたくなかったのですが、
そこまで複雑なスクリプトでもないので、これはこれで良しとしました。

まとめ

  • Cognito 認証 / API Key 認証 を使い分けるには URL パスを分ける必要がある
  • Chalice で Cognito 認証を設定する場合は authorizer=... で設定する
  • Chalice で API Key 認証を設定する場合は api_key_required=True で設定する
  • ビルド(chalice package)実行時に Cognito ユーザープール ARN が必要になるが、ARN をソースコードに含めたくない場合は今回紹介した build.py のような工夫が必要

共同著者: 神津