9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

BeeXAdvent Calendar 2020

Day 22

API GatewayをVPC内のプライベートに変更するとHTTP2非対応になる

Last updated at Posted at 2020-12-22

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の記事が続いています。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?