はじめに
ソーイ株式会社 村上です。
ソーイではAWSへのホスティングをメインにシステム構築を行っております。普段はWebフレームワーク内からAWSサービスを組み込むことが多いのですが、AWSサービス同士を連携させる構成も増えてきました。そこでこの記事では、API Gatewayで認証を設ける場合の検証を行いました。AWSのサーバーレスサービスを組み合わせて、認証付きAPIを構築して実行します。
既存システムの認証と組み合わせる場合、どういった方法があるか調査・検証し、シンプルなAPIを作成しました。
API Gatewayの認証について
調査したところAPI Gatewayでエンドポイントに認証を追加する場合、以下の選択肢がありました。
認証の選択肢
1. IAM認証
AWS内部やSDKを利用して呼び出す場合に適しています。AWSのIAMロールやIAMユーザーを使用して認証を行います。
特徴:
- AWS SDKを使用した認証
- AWS内部からの呼び出しに最適
フロー図:
2. Lambda Authorizer
ヘッダー、クエリパラメータ、IPアドレス、JWTなど、任意の情報を使って認可処理ができます。柔軟性が高く、カスタム認証ロジックを実装できます。
特徴:
- カスタム認証ロジックを実装可能
- ヘッダー、クエリパラメータ、IPアドレスなど様々な情報を利用
- JWTトークンの検証も可能
- 外部からの呼び出しに対応
フロー図:
3. Cognito ユーザプール
ユーザー認証基盤をAWSに任せたい場合に最適です。ユーザー管理、パスワードリセット、MFAなどの機能が提供されます。
特徴:
- ユーザー管理機能が充実
- パスワードリセット、MFAなどの機能
- ユーザー認証基盤をAWSに任せられる
- 外部からの呼び出しに対応
フロー図:
今回の選択
今回はLambda Authorizerを使用します。認証システムをすでに実装しており運用中のシステムをAWSと組み合わせたい場合に一番利用しやすそうなためです。
今回はシンプルにリクエスト時の固定アクセストークンを検証することで認証の可否ができるものを作成します。
この記事で学べること
- API GatewayでREST APIを構築する方法
- Lambda AuthorizerでAPIを保護する方法
- Step Functionsでワークフローを定義する方法
- SAM(Serverless Application Model)によるインフラ管理
使用技術の概要
API Gateway
REST APIやHTTP APIを作成・管理するサービスです。リクエストを受け取り、バックエンドサービスにルーティングします。
Lambda Authorizer
API Gatewayのリクエストを、実際のAPI処理の前に検証するLambda関数です。今回はトークンベースの認証を実装し、認証トークンを含むリクエストのみを許可します。
Step Functions
複数のAWSサービスを組み合わせたワークフローを視覚的に定義・実行できるサービスです。今回はLambda関数を呼び出すシンプルなワークフローを作成します。
Lambda
サーバーレスでコードを実行できるサービスです。今回は2つのLambda関数を使用します:
- Authorizer関数: 認証トークンを検証して認証を行う
- Task関数: 実際の処理を実行
SAM (Serverless Application Model)
サーバーレスアプリケーションを定義するためのフレームワークです。YAMLファイルでインフラをコード化できます。
アーキテクチャ
このシステムの構成は以下の通りです:
Client (curl, Postman等)
│
▼
API Gateway (/hello POST)
│
├─▶ Lambda Authorizer (トークン検証)
│ └─ 有効なトークン → 許可
│ └─ 無効なトークン → 拒否 (401)
│
▼
Step Functions (learning-api-workflow)
│
▼
Lambda (task-function)
└─ 処理を実行して結果を返す
前提条件
以下のツールがインストール・設定されている必要があります:
- AWS CLI: AWSサービスをコマンドラインから操作
- SAM CLI: サーバーレスアプリケーションのビルド・デプロイ
- Python 3.14: Lambda関数の開発(ローカル環境)
プロジェクト構成
learning-api/
├── functions/
│ ├── authorizer/
│ │ └── app.py # 認証処理
│ └── task/
│ └── app.py # タスク処理
├── statemachine.asl.json # Step Functions定義
├── template.yaml # SAMテンプレート
├── samconfig.toml # SAM設定
└── README.md
コード解説
1. Lambda Authorizer (functions/authorizer/app.py)
認証トークンを検証するLambda関数です。今回はトークンベースの認証を実装していますが、IPアドレスベースの認証やJWT認証にも対応可能です。
import os
VALID_TOKEN = os.environ.get('VALID_TOKEN', 'my-secret-token-123')
def lambda_handler(event, context):
print(f"Event: {event}")
token = event.get('authorizationToken', '')
method_arn = event['methodArn']
# Bearer プレフィックス除去
if token.startswith('Bearer '):
token = token[7:]
if token == VALID_TOKEN:
return generate_policy('user', 'Allow', method_arn)
else:
return generate_policy('user', 'Deny', method_arn)
def generate_policy(principal_id, effect, resource):
return {
'principalId': principal_id,
'policyDocument': {
'Version': '2012-10-17',
'Statement': [
{
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': resource
}
]
},
'context': {
'userId': principal_id
}
}
ポイント:
-
authorizationTokenからトークンを取得(API GatewayがAuthorizationヘッダーから自動的に取得) - 環境変数
VALID_TOKENと比較 - 一致すれば
Allow、不一致ならDenyのポリシーを返す - 返却値はIAMポリシードキュメント形式で返す必要がある(API GatewayがIAMポリシーを使用してアクセス制御を行うため)
IAMポリシードキュメント形式について:
Lambda Authorizerの返却値は、必ずIAMポリシードキュメント形式である必要があります。これはAPI GatewayがIAMポリシーを使用してアクセス制御を行うためです。
必須フィールド:
-
principalId: リクエスト元を識別するID(文字列) -
policyDocument: IAMポリシードキュメント-
Version: ポリシー言語のバージョン(通常は"2012-10-17") -
Statement: ポリシーステートメントの配列-
Effect:"Allow"または"Deny" -
Action:"execute-api:Invoke"(API Gatewayのアクション) -
Resource: 許可/拒否するリソース(通常はmethodArn)
-
-
オプションフィールド:
-
context: 後続のLambda関数や統合に渡す追加情報(キー・バリューのマップ)
2. Task Lambda (functions/task/app.py)
実際の処理を実行するLambda関数です。
import json
from datetime import datetime
def lambda_handler(event, context):
print(f"Received event: {json.dumps(event)}")
result = {
'status': 'success',
'message': 'Task completed successfully',
'processedAt': datetime.now().isoformat(),
'inputReceived': event
}
return result
ポイント:
- Step Functionsから渡されたイベントを受け取る
- 処理結果をJSON形式で返す
- 実際のアプリケーションでは、ここにビジネスロジックを実装
3. Step Functions定義 (statemachine.asl.json)
ワークフローを定義するJSONファイルです。
{
"Comment": "Learning workflow - Simple Lambda invocation",
"StartAt": "ProcessTask",
"States": {
"ProcessTask": {
"Type": "Task",
"Resource": "${TaskFunctionArn}",
"ResultPath": "$.taskResult",
"Next": "Success"
},
"Success": {
"Type": "Succeed"
}
}
}
ポイント:
-
StartAt: ワークフローの開始状態 -
ProcessTask: Task関数を呼び出す状態 -
Success: 成功を表す終了状態 -
${TaskFunctionArn}: SAMテンプレートから置換される値
4. SAMテンプレート (template.yaml)
インフラを定義するYAMLファイルです。主要な部分を解説します。
グローバル設定
Globals:
Function:
Timeout: 30
Runtime: python3.14
Architectures:
- arm64
すべてのLambda関数に適用される共通設定です。
パラメータ
Parameters:
ValidToken:
Type: String
Default: my-secret-token-123
NoEcho: true
デプロイ時に指定できるパラメータです。NoEcho: true により、CloudFormationコンソールで表示されません。認証に使用するトークンを設定します。
API Gateway設定
LearningApi:
Type: AWS::Serverless::Api
Properties:
Name: learning-api
StageName: dev
Auth:
DefaultAuthorizer: LambdaAuthorizer
Authorizers:
LambdaAuthorizer:
FunctionArn: !GetAtt AuthorizerFunction.Arn
Identity:
Header: Authorization
ReauthorizeEvery: 300
-
DefaultAuthorizer: すべてのエンドポイントに適用される認証 -
ReauthorizeEvery: 300: 300秒ごとに再認証(キャッシュ時間)
注意点:
デバッグ中にトークンを変更しても認証が通り続ける現象が発生しました。原因は ReauthorizeEvery: 300 による認証結果のキャッシュでした。開発中は ReauthorizeEvery: 0 に設定するか、キャッシュを意識してテストする必要があります。本番環境ではパフォーマンスのためキャッシュを有効にすることをお勧めします。
Lambda関数定義
AuthorizerFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: learning-api-authorizer
CodeUri: functions/authorizer/
Handler: app.lambda_handler
Environment:
Variables:
VALID_TOKEN: !Ref ValidToken
-
CodeUri: 関数のコードが格納されているディレクトリ -
Handler: エントリーポイント(ファイル名.関数名) -
Environment: 環境変数の設定(VALID_TOKENに認証トークンを設定)
Step Functions定義
ProcessingStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
Name: learning-api-workflow
DefinitionUri: statemachine.asl.json
DefinitionSubstitutions:
TaskFunctionArn: !GetAtt TaskFunction.Arn
Events:
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref LearningApi
Path: /hello
Method: POST
-
DefinitionUri: ステートマシン定義ファイル -
DefinitionSubstitutions: 定義ファイル内の変数を置換 -
Events: API Gatewayからのイベントを受け取る設定
デプロイ手順
1. ビルド
sam build
このコマンドで:
- Lambda関数のコードをパッケージ化
- 依存関係を解決
- デプロイ用のアーティファクトを生成
2. デプロイ(初回)
sam deploy --guided
初回は --guided オプションを使用します。対話形式で以下を設定:
- Stack Name:
learning-api - AWS Region:
ap-northeast-1など - Parameter ValidToken: 認証トークン(デフォルト:
my-secret-token-123) - その他の設定
注意: 実際に使用する場合は、セキュアなトークンを設定してください。デフォルトのトークンは学習用のため、本番環境では使用しないでください。
3. デプロイ(2回目以降)
sam deploy
設定は samconfig.toml に保存されているため、オプションを指定する必要はありません。
デプロイの確認
デプロイが成功すると、以下のような出力が表示されます:
Successfully created/updated stack - learning-api in ap-northeast-1
動作確認
1. APIエンドポイントの取得
API_URL=$(aws cloudformation describe-stacks \
--stack-name learning-api \
--query 'Stacks[0].Outputs[?OutputKey==`ApiEndpoint`].OutputValue' \
--output text)
echo $API_URL
2. 認証なしでリクエスト(401エラー)
認証トークンを含めずにリクエストを送信すると、401エラーが返されます。
curl -X POST $API_URL \
-H "Content-Type: application/json" \
-d '{"message": "hello"}'
結果:
{"message": "Unauthorized"}
ステータスコード: 401
3. 認証ありでリクエスト(200 OK)
認証トークンを含めてリクエストを送信すると、正常に処理されます。
curl -X POST $API_URL \
-H "Content-Type: application/json" \
-H "Authorization: Bearer my-secret-token-123" \
-d '{"message": "hello"}'
結果:
{
"executionArn": "arn:aws:states:...",
"startDate": "2024-01-08T...",
...
}
ステータスコード: 200
補足: リクエストヘッダーに Authorization: Bearer <token> を含める必要があります。トークンはデプロイ時に設定した ValidToken パラメータと一致している必要があります。
4. Step Functions実行結果の確認
# 最新の実行を取得
STATE_MACHINE_ARN=$(aws cloudformation describe-stacks \
--stack-name learning-api \
--query 'Stacks[0].Outputs[?OutputKey==`StateMachineArn`].OutputValue' \
--output text)
aws stepfunctions list-executions \
--state-machine-arn $STATE_MACHINE_ARN \
--max-results 1
実行詳細を確認:
aws stepfunctions describe-execution --execution-arn <EXECUTION_ARN>
認証トークンの変更
下記コマンドでパラメータを上書きして反映できます。
sam deploy --parameter-overrides ValidToken=your-custom-token
Step Functionsにステップを追加
statemachine.asl.json を編集して、より複雑なワークフローを定義できます。
例:入力検証を追加
{
"StartAt": "ValidateInput",
"States": {
"ValidateInput": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.message",
"IsPresent": true,
"Next": "ProcessTask"
}
],
"Default": "InvalidInput"
},
"ProcessTask": {
"Type": "Task",
"Resource": "${TaskFunctionArn}",
"Next": "Success"
},
"InvalidInput": {
"Type": "Fail",
"Error": "InvalidInput",
"Cause": "message field is required"
},
"Success": {
"Type": "Succeed"
}
}
}
編集後、再デプロイ:
sam build
sam deploy
再度CURLでリクエストすると、下記のようにStep Functionsの実行が成功しました。
クリーンアップ
実行まで成功したため、リソースを削除します。
sam delete --stack-name learning-api
これで、作成したすべてのリソースが削除されます。
まとめ
Lambda Authorizerを利用してAPI Gatewayに認証を追加することができました。Lambdaを更新することで任意の認証処理を挟むことができます。Lambda Authorizerの返却値はIAMポリシー形式の必要があるため、注意が必要です。また、SAMを使うことで管理とデプロイが容易になるため積極的に使っていきたいです。
お知らせ
技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!
https://qiita.com/organizations/sewii
