Lambda & API Gateway — 実行ロール・認証方式・mTLS
Lambda実行ロール/VPC設計、API Gateway認証3方式、リソースポリシー、mTLSを徹底解説
イントロダクション
Lambda と API Gateway は現代的なAWSアーキテクチャの中核をなすサーバーレスコンポーネント。しかしセキュリティの複雑さは見た目以上だ。
実行ロール、リソースポリシー、認証方式の選択——これらはすべて独立ではなく相互に作用する。本講では、SCS-C03 で頻出する設計パターンと落とし穴を徹底解説する。
API Gateway 認証3方式の比較フロー
Loading diagram...
Lambda VPC 接続アーキテクチャ
Loading diagram...
第1部: Lambda セキュリティ
1. 実行ロール(Execution Role)— 最小権限設計の基本
実行ロールの役割
Lambda関数が実行時に引き受けるIAMロール。S3へのアクセス、DynamoDB書き込み、ログ出力など、関数が「自分で」リソースにアクセスするときの権限を定義する。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/my-function:*"
},
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/input/*"
},
{
"Effect": "Allow",
"Action": "dynamodb:PutItem",
"Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/my-table"
}
]
}
リソースベースポリシーとの違い
| 観点 | 実行ロール | リソースベースポリシー |
|---|---|---|
| 何を制御するか | Lambdaが「できること」 | 「誰がLambdaを呼べるか」 |
| ポリシー定義の位置 | IAMロールに付与 | Lambda関数リソースに直接付与 |
| クロスアカウント対応 | 信頼ポリシー(Trust Policy)が必要 | 直接指定可能 |
| 同一アカウント内通信 | 通常は実行ロールのみで十分 | 不要(通常) |
クロスアカウント呼び出しの例:
アカウントA内のEC2がアカウントB内のLambdaを呼ぶ場合:
// アカウントB: Lambda のリソースベースポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/ec2-role" // アカウントA
},
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:ap-northeast-1:222222222222:function:my-function"
}
]
}
さらに、アカウントA内のEC2ロール自体が以下の信頼ポリシーを持つ必要はない(Lambda呼び出しはリソースベースで許可されているため)。
2. リソースベースポリシー — 「誰が呼べるか」を制御
API Gateway からの呼び出し
API Gatewayがログを出力するには、Lambda関数のリソースベースポリシーで許可される必要はない(CloudWatch Logsロールで制御される)。しかし、API GatewayがLambdaを呼ぶことは許可が必要。
AWS マネージドポリシー AWSLambdaFullAccess を使わず、APIGatewayからの呼び出しのみ許可する:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
},
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:my-function",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:execute-api:ap-northeast-1:123456789012:abcdef123/*/POST/users"
}
}
}
]
}
S3 イベント トリガー
S3バケットからのLambda呼び出しも同様:
{
"Effect": "Allow",
"Principal": {
"Service": "s3.amazonaws.com"
},
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:ap-northeast-1:123456789012:function:s3-processor",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:s3:::my-bucket"
}
}
}
試験で狙われるポイント
- リソースベースポリシーは呼び出し元ごとに制御可能(ワイルドカード回避)
- IAM実行ロール + リソースベースポリシーの両方が「許可」である必要がある(AND条件)
- 見落としやすい: Lambda実行ロールにCloudWatch Logsのputイベント権限がないと、アクティビティログすら出力されない
3. VPC接続Lambda — ENI、NAT Gateway、セキュリティグループ
VPC接続の仕組み
Lambda関数がVPC内のリソース(RDS、Elasticacheなど)にアクセスするには、VPCに接続する必要がある。この時、AWSが自動的にENI(Elastic Network Interface)をプライベートサブネットに作成する。
# Lambda 設定(例)
VpcConfig:
SubnetIds:
- subnet-12345678 # プライベートサブネット(NAT Gateway経由でインターネット接続)
- subnet-87654321 # AZ冗長化
SecurityGroupIds:
- sg-lambda
NAT Gateway の必須性
VPC内のLambdaがインターネットアクセスが必要な場合(例:外部APIを呼ぶ、S3にアップロード)、以下が必須:
- NAT Gateway をパブリックサブネットに配置
- プライベートサブネットのルートテーブルで
0.0.0.0/0をNAT Gatewayに向ける - 無料枠なし — NAT Gateway は時間単位 + データ転送で課金
VPC接続がない場合:
VpcConfig: {} # または、このセクション自体を省略
この場合、Lambda はデフォルトVPCのIAMロール権限でのみ動作し、パブリックインターネットアクセスは可能だが、VPC内プライベートリソースへのアクセスは不可。
VPC エンドポイント活用(コスト削減)
NAT Gateway を使わずに AWS サービスへアクセス:
# VPC エンドポイント (Gateway) — S3, DynamoDB は無料
- Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: vpc-12345
ServiceName: com.amazonaws.ap-northeast-1.s3
RouteTableIds:
- rtb-lambda
# VPC エンドポイント (Interface) — API Gateway 等
- Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: vpc-12345
ServiceName: com.amazonaws.ap-northeast-1.apigateway
PrivateDnsEnabled: true
SubnetIds:
- subnet-lambda
SecurityGroupIds:
- sg-vpce
セキュリティグループ設定:
// sg-vpce のインバウンド
{
"IpProtocol": "tcp",
"FromPort": 443,
"ToPort": 443,
"SourceSecurityGroupId": "sg-lambda"
}
これにより、Lambda は NAT Gateway コスト(月 $32 以上)を回避して AWS サービスに接続できる。
4. 環境変数の暗号化 — KMS キーの選択
デフォルト暗号化 vs カスタム KMS キー
Lambda環境変数は デフォルトでAWS管理キーで暗号化 される。
| 方式 | キー管理者 | キー操作権限 | コスト | 使用場面 |
|---|---|---|---|---|
| デフォルト暗号化 | AWS | ユーザーが制御不可 | 無料 | 非機密データ、開発環境 |
| カスタムKMSキー | ユーザー | IAMで制御 | $1/月 + 使用料 | APIキー、データベース接続文字列、本番環境 |
カスタム KMS キー設定
import boto3
import json
# Lambda関数内で、カスタムキーで暗号化された環境変数を復号
kms = boto3.client('kms')
def decrypt_env_var(encrypted_value):
"""KMSで暗号化された環境変数を復号(第1回呼び出しでキャッシュされる)"""
if encrypted_value.startswith('aws/lambda'): # デフォルトキー
# CloudWatch Logsに出力される際、自動的に復号される
return encrypted_value
response = kms.decrypt(
CiphertextBlob=bytes.fromhex(encrypted_value),
EncryptionContext={
'aws:lambda:FunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME'],
'aws:lambda:alias': 'LIVE'
}
)
return response['Plaintext'].decode('utf-8')
def lambda_handler(event, context):
db_password = decrypt_env_var(os.environ['DB_PASSWORD'])
# 以下のロジック...
IAM ポリシー(Lambda実行ロール):
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/12345678-1234-1234-1234-123456789012",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:aws:lambda:FunctionName": "my-function"
}
}
}
注意: CloudWatch Logsに環境変数がログアウトされる場合、デフォルトでは復号されない。別途マスキング設定が必要:
{
"LogGroupName": "/aws/lambda/my-function",
"LogGroups": [
{
"LogGroupName": "/aws/lambda/my-function",
"KmsKeyArn": "arn:aws:kms:ap-northeast-1:123456789012:key/..."
}
]
}
5. レイヤーのセキュリティ — 共有コードの信頼性
Lambda レイヤーは複数の関数間で共有できる非常に便利な機構だが、セキュリティ上の落とし穴がある。
レイヤーのリソースベースポリシー
公開されたレイヤーは 他のAWSアカウントから利用可能 になる:
# レイヤーのアクセス許可確認
aws lambda get-layer-version-policy \
--layer-name my-shared-layer \
--version-number 1
レイヤーを特定のアカウントのみに限定:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root" // 同一アカウント
},
"Action": "lambda:GetLayerVersion",
"Resource": "arn:aws:lambda:ap-northeast-1:111111111111:layer:my-layer:1"
}
]
}
レイヤー内の依存関係の検証
レイヤーに含まれるライブラリの脆弱性を定期的にスキャン:
# レイヤーに含まれるPythonライブラリをスキャン
pip install safety
safety check --file requirements.txt
# または
npm audit # Node.js の場合
CI/CD パイプラインでレイヤー更新前に実行:
# GitHub Actions 例
- name: Scan layer dependencies
run: |
pip install -r layers/common/python/requirements.txt
safety check --file layers/common/python/requirements.txt
6. 予約済み同時実行数(Reserved Concurrency)— DDoS保護
予約済み同時実行数の役割
Lambda の同時実行数に対して ハード制限 を設ける:
# 同時実行数を10に制限
aws lambda put-function-concurrency \
--function-name my-function \
--reserved-concurrent-executions 10
この場合:
- 同時実行数が10を超えると、追加リクエストは ThrottlingException で拒否
- 他のLambda関数の予約済み同時実行を圧迫しない(アカウント内の独立した上限)
スケーリング戦略
| パターン | 予約済み同時実行数 | 用途 |
|---|---|---|
| Web API | 100 | 通常のトラフィック許容範囲 |
| クリティカルシステム | 50-200 | SLA厳守が必須 |
| バッチ処理 | 5-20 | 時間がかかるため、並行度を低く |
| DDoS標的 | 10 | 攻撃時の連鎖効果を最小化 |
プロビジョニング済み同時実行数との違い
| タイプ | 実行準備 | コスト | 用途 |
|---|---|---|---|
| 予約済み | コールドスタート可能性あり | 無料(制限のみ) | 制御・保護 |
| プロビジョニング済み | 常に準備完了(コールドスタートなし) | $0.015/時間/インスタンス | 低遅延が必須 |
7. CodeSigningConfig — コード署名と改ざん防止
コード署名の設定
Lambda関数がデプロイされる際、コードが署名されていることを強制:
# Signer 証明書を作成(AWS Signer)
aws signer put-signing-profile \
--profile-name my-profile \
--signature-version SHA256withRSA
# Lambda関数にコード署名設定を適用
aws lambda put-code-signing-config \
--function-name my-function \
--code-signing-config-arn arn:aws:lambda:ap-northeast-1:123456789012:code-signing-config:csc-1234567890abcdef0
# 署名されていないコードをアップロードしようとするとエラー
# error: Invalid or missing code signing configuration
IAM ポリシー(署名権限)
{
"Effect": "Allow",
"Action": [
"signer:GetSigningProfile",
"signer:GetRevocation",
"signer:ListSigningProfiles",
"signer:DescribeSigningJob"
],
"Resource": "*"
}
Use Case: 本番環境では全てのLambda関数にコード署名を強制し、未検証なコードの実行を防ぐ。
8. Lambda SnapStart — 初期化のセキュリティ考慮事項
SnapStart は 関数の初期化状態をスナップショット して、コールドスタート時間を短縮する(Java/Go)。
セキュリティ上の注意点
-
スナップショット内に機密情報が含まれていないか確認
// 危険: スナップショット時にAPIキーをメモリに保持 public class LambdaHandler { private static String API_KEY = System.getenv("API_KEY"); // スナップショット時に読み込まれる public void handleRequest(Event event, Context context) { // API_KEY がスナップショット内に保存される可能性 } }改善: 関数起動時に環境変数を読み込む
public class LambdaHandler { private String apiKey; @Override public void init() { apiKey = System.getenv("API_KEY"); // 毎回読み込む } } -
データベース接続の状態管理
// スナップショット時のDB接続が古い可能性 private static Connection dbConn = DriverManager.getConnection(dbUrl); // 改善: SnapStart復帰時に再接続 @SnapStartHandler public void restoreConnection() { if (dbConn != null && dbConn.isClosed()) { dbConn = DriverManager.getConnection(dbUrl); } }
9. /tmp ディレクトリの暗号化
Lambda は 一時的なファイルストレージとして /tmp を提供 する(512MB)。
暗号化設定
デフォルトでは暗号化されないため、機密ファイルは避けるべき:
import os
import tempfile
from cryptography.fernet import Fernet
# 不安全: /tmp にAPIキーを平文で保存
def unsafe_example():
with open('/tmp/api_key.txt', 'w') as f:
f.write(os.environ['API_KEY']) # 危険!
# 安全: /tmp にはダウンロードした公開ファイルのみ
def safe_example():
key = Fernet.generate_key()
cipher = Fernet(key)
# メモリ内で処理、/tmp は避ける
encrypted_data = cipher.encrypt(b'sensitive')
# または、Secrets Manager から読む
import boto3
secrets = boto3.client('secretsmanager')
secret = secrets.get_secret_value(SecretId='my-secret')
10. Lambda の制限 — できないことを理解する
実行時間上限: 15分
import time
def lambda_handler(event, context):
for i in range(1000):
time.sleep(1) # 15分後にタイムアウト(LambdaTimeoutException)
代替案:
- 長時間処理: Step Functions で調整
- バッチ: SQS + Lambda polling(連鎖実行)
メモリ上限: 10GB
10GB を超えるメモリが必要な場合:
- EC2, ECS, Fargate を検討
- GlueJob(大容量データ処理)
コールドスタートのセキュリティ影響
コールドスタート中は実行ロール認証が新規取得される。KMS暗号化キーにアクセスする場合、遅延が増加:
通常: 10ms → KMS キー復号で +50ms
改善: プロビジョニング済み同時実行数を使用し、コールドスタートを回避。
第2部: API Gateway セキュリティ
11. 認証方式の比較 — IAM vs Cognito vs Lambda オーソライザー
11.1 IAM 認証
API が AWS IAM ユーザー・ロール でのみアクセス可能な場合(内部API)。
import boto3
import requests
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
# Python クライアント例
session = boto3.Session()
credentials = session.get_credentials()
url = 'https://api.example.com/resource'
request = AWSRequest(method='GET', url=url)
SigV4Auth(credentials, 'execute-api', 'ap-northeast-1').add_auth(request)
response = requests.get(url, headers=dict(request.headers))
メリット:
- AWS内の統一認証
- CloudTrail で追跡可能
- 複数の認証を組み合わせ不要
デメリット:
- モバイルアプリ・SPA には不向き(認証情報の管理が複雑)
- 署名プロセスが必須
11.2 Amazon Cognito オーソライザー
ユーザープール + API Gateway を組み合わせたOpenID Connect方式。
# API Gateway オーソライザー設定
Authorizers:
- Type: COGNITO_USER_POOLS
ProviderArn: arn:aws:cognito-idp:ap-northeast-1:123456789012:userpool/ap-northeast-1_aBcDeFgHi
IdentitySource: method.request.header.Authorization
AuthorizerResultTtlInSeconds: 300
フロントエンド側(SPA):
// Amazon Cognito SDK
const userPool = new AmazonCognitoIdentityServiceProvider.CognitoUserPool({
UserPoolId: 'ap-northeast-1_aBcDeFgHi',
ClientId: 'abc123def456'
});
const user = userPool.getCurrentUser();
user.getSession((err, session) => {
const idToken = session.getIdToken().getJwtToken();
fetch('https://api.example.com/users', {
headers: {
'Authorization': idToken
}
});
});
API Gateway は JWT を自動的に検証する(署名、有効期限)。
メリット:
- ユーザー管理がビルトイン
- JWT トークン自動検証
- MFA, ソーシャルログイン対応
デメリット:
- Lambda オーソライザーより複雑なカスタマイズ不可
- ユーザープール利用料(月 $0.5 /100アクティブユーザー)
11.3 Lambda オーソライザー(最も柔軟)
カスタムロジックで認証を実装。3つのタイプ:
TOKEN 型(推奨):
def lambda_handler(event, context):
"""
event['authorizationToken'] = 'Bearer <token>'
event['methodArn'] = 'arn:aws:execute-api:ap-northeast-1:123456789012:abcdef123/prod/GET/users'
"""
token = event['authorizationToken'].split(' ')[1]
try:
# カスタム認証ロジック
decoded = verify_token(token) # JWT検証、または外部API呼び出し
principal_id = decoded['sub']
policy = build_policy('Allow', event['methodArn'])
policy['context'] = {
'userId': principal_id,
'email': decoded['email'],
'role': decoded['role']
}
return policy
except Exception as e:
raise Exception('Unauthorized')
def build_policy(effect, resource):
return {
'principalId': 'user', # APIアクセス元の識別子
'policyDocument': {
'Version': '2012-10-17',
'Statement': [
{
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': resource
}
]
}
}
REQUEST 型(ヘッダー・クエリパラメータの詳細検査):
def lambda_handler(event, context):
"""
request形式でくる
- event['headers']
- event['queryStringParameters']
- event['requestContext']
"""
auth_header = event['headers'].get('Authorization', '')
# IP制限を含める
source_ip = event['requestContext']['identity']['sourceIp']
if source_ip not in WHITELIST_IPS:
return deny_policy()
return allow_policy()
キャッシング:
policy = {
'principalId': 'user-123',
'policyDocument': {...},
'context': {
'userId': 'user-123'
},
'usageIdentifierKey': 'api-key-123' # キャッシュキー
}
# このキャッシュキーが同じ場合、5分間このポリシーが再利用される
メリット:
- 完全なカスタマイズ(多要素認証、外部API連携など)
- WebSocket認証対応
デメリット:
- Lambda実行時間が認証遅延に
- オーソライザー自体のエラー処理が複雑
比較表
| 方式 | 認証スピード | カスタマイズ | 用途 |
|---|---|---|---|
| IAM | 高速 | 低 | AWS 内部API |
| Cognito | 中 | 中 | モバイル・SPA、ユーザー管理が必要 |
| Lambda | 遅い(+50-200ms) | 高 | カスタム認証ロジック、レガシーシステム連携 |
12. リソースポリシー — IP制限、VPC エンドポイント制限
IP ベースのアクセス制限
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "execute-api:/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": [
"203.0.113.0/24", // 許可するIP
"198.51.100.0/24"
]
},
"Bool": {
"aws:SecureTransport": "false" // HTTPS強制
}
}
}
]
}
VPC エンドポイント制限(プライベート API)
内部ユーザーのみがアクセス可能なAPI(インターネット非公開):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "execute-api:/*",
"Condition": {
"StringNotEquals": {
"aws:SourceVpc": "vpc-12345678"
}
}
}
]
}
API Gateway の設定:
aws apigateway update-rest-api \
--rest-api-id abcdef123 \
--patch-operations op=replace,path=/endpointConfiguration/types/0,value=PRIVATE
これにより、API はVPCエンドポイント経由のみアクセス可能になる。
13. WAF 連携(REST API のみ)
HTTP API では WAF 非対応
| API タイプ | WAF | 理由 |
|---|---|---|
| REST API | サポート | CloudFront との統合 |
| HTTP API | 非サポート | 設計が簡潔(オーバーヘッド最小化) |
WAF ルール設定
import boto3
waf = boto3.client('wafv2')
# Web ACL 作成
response = waf.create_web_acl(
Name='api-protection',
Scope='REGIONAL',
DefaultAction={
'Allow': {}
},
Rules=[
{
'Name': 'SQLiProtection',
'Priority': 0,
'Statement': {
'SqliMatchStatement': {
'FieldToMatch': {
'Body': {}
},
'TextTransformations': [
{'Priority': 0, 'Type': 'URL_DECODE'},
{'Priority': 1, 'Type': 'HTML_ENTITY_DECODE'}
]
}
},
'Action': {
'Block': {
'CustomResponse': {
'ResponseCode': 403,
'CustomResponseBodyKey': 'blocked'
}
}
},
'VisibilityConfig': {
'SampledRequestsEnabled': True,
'CloudWatchMetricsEnabled': True,
'MetricName': 'SQLiProtectionMetrics'
}
}
],
VisibilityConfig={
'SampledRequestsEnabled': True,
'CloudWatchMetricsEnabled': True,
'MetricName': 'api-protection-metrics'
}
)
# REST API に関連付け
apigateway = boto3.client('apigateway')
apigateway.associate_web_acl(
WebAclArn=response['Summary']['Arn'],
ResourceArn=f'arn:aws:apigateway:ap-northeast-1::/restapis/abcdef123/stages/prod'
)
14. 使用量プランと API キー — スロットリングとクォータ
API キーはあくまで「ロードバランス」、認証ではない
# 危険な勘違い
# "API Key があれば誰でも呼べる" と考えるのは間違い
# 正しい理解
# API Key は複数のクライアントのレート制限を分離するだけ
使用量プランの設定
# 使用量プラン作成
aws apigateway create-usage-plan \
--name enterprise-plan \
--api-stages apiId=abcdef123,stage=prod \
--throttle burstLimit=5000,rateLimit=2000 \
--quota limit=1000000,period=DAY
# APIキー作成
key=$(aws apigateway create-api-key \
--name client-api-key \
--enabled \
--query 'id' \
--output text)
# 使用量プランにAPIキーをリンク
aws apigateway create-usage-plan-key \
--usage-plan-id <usage-plan-id> \
--key-id $key \
--key-type API_KEY
スロットリング戦略
| シナリオ | スロットル | 使用量プラン | 目的 |
|---|---|---|---|
| パブリックAPI | 低(100 req/s) | 基本, プロフェッショナル | DDoS対策 |
| 内部API | 高(10,000 req/s) | エンタープライズ | バースト許容 |
| リソース集約的 | 非常に低(10 req/s) | スターター | リソース保護 |
15. mTLS(相互TLS認証)— 両者が証明書で認証
mTLS の必要性
通常のTLS(HTTPS):
- サーバー証明書をクライアントが検証(サーバーの真正性確保)
- クライアント側は認証なし
mTLS:
- クライアント証明書 をサーバーが検証
- B2B API で必須(金融機関、医療機関など)
API Gateway での mTLS 設定
# クライアント証明書を作成(自己署名例)
openssl req -x509 -newkey rsa:4096 -keyout client-key.pem -out client-cert.pem -days 365 -nodes
# API Gateway で trust store を作成
aws apigateway create-domain-name \
--domain-name api.example.com \
--certificate-arn arn:aws:acm:... \
--mutual-tls-authentication \
--truststore-uri s3://my-bucket/truststore.pem \
--truststore-version 1
truststore.pem(許可するクライアント証明書のCA):
-----BEGIN CERTIFICATE-----
MIIC...(クライアントCAの証明書)...
-----END CERTIFICATE-----
クライアント側の実装
import requests
from requests.auth import HTTPCertAuth
response = requests.get(
'https://api.example.com/secure',
cert=('client-cert.pem', 'client-key.pem'),
verify='ca-bundle.crt' # サーバーCA検証
)
16. プライベート API — VPC エンドポイント経由のみ
前述の「VPC エンドポイント制限」を実装したAPI。
アーキテクチャ
┌─────────────┐
│ VPC内の │
│ アプリケーション├──┐
└─────────────┘ │
│
┌───▼───┐
│VPC EP │
│(API) │
└───┬───┘
│
┌───────▼─────────┐
│ API Gateway │
│ (PRIVATE type) │
└───────┬─────────┘
│
┌───────▼─────────┐
│ Lambda │
│ (VPC接続) │
└─────────────────┘
リソースポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "execute-api:/*",
"Condition": {
"StringEquals": {
"aws:sourceVpce": "vpce-0123456789abcdef0"
}
}
},
{
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "execute-api:/*",
"Condition": {
"StringNotEquals": {
"aws:sourceVpce": "vpce-0123456789abcdef0"
}
}
}
]
}
17. CloudWatch ログ — 実行ログ vs アクセスログ
実行ログ(Execution Logs)
API Gateway が各リクエスト・レスポンスの詳細をログ出力。デバッグ向け、本番では無効推奨。
{
"extendedRequestId": "abcd1234==",
"requestTime": "27/Apr/2026:10:00:00 +0000",
"httpMethod": "POST",
"resourcePath": "/users",
"status": 200,
"protocol": "HTTP/1.1",
"responseLength": 256,
"error": null,
"integrationErrorMessage": null,
"authorizerError": null,
"authorizerIntegrationStatus": 200,
"authorizer": {
"principalId": "user-123",
"integrationLatency": 45
},
"integration": {
"requestTemplateSelectionExpression": "$default",
"httpMethod": "POST",
"type": "AWS_PROXY",
"status": 200,
"latency": 120,
"responseParameters": {}
},
"domainName": "api.example.com",
"domainPrefix": "api",
"stage": "prod",
"connectedTime": 1682000400000,
"ttl": 3600
}
アクセスログ(Access Logs)
本番環境で必須。リクエスト概要のみ(実行ログより軽量)。
# アクセスログフォーマット定義
$context.requestId
$context.extendedRequestId
$context.identity.sourceIp
$context.identity.userAgent
$context.requestTime
$context.httpMethod
$context.resourcePath
$context.status
$context.protocol
$context.responseLength
$context.error
$context.authorizer.principalId
CloudWatch Logs IAM ロール:
{
"Effect": "Allow",
"Action": [
"logs:CreateLogDelivery",
"logs:GetLogDelivery",
"logs:UpdateLogDelivery",
"logs:DeleteLogDelivery",
"logs:ListLogDeliveries",
"logs:PutResourcePolicy",
"logs:DescribeResourcePolicies",
"logs:DescribeLogGroups"
],
"Resource": "*"
}
18. CORS 設定のセキュリティ — ワイルドカード回避
危険な設定
# 全てのオリジンを許可(セキュリティ上NG)
CorsConfiguration:
AllowedOrigins:
- "*"
AllowedMethods:
- GET
- POST
- PUT
- DELETE
AllowedHeaders:
- "*"
MaxAge: 86400
安全な設定
CorsConfiguration:
AllowedOrigins:
- "https://app.example.com" # 本番環境
- "https://staging.example.com" # ステージング
- "http://localhost:3000" # 開発環境(HTTPOKの根拠あり)
AllowedMethods:
- GET
- POST
AllowedHeaders:
- Content-Type
- Authorization
- X-Amz-Date
ExposedHeaders:
- X-Request-Id
MaxAge: 3600 # 1時間(短め)
AllowCredentials: true # Cookie送信許可
OPTIONS プリフライトリクエスト
// ブラウザが自動的に送信(開発者が意識する必要はない)
// CORS対応しないAPIはここで失敗
// リクエスト
OPTIONS /users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
// レスポンス
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 3600
19. REST API vs HTTP API — セキュリティ機能の差異
| 機能 | REST API | HTTP API | 備考 |
|---|---|---|---|
| IAM認証 | ✅ | ✅ | リソースベースポリシーも同じ |
| Cognito認証 | ✅ | ✅ | |
| Lambda オーソライザー | ✅ | ✅ | |
| mTLS | ✅ | ✅(v2.0のみ) | |
| WAF連携 | ✅ | ❌ | REST限定 |
| リソースベースポリシー | ✅ | ✅ | |
| VPCエンドポイント | ✅ | ✅ | |
| 実行ログ/アクセスログ | ✅ | ✅ |
選定基準:
- REST API: エンタープライズ、複雑な認可、WAF必須
- HTTP API: スタートアップ、シンプルなAPI、コスト最適化重視
20. API Gateway の制限 — できないこと
APIキーは認証ではない
APIキーは以下を提供しない:
- ユーザーの身元確認
- リクエストの完全性検証
- 暗号化
# 危険な実装
api_key = request.headers.get('x-api-key')
if api_key == 'hardcoded-secret':
return allow_request()
# 正しい実装
api_key = request.headers.get('x-api-key')
key_metadata = validate_api_key_in_database(api_key) # DBから検証
if key_metadata['is_active'] and key_metadata['expires_at'] > now():
return allow_request()
WAF は REST API のみ
HTTP API を使用している場合、WAF 保護はCloudFront + Lambda@Edge で実装:
# CloudFront + Lambda@Edge でのWAF代替
def lambda_handler(event, context):
request = event['Records'][0]['cf']['request']
headers = request['headers']
# SQL injection 検出
if 'sql' in request['querystring'].lower():
return {
'status': 403,
'statusDescription': 'Forbidden'
}
return request
第3部: 連携パターン
21. パターン 1: API Gateway + Lambda + Cognito
フロー図
User
↓
[SPA (React)]
↓ (1) login: username/password
[Cognito Auth]
↓ (2) returns: ID Token, Access Token
[SPA stores tokens]
↓ (3) API request with ID Token
[API Gateway - Cognito Authorizer]
↓ (4) JWT 検証, ユーザーID抽出
[Lambda Function]
↓ (5) DynamoDB or RDS
[Database]
コード例
フロント(React + Amplify):
import { Auth } from 'aws-amplify';
async function loginUser(username: string, password: string) {
try {
const user = await Auth.signIn(username, password);
const session = await Auth.currentSession();
const idToken = session.idToken.jwtToken;
// API呼び出し
const response = await fetch('https://api.example.com/profile', {
headers: {
'Authorization': idToken
}
});
} catch (error) {
console.error('Login failed:', error);
}
}
Lambda実行ロール:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/users"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/*"
}
]
}
22. パターン 2: API Gateway + VPCリンク + プライベートバックエンド
アーキテクチャ(オンプレミス統合)
Terraform実装例
# VPCリンク作成
resource "aws_apigateway_vpc_link" "private_backend" {
name = "private-backend-link"
description = "VPC Link to Private NLB"
target_arns = [aws_lb.private_nlb.arn]
tags = {
Environment = "production"
}
}
# API Gateway統合
resource "aws_apigateway_integration" "backend" {
rest_api_id = aws_apigateway_rest_api.example.id
resource_id = aws_apigateway_resource.proxy.id
http_method = "ANY"
type = "HTTP_PROXY"
integration_http_method = "POST"
uri = "https://${aws_lb.private_nlb.dns_name}"
vpc_link_id = aws_apigateway_vpc_link.private_backend.id
request_parameters = {
"integration.request.header.Authorization" = "method.request.header.Authorization"
}
}
# NLB(プライベート)
resource "aws_lb" "private_nlb" {
name = "private-backend-nlb"
internal = true
load_balancer_type = "network"
subnets = [aws_subnet.private_a.id, aws_subnet.private_b.id]
enable_deletion_protection = true
tags = {
Name = "private-backend-nlb"
}
}
# NLB リスナー(TLS)
resource "aws_lb_listener" "tls" {
load_balancer_arn = aws_lb.private_nlb.arn
port = 443
protocol = "TLS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.nlb.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.backend.arn
}
}
# セキュリティグループ(NLBインバウンド)
resource "aws_security_group_rule" "nlb_from_apigw" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
source_security_group_id = aws_security_group.api_gateway.id
security_group_id = aws_security_group.nlb.id
}
23. パターン 3: Lambda + Secrets Manager + RDS Proxy
RDS Proxy の役割
RDSへの接続プール管理。Lambda が毎回新規接続を作成する代わりに、Proxyが接続を再利用。
Lambda 1 ──┐
Lambda 2 ──┼─→ RDS Proxy ──→ RDS (1-5 接続)
Lambda 3 ──┘
(数百 接続)
利点:
- "Too Many Connections" エラー回避
- レイテンシー削減(接続確立不要)
- 接続キープアライブ(アイドル切断対策)
Secrets Manager 統合
import boto3
import pymysql
from botocore.exceptions import ClientError
secrets_client = boto3.client('secretsmanager')
def get_db_connection():
try:
# Secrets Manager から認証情報取得
secret_response = secrets_client.get_secret_value(
SecretId='prod/rds/mysql'
)
secret = json.loads(secret_response['SecretString'])
# RDS Proxy 経由で接続(Secrets Manager 認証)
connection = pymysql.connect(
host=f"{secret['proxy_endpoint']}:3306", # RDS Proxy endpoint
user=secret['username'],
password=secret['password'],
database=secret['dbname'],
ssl_verify_cert=True,
ssl_verify_identity=True # mTLS
)
return connection
except ClientError as e:
print(f"Failed to retrieve secret: {e}")
raise
def lambda_handler(event, context):
conn = None
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM users LIMIT 1")
result = cursor.fetchone()
return {
'statusCode': 200,
'body': json.dumps({'user': result})
}
finally:
if conn:
conn.close()
RDS Proxy IAM 認証
# パスワード認証ではなくIAM認証を使用(セキュリティ向上)
import boto3
from botocore.exceptions import ClientError
rds = boto3.client('rds')
def get_db_token():
"""RDS Proxy IAM認証トークン取得(15分有効)"""
token = rds.generate_db_auth_token(
DBHostname='proxy.123456789012.ap-northeast-1.rds.amazonaws.com',
Port=3306,
DBUser='iam_user', # DB内で CREATE USER 'iam_user'@'%' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'
Region='ap-northeast-1'
)
return token
# パスワード代わりにトークンを使用
connection = pymysql.connect(
host='proxy.123456789012.ap-northeast-1.rds.amazonaws.com',
user='iam_user',
password=get_db_token(),
database='mydb',
ssl={'ssl': True}
)
Lambda 実行ロール:
{
"Effect": "Allow",
"Action": [
"rds-db:connect"
],
"Resource": "arn:aws:rds:ap-northeast-1:123456789012:db:my-database/iam_user"
}
試験で狙われるポイント(最終まとめ)
Lambda
-
実行ロール vs リソースベースポリシー
- 実行ロール: Lambda「ができること」
- リソースベースポリシー: 「誰がLambdaを呼べるか」
- クロスアカウント = リソースベースポリシー
-
VPC接続 Lambda
- NAT Gateway必須(インターネット接続時)
- VPC エンドポイント で NAT コスト削減
- セキュリティグループ設定忘れず
-
環境変数暗号化
- デフォルト = AWS管理キー(無料、制御不可)
- カスタムKMS = ユーザー管理(有料、制御可)
- 機密情報は Secrets Manager 推奨
-
予約済み同時実行数
- DDoS対策の一部
- Lambda全体ではなく関数ごと設定
- ThrottlingException で拒否
-
実行時間15分上限
- 長時間処理はStep Functions
- バッチはSQS+ポーリング
API Gateway
-
認証方式3つ
- IAM: 内部API
- Cognito: ユーザー管理必須
- Lambda: カスタムロジック(遅い)
-
リソースポリシー
- IP制限、VPC制限、クロスアカウント
- ワイルドカード避ける
-
WAF は REST API のみ
- HTTP API は CloudFront + Lambda@Edge
-
APIキーは認証ではない
- スロットリング・クォータのみ
- 別途認証が必須
-
mTLS(相互TLS)
- クライアント証明書でサーバーが検証
- B2B API で必須
-
プライベートAPI
- VPCエンドポイント経由のみ
- リソースポリシーで VPC制限
-
CORS設定
- ワイルドカード許可は NG
- AllowedOrigins を明示的に指定
- MaxAge は短め(3600秒程度)
-
ログ
- 実行ログ: デバッグ向け(本番で無効)
- アクセスログ: 本番必須
参考資料
- AWS Lambda Developer Guide: https://docs.aws.amazon.com/lambda/
- API Gateway Developer Guide: https://docs.aws.amazon.com/apigateway/
- AWS SCS-C03 Exam Guide
- AWS Security Best Practices Whitepaper
次章予告: EC2・ECS・EKS のセキュリティ(IAM ロール、セキュリティグループ、IMDS、Pod Security Policy)