TerraformでAmazon API Gatewayを構築する(アクセスログ詳細編)

# はじめに
API Gateway＋Terraform記事第3弾。
[前回の記事でカスタムアクセスログについてさらっと触れた](https://qiita.com/neruneruo/items/a568cd3f0542cde8c967#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%AE%E3%83%AD%E3%82%B0%E8%A8%AD%E5%AE%9A)が、今回はログフォーマット関連でもう少し掘り下げてみる。

# アクセスログに出せるもの
以下のドキュメントを参照しよう。

【AWS公式】[API Gateway での REST API の CloudWatch ログ記録の設定](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-logging.html)
【AWS公式】[API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#context-variable-reference%3Fcmpid=docs_apigateway_console)

API Gatewayのドキュメントは結構散らかっていて、後者のページではあたかも色々なものをログに出せそうに見えるが、実際には前者のページに記載の通り、$context 変数のものだけしか出力できない。

例えば、ヘッダやパスパラメータやBodyで指定されるようなパラメータについては、出力できないため、あくまでもHTTPサーバとしてのアクセスログといった位置づけだろう。
※統合リクエストでAWSサービスとも統合できるのに、アプリケーションサーバ的なログが出せないのはちょっといただけないのだが……

# ログ出力の方法
前回の記事にも書いた通り、カスタムアクセスログについては、Terraformの ```aws_api_gateway_stage``` で ```access_log_settings``` を指定してあげることで出力可能だ。CloudWatch Logsの作成や、ログ書き込みのためのロール設定等は前回を参照してほしい。

なお、ログフォーマットには、CLF、JSON、XML、CSVが設定可能だが、CloudWatch Logs Insightsで使えることを考えると、JSONが良いだろうということで、今回はJSONで書いている(後でJSON→CSV変換であれば色々な手段で簡単にできるし)。

今回の記事では、簡単な Mock 統合を作成し、そこの prod ステージに対して以下の設定をした。

```HCL
resource "aws_api_gateway_stage" "prod" {
  stage_name    = "prod"
  rest_api_id   = aws_api_gateway_rest_api.my.id
  deployment_id = aws_api_gateway_deployment.dev_to_prod.id

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.apigateway_accesslog.arn
    format          = replace(file("${path.module}/logformat.json"), "\n", "")
  }
}
```

format には直接書き込んでも良いが、色々と書きだしたい場合はファイルを分けた方が良いだろう。
replace() 関数を入れているのは、JSONを複数行にするとエラーになるためだ(API Gatewayの仕様)。
ファイルに '\' を入れても良いのだが、可読性を上げるために、ファイルは普通のJSON形式にして、Terraformに食わせるときに変換している。

# 実際の出力
実際に設定した際の出力を確認していってみよう。
なお、全部一気にまとめて出力しようとしたら、ログフォーマット設定は最大で 3,000Byte までしか指定できないらしいので、まとめて出力することはできないようだ。実運用する際は、必要なものをピックアップしよう。

なお、値が ```"-"```のものは、今回の単純な Mock のAPIでは使っていない機能によるものなので、実際には利用シーンに合わせた値が設定されるはずである。

## 例に記載されているもの
### フォーマットでの指定
```JSON
{
  "requestId": "$context.requestId",
  "ip": "$context.identity.sourceIp",
  "caller": "$context.identity.caller",
  "user": "$context.identity.user",
  "requestTime": "$context.requestTime",
  "httpMethod": "$context.httpMethod",
  "resourcePath": "$context.resourcePath",
  "status": "$context.status",
  "protocol": "$context.protocol",
  "responseLength": "$context.responseLength"
}
```

### ログ出力内容
```JSON
{
  "requestId": "feee42ea-1b14-4f99-ad2a-5c0e17b01d5c",
  "ip": "xxx.xxx.xxx.xxx",
  "caller": "-",
  "user": "-",
  "requestTime": "26/Sep/2020:13:10:33 +0000",
  "httpMethod": "GET",
  "resourcePath": "/employee",
  "status": "200",
  "protocol": "HTTP/1.1",
  "responseLength": "0",
```

## 共通的なコンテキスト変数
### フォーマットでの指定
```JSON
{
  "account_id": "$context.accountId",
  "api_id": "$context.apiId",
  "authorizer_claims_property": "$context.authorizer.claims.property",
  "authorizer_principal_id": "$context.authorizer.principalId",
  "authorizer_property": "$context.authorizer.property",
  "aws_endpoint_request_id": "$context.awsEndpointRequestId",
  "domain_name": "$context.domainName",
  "domain_prefix": "$context.domainPrefix",
  "error_message": "$context.error.message",
  "error_message_string": "$context.error.messageString",
  "error_response_type": "$context.error.responseType",
  "error_validation_error_string": "$context.error.validationErrorString",
  "extended_request_id": "$context.extendedRequestId",
  "identity_account_id": "$context.identity.accountId",
  "identity_api_key": "$context.identity.apiKey",
  "identity_api_key_id": "$context.identity.apiKeyId",
  "identity_cognito_authentication_provider": "$context.identity.cognitoAuthenticationProvider",
  "identity_cognito_authentication_type": "$context.identity.cognitoAuthenticationType",
  "identity_cognito_identity_id": "$context.identity.cognitoIdentityId",
  "identity_cognito_identity_pool_id": "$context.identity.cognitoIdentityPoolId",
  "identity_principal_org_id": "$context.identity.principalOrgId",
  "identity_client_cert_client_cert_pem": "$context.identity.clientCert.clientCertPem",
  "identity_client_cert_subject_d_n": "$context.identity.clientCert.subjectDN",
  "identity_client_cert_issuer_d_n": "$context.identity.clientCert.issuerDN",
  "identity_client_cert_serial_number": "$context.identity.clientCert.serialNumber",
  "identity_client_cert_validity_not_before": "$context.identity.clientCert.validity.notBefore",
  "identity_client_cert_validity_not_after": "$context.identity.clientCert.validity.notAfter",
  "identity_user_agent": "$context.identity.userAgent",
  "identity_user_arn": "$context.identity.userArn",
  "path": "$context.path",
  "request_override_header_header_name": "$context.requestOverride.header.header_name",
  "request_override_path_path_name": "$context.requestOverride.path.path_name",
  "request_override_querystring_querystring_name": "$context.requestOverride.querystring.querystring_name",
  "response_override_header_header_name": "$context.responseOverride.header.header_name",
  "response_override_status": "$context.responseOverride.status",
  "request_time_epoch": "$context.requestTimeEpoch",
  "resource_id": "$context.resourceId",
  "stage": "$context.stage",
  "waf_response_code": "$context.wafResponseCode",
  "webacl_arn": "$context.webaclArn",
  "xray_trace_id": "$context.xrayTraceId"
}
```

### ログ出力内容
```JSON
{
    "account_id": "xxxxxxxxxxxx",
    "api_id": "xxxxxxxxxx",
    "authorizer_claims_property": "-",
    "authorizer_principal_id": "-",
    "authorizer_property": "-",
    "aws_endpoint_request_id": "-",
    "domain_name": "xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com",
    "domain_prefix": "xxxxxxxxxx",
    "error_message": "-",
    "error_message_string": "-",
    "error_response_type": "-",
    "error_validation_error_string": "-",
    "extended_request_id": "TeaXkGWhNjMFVjw=",
    "identity_account_id": "-",
    "identity_api_key": "-",
    "identity_api_key_id": "-",
    "identity_cognito_authentication_provider": "-",
    "identity_cognito_authentication_type": "-",
    "identity_cognito_identity_id": "-",
    "identity_cognito_identity_pool_id": "-",
    "identity_principal_org_id": "-",
    "identity_client_cert_client_cert_pem": "-",
    "identity_client_cert_subject_d_n": "-",
    "identity_client_cert_issuer_d_n": "-",
    "identity_client_cert_serial_number": "-",
    "identity_client_cert_validity_not_before": "-",
    "identity_client_cert_validity_not_after": "-",
    "identity_user_agent": "curl/7.61.1",
    "identity_user_arn": "-",
    "path": "/prod/employee",
    "request_override_header_header_name": "-",
    "request_override_path_path_name": "-",
    "request_override_querystring_querystring_name": "-",
    "response_override_header_header_name": "-",
    "response_override_status": "-",
    "request_time_epoch": "1601126133567",
    "resource_id": "xxxxxx",
    "stage": "prod",
    "waf_response_code": "-",
    "webacl_arn": "-",
    "xray_trace_id": "-"
}
```

## アクセスログのみのコンテキスト変数
### フォーマットでの指定
```JSON
{
  "authorize_error": "$context.authorize.error",
  "authorize_latency": "$context.authorize.latency",
  "authorize_status": "$context.authorize.status",
  "authorizer_error": "$context.authorizer.error",
  "authorizer_integration_latency": "$context.authorizer.integrationLatency",
  "authorizer_integration_status": "$context.authorizer.integrationStatus",
  "authorizer_latency": "$context.authorizer.latency",
  "authorizer_request_id": "$context.authorizer.requestId",
  "authorizer_status": "$context.authorizer.status",
  "authenticate_error": "$context.authenticate.error",
  "authenticate_latency": "$context.authenticate.latency",
  "authenticate_status": "$context.authenticate.status",
  "integration_error": "$context.integration.error",
  "integration_integration_status": "$context.integration.integrationStatus",
  "integration_latency1": "$context.integration.latency",
  "integration_request_id": "$context.integration.requestId",
  "integration_status1": "$context.integration.status",
  "integration_error_message": "$context.integrationErrorMessage",
  "integration_latency2": "$context.integrationLatency",
  "integration_status2": "$context.integrationStatus",
  "response_latency": "$context.responseLatency",
  "waf_error": "$context.waf.error",
  "waf_latency": "$context.waf.latency",
  "waf_status": "$context.waf.status"
}
```

### ログ出力内容
```JSON
{
    "authorize_error": "-",
    "authorize_latency": "-",
    "authorize_status": "-",
    "authorizer_error": "-",
    "authorizer_integration_latency": "-",
    "authorizer_integration_status": "-",
    "authorizer_latency": "-",
    "authorizer_request_id": "-",
    "authorizer_status": "-",
    "authenticate_error": "-",
    "authenticate_latency": "-",
    "authenticate_status": "-",
    "integration_error": "-",
    "integration_integration_status": "200",
    "integration_latency1": "0",
    "integration_request_id": "-",
    "integration_status1": "-",
    "integration_error_message": "-",
    "integration_latency2": "0",
    "integration_status2": "200",
    "response_latency": "3",
    "waf_error": "-",
    "waf_latency": "-",
    "waf_status": "-"
}
```