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

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

```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

```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のテンプレートファイル

このファイルの中身でグローバルなのかプライベートなのかを切り替えます。

```yaml
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ではどう見えるのかも調べています。

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

- [API GatewayをVPC内のプライベートアクセスのみに限定](https://suzuki-navi.hatenablog.com/entry/2020/12/21/081530) (12/21)
- [CloudFormationでAPI Gatewayを変更しても反映されない](https://suzuki-navi.hatenablog.com/entry/2020/12/17/214538) (12/17)
- [AWS::ApiGateway::Stage と AWS::ApiGateway::Deployment の違い](https://suzuki-navi.hatenablog.com/entry/2020/12/16/230323) (12/16)