API GatewayがHTTP/2のときとHTTP/1.1のときがあり、どういうことだろうと思って条件の切り分けをしてました。
結論
タイトルの通りですが、API GatewayがグローバルにあるときはHTTP/2に対応し、VPCの中に入れてプライベートAPIにするとHTTP/1.1のみになるようでした。
ドキュメントによると、Edge-optimizedのAPI Gatewayは内部でCloudFrontが使われるが、ほかは違うようです。
API Gateway自身にはHTTP/2の機能がなく、Edge-optimizedにしたときはCloudFrontがHTTP/2を提供できるが、VPCの中に入れてしまったらCloudFrontがないのでHTTP/1.1になるのかもしれません。
HTTP/2とHTTP/1.1の両対応時のサーバサイド側実装時の注意
リクエストヘッダの大文字小文字の扱いがHTTP/2とHTTP/1.1とで違います。
HTTP/1.1のときはリクエストヘッダがそのまま来るのに対し、HTTP/2ではブラウザがリクエストヘッダ名を小文字に変換してしまいます。HTTP/2ではヘッダ名に大文字が認められていないためです。
API Gatewayの後ろにLambdaを置き、Lambdaがリクエストヘッダを読み取るときには大文字小文字の違いを無視して読み取ることが必要です。
ソースコードとコマンドの記録
調べるために使ったソースコードをここに残しておきます。
Lambdaのコード
Lambdaが受け取ったリクエストヘッダをそのままJSON形式でレスポンスしています。
大文字小文字の違いを無視して処理するには先に大文字小文字どちらかに寄せてしまうのが楽なので、小文字に寄せるサンプルコードも入っています。ここでは検証目的のためコメントアウト状態で実行しました。
PythonとJavaScriptの2通り書きました。
Python
import json
def lambda_handler(event, context):
# リクエストヘッダの項目名を小文字に寄せるには以下のコード
#event["headers"] = dict(map(lambda kv: (kv[0].lower(), kv[1]), event["headers"].items()))
return {
"statusCode": 200,
"body": json.dumps(event["headers"], indent = " ")
}
JavaScript
exports.lambda_handler = async function(event, context) {
// リクエストヘッダの項目名を小文字に寄せるには以下のコード
//event.headers = Object.fromEntries(
// Object.entries(event.headers).map(([k, v]) => [k.toLowerCase(), v])
//);
return {
"statusCode": 200,
"body": JSON.stringify(event.headers, null, " "),
};
}
CloudFormationのテンプレートファイル
このファイルの中身でグローバルなのかプライベートなのかを切り替えます。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
SubnetId:
Type: String
SecurityGroupId:
Type: String
ApiGatewayVpce:
Type: String
Resources:
# Lambda
Http2TestFunction:
Type: AWS::Lambda::Function
Properties:
Code: api
Handler: app.lambda_handler
FunctionName: http2-test
Role: !GetAtt Http2TestFunctionRole.Arn
# 言語によって以下を書き換える
Runtime: python3.8
#Runtime: nodejs12.x
Timeout: 30
# プライベートAPIでは以下を有効化
#VpcConfig:
# SubnetIds:
# - !Ref SubnetId
# SecurityGroupIds:
# - !Ref SecurityGroupId
Http2TestPermissionProd:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
FunctionName: !Ref Http2TestFunction
SourceArn: !Sub
- arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/${__Method__}/test
- __Stage__: "*"
__ApiId__: !Ref Http2TestApiGateway
__Method__: "*"
# Role
Http2TestFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole # ログ書き込み用
# プライベートAPIでは以下を有効化
#- arn:aws:iam::aws:policy/service-role/AWSLambdaENIManagementAccess # VPC内実行用
# API Gateway
Http2TestApiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
Name: http2-test
Body:
info:
version: 1.0
title: !Ref AWS::StackName
paths:
"/test":
get:
x-amazon-apigateway-integration:
httpMethod: POST
type: aws_proxy
uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Http2TestFunction.Arn}/invocations
swagger: 2.0
# プライベートAPIでは以下を有効化
#EndpointConfiguration:
# Types:
# - PRIVATE
Policy:
Version: 2012-10-17
Statement:
# プライベートAPIでは以下を有効化
#- Effect: Deny
# Principal: "*"
# Action: execute-api:Invoke
# Resource: "execute-api:/*"
# Condition:
# StringNotEquals:
# "aws:sourceVpce": !Ref ApiGatewayVpce
- Effect: Allow
Principal: "*"
Action: execute-api:Invoke
Resource: "execute-api:/*"
Http2TestApiGatewayProdStage:
Type: AWS::ApiGateway::Stage
Properties:
DeploymentId: !Ref Http2TestApiGatewayDeployment
RestApiId: !Ref Http2TestApiGateway
StageName: Prod
Http2TestApiGatewayDeployment:
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId: !Ref Http2TestApiGateway
Outputs:
Api:
Value: !Sub "https://${Http2TestApiGateway}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
RestId:
Value: !Ref Http2TestApiGateway
デプロイのスクリプト
# 環境に合わせてパラメータを設定
profile=default
cf_s3_bucket=xxxx
subnetId=subnet-xxxxxxxx
securityGroupId=sg-xxxxxxxxxxxxxxxxx
apiGatewayVpce=vpce-xxxxxxxxxxxxxxxxx
aws --profile $profile cloudformation package \
--template-file template.yaml \
--s3-bucket $cf_s3_bucket \
--s3-prefix http2-test \
--output-template-file template-$profile-packaged.yaml
aws --profile $profile cloudformation deploy \
--template-file template-$profile-packaged.yaml \
--stack-name http2-test \
--capabilities CAPABILITY_IAM \
--no-fail-on-empty-changeset \
--parameter-overrides \
SubnetId=$subnetId \
SecurityGroupId=$securityGroupId \
ApiGatewayVpce=$apiGatewayVpce
api=$(aws --profile $profile cloudformation describe-stacks \
--stack-name http2-test |
jq -r '.Stacks[0].Outputs[0].OutputValue')
echo "url: ${api}test"
restid=$(aws --profile $profile cloudformation describe-stacks \
--stack-name http2-test |
jq -r '.Stacks[0].Outputs[1].OutputValue')
aws --profile $profile apigateway create-deployment --rest-api-id $restid --stage-name Prod
実行結果
以下のcurlコマンドでプロトコルを調べました。カスタムリクエストヘッダがLambdaではどう見えるのかも調べています。
$ curl -vsS -H 'X-Foo: HelloWorld' https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/test 2>&1 | less
まずは、API Gatewayがグローバルの場合。
> GET /Prod/test HTTP/2
> Host: xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com
> user-agent: curl/7.68.0
> accept: */*
> x-foo: HelloWorld
>
{ [5 bytes data]
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
} [5 bytes data]
< HTTP/2 200
< content-type: application/json
< content-length: 721
< date: Wed, 16 Dec 2020 11:23:47 GMT
< x-amzn-requestid: ...
< x-amz-apigw-id: ...
< x-amzn-trace-id: ...
< x-cache: Miss from cloudfront
< via: ...
< x-amz-cf-pop: ...
< x-amz-cf-id: ...
<
{ [721 bytes data]
* Connection #0 to host xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com left intact
{
"Accept": "*/*",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "JP",
"Host": "xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com",
"User-Agent": "curl/7.68.0",
"Via": "2.0 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": ...,
"X-Amzn-Trace-Id": ...,
"x-foo": "HelloWorld",
"X-Forwarded-For": ...,
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
}
HTTP/2になっています。Lambdaが受け取るリクエストヘッダの項目名は小文字になっています。(x-foo
の部分)
次はAPI Gatewayがプライベートの場合。
> GET /Prod/test HTTP/1.1
> Host: xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com
> User-Agent: curl/7.68.0
> Accept: */*
> X-Foo: HelloWorld
>
{ [5 bytes data]
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: Server
< Date: Wed, 16 Dec 2020 11:27:54 GMT
< Content-Type: application/json
< Content-Length: 380
< Connection: keep-alive
< x-amzn-RequestId: ...
< x-amz-apigw-id: ...
< X-Amzn-Trace-Id: ...
<
{ [5 bytes data]
* Connection #0 to host xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com left intact
{
"Accept": "*/*",
"Host": "xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com",
"User-Agent": "curl/7.68.0",
"x-amzn-cipher-suite": "ECDHE-RSA-AES128-GCM-SHA256",
"x-amzn-tls-version": "TLSv1.2",
"x-amzn-vpc-id": "vpc-xxxxxxxx",
"x-amzn-vpce-config": "1",
"x-amzn-vpce-id": "vpce-xxxxxxxxxxxxxxxxx",
"X-Foo": "HelloWorld",
"X-Forwarded-For": "172.31.xxx.xxx"
}
HTTP/1.1になっています。Lambdaが受け取るリクエストヘッダの項目名の大文字小文字は変換されていません。(X-Foo
の部分)
関連記事
最近API Gatewayの記事が続いています。