Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Apex(Terraform)로 API Gateway 구성하기 #2

이 글은 Apex(Terraform)로 API Gateway 구성하기 #1에서 이어진 글이다.


API Gateway 로깅

API Gateway가 안되는 이유를 보려면 API Gateway에서 발생한 오류를 로그로 남겨서 봐야 한다. API Gateway에는 오류를 보는 기능이 없으므로 확인하는 CloudWatch에 오류를 남길 수 있도록 설정해야 한다.

API Gateway의 클라우드워치 ARN 설정 화면

API Gateway에서는 리전 내 전체 설정에서 CloudWatch에 로그를 남길 수 있는 권한이 있는 role의 ARN을 설정해 놓고 각 API의 리소스나 메서드에서 이 role을 사용해서 로깅을 키거나 끌 수 있다. 이 설정은 특정 Lambda에 종속 된 것이 아니므로 별도의 파일 cloudwatch.tf로 분리했다.

// infrastructure/cloudwatch.tf

// cloudwatch를 사용할 policy
data "aws_iam_policy_document" "allow_log_to_cloudwatch" {
  statement {
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:DescribeLogGroups",
      "logs:DescribeLogStreams",
      "logs:PutLogEvents",
      "logs:GetLogEvents",
      "logs:FilterLogEvents"
    ]
    resources = [
      "*",
    ]
  }
}

resource "aws_iam_role_policy" "allow_log_to_cloudwatch" {
  name   = "allow_log_to_cloudwatch"
  role = "${aws_iam_role.api_gateway_cloudwatch.id}"
  policy = "${data.aws_iam_policy_document.allow_log_to_cloudwatch.json}"
}

// cloudwatch를 사용할 role
data "aws_iam_policy_document" "allow_log_to_cloudwatch_from_apigateway" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["apigateway.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "api_gateway_cloudwatch" {
  name = "api_gateway_cloudwatch"
  assume_role_policy = "${data.aws_iam_policy_document.allow_log_to_cloudwatch_from_apigateway.json}"
}

// 로깅을 위한 CloudWatch 설정
resource "aws_api_gateway_account" "api_gateway_account" {
  cloudwatch_role_arn = "${aws_iam_role.api_gateway_cloudwatch.arn}"
}

여기서는 IAM에서 policy를 하나 만들고 role을 하나 만들어서 policy에 연결한 후에 API Gateway에 설정한 것이다. aws_iam_policy_document.allow_log_to_cloudwatch에서 정책 규칙을 정의해서 aws_iam_role_policy로 새로운 policy를 만들고 aws_iam_policy_document.allow_log_to_cloudwatch_from_apigateway에서 role의 규칙을 만든 후 aws_iam_role에서 role을 만들고 이 role을 policy와 연결했다. 그리고 이렇게 생성한 role의 ARN은 aws_api_gateway_account에서 설정했다.

이를 적용해보자.

$ apex infra plan

+ aws_api_gateway_account.api_gateway_account
    cloudwatch_role_arn: "${aws_iam_role.api_gateway_cloudwatch.arn}"
    throttle_settings.#: "<computed>"

-/+ aws_api_gateway_deployment.status_api_test
    created_date:      "2017-05-07T17:22:22Z" => "<computed>"
    description:       "Deployed at 2017-05-07T17:22:22Z" => "Deployed at 2017-05-07T17:36:36Z"
    execution_arn:     "arn:aws:execute-api:ap-northeast-1:410000000000:1w03z7dzmi/test" => "<computed>"
    invoke_url:        "https://1w03z7dzmi.execute-api.ap-northeast-1.amazonaws.com/test" => "<computed>"
    rest_api_id:       "1w03z7dzmi" => "1w03z7dzmi"
    stage_description: "2017-05-07T17:22:22Z" => "2017-05-07T17:36:36Z" (forces new resource)
    stage_name:        "test" => "test"

+ aws_iam_role.api_gateway_cloudwatch
    arn:                "<computed>"
    assume_role_policy: "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Sid\": \"\",\n      \"Effect\": \"Allow\",\n      \"Action\": \"sts:AssumeRole\",\n      \"Principal\": {\n        \"Service\": \"apigateway.amazonaws.com\"\n      }\n    }\n  ]\n}"
    create_date:        "<computed>"
    name:               "api_gateway_cloudwatch"
    path:               "/"
    unique_id:          "<computed>"

+ aws_iam_role_policy.allow_log_to_cloudwatch
    name:   "allow_log_to_cloudwatch"
    policy: "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Sid\": \"\",\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"logs:PutLogEvents\",\n        \"logs:GetLogEvents\",\n        \"logs:FilterLogEvents\",\n        \"logs:DescribeLogStreams\",\n        \"logs:DescribeLogGroups\",\n        \"logs:CreateLogStream\",\n        \"logs:CreateLogGroup\"\n      ],\n      \"Resource\": \"*\"\n    }\n  ]\n}"
    role:   "${aws_iam_role.api_gateway_cloudwatch.id}"

-/+ aws_lambda_permission.status_api_resource_check_post
    action:        "lambda:InvokeFunction" => "lambda:InvokeFunction"
    function_name: "github-status_github-status" => "github-status_github-status"
    principal:     "apigateway.amazonaws.com" => "apigateway.amazonaws.com"
    source_arn:    "arn:aws:execute-api:ap-northeast-1:410000000000:1w03z7dzmi/test/POST/check" => "${aws_api_gateway_deployment.status_api_test.execution_arn}/${aws_api_gateway_integration.status_api_resource_check_post.integration_http_method}${aws_api_gateway_resource.status_api_resource_check.path}" (forces new resource)
    statement_id:  "AllowInvokeFromAPIGateway" => "AllowInvokeFromAPIGateway"


Plan: 5 to add, 0 to change, 2 to destroy.

API Gateway의 설정 화면에 들어가면 ARN이 잘 설정된 것을 볼 수 있다.

API Gateway에서 ARN이 설정된 화면

이제 API에 로깅을 활성화 해야 한다.

// infrastructure/github-status.tf

// API 메서드 설정
resource "aws_api_gateway_method_settings" "status_api_resource_check_post" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  stage_name  = "${aws_api_gateway_deployment.status_api_test.stage_name}"
  method_path = "${aws_api_gateway_resource.status_api_resource_check.path_part}/${aws_api_gateway_method.status_api_resource_check_post.http_method}"

  settings {
    metrics_enabled = true
    logging_level = "INFO"
  }
}

이는 aws_api_gateway_method_settings를 사용하는데 메서드 레벨에서 로깅 등을 활성화할 수 있다. 웹 콘솔에서는 스테이지 단위에서 설정을 한 후 다른 리소스나 메서드에서는 이 설정을 상속받는데 Terraform에서는 그런 방법을 못 찾았고 위처럼 스테이지와 메서드를 지정하고 로깅 등을 설정할 수 있다.

$ apex infra plan

-/+ aws_api_gateway_deployment.status_api_test
    created_date:      "2017-05-07T17:42:26Z" => "<computed>"
    description:       "Deployed at 2017-05-07T17:42:25Z" => "Deployed at 2017-05-07T17:46:07Z"
    execution_arn:     "arn:aws:execute-api:ap-northeast-1:410000000000:1w03z7dzmi/test" => "<computed>"
    invoke_url:        "https://1w03z7dzmi.execute-api.ap-northeast-1.amazonaws.com/test" => "<computed>"
    rest_api_id:       "1w03z7dzmi" => "1w03z7dzmi"
    stage_description: "2017-05-07T17:42:25Z" => "2017-05-07T17:46:07Z" (forces new resource)
    stage_name:        "test" => "test"

+ aws_api_gateway_method_settings.status_api_resource_check_post
    method_path:                "check/POST"
    rest_api_id:                "1w03z7dzmi"
    settings.#:                 "1"
    settings.0.logging_level:   "INFO"
    settings.0.metrics_enabled: "true"
    stage_name:                 "test"

-/+ aws_lambda_permission.status_api_resource_check_post
    action:        "lambda:InvokeFunction" => "lambda:InvokeFunction"
    function_name: "github-status_github-status" => "github-status_github-status"
    principal:     "apigateway.amazonaws.com" => "apigateway.amazonaws.com"
    source_arn:    "arn:aws:execute-api:ap-northeast-1:410000000000:1w03z7dzmi/test/POST/check" => "${aws_api_gateway_deployment.status_api_test.execution_arn}/${aws_api_gateway_integration.status_api_resource_check_post.integration_http_method}${aws_api_gateway_resource.status_api_resource_check.path}" (forces new resource)
    statement_id:  "AllowInvokeFromAPIGateway" => "AllowInvokeFromAPIGateway"


Plan: 3 to add, 0 to change, 2 to destroy.

이를 적용하고 CloudWatch 로그에 가면 다음과 같은 로그를 볼 수 있다.

17:50:30 Verifying Usage Plan for request: a9951f1b-334d-11e7-8a79-7fd16e045aa1. API Key: API Stage: 1w03z7dzmi/test
17:50:30 API Key authorized because method 'POST /check' does not require API Key. Request will not contribute to throttle or quota limits
17:50:30 Usage Plan check succeeded for API Key and API Stage 1w03z7dzmi/test
17:50:30 Starting execution for request: a9951f1b-334d-11e7-8a79-7fd16e045aa1
17:50:30 HTTP Method: POST, Resource Path: /check
17:50:31 Execution failed due to configuration error: No match for output mapping and no default output mapping configured
17:50:31 Method completed with status: 500

위에서 Internal server error 오류가 발생한 이유가 기본 output mapping이 설정되지 않았기 때문임을 알 수 있다. Lambda 자체의 오류가 아니라면 API Gateway에서 제공하는 테스트 기능으로 확인하거나 이 로그를 키고 확인해야 한다.

출력 매핑 추가

여기서 출력 매핑이란 것은 HTTP API에 응답을 결정하는 역할을 하로 아래 API 화면에서 Method Response와 Integration Response를 의미한다.

API Gateway에서 API가 설정된 화면

HTTP로 200을 줄 수도 있고 201이나 400, 404등 다양한 응답을 줄 수 있으므로 매핑 조건에 따라 어떤 응답을 줄지를 결정할 수 있다. 좀 더 설명하면 Lambda의 결과를 받아서 상태 코드와 함께 응답 데이터 형식을 결정해서 반환할 수 있다.

여기서는 일단 기본 응답인 200 OK를 추가해 보자.

// infrastructure/github-status.tf

// 200 응답 매핑
resource "aws_api_gateway_method_response" "status_api_resource_check_post_200" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "200"
  response_models = {
    "application/json" = "Empty"
  }
}

resource "aws_api_gateway_integration_response" "status_api_resource_check_post_200" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "${aws_api_gateway_method_response.status_api_resource_check_post_200.status_code}"

  response_templates = {
    "application/json" = ""
  }
}

일단 aws_api_gateway_method_response로 메서드의 응답을 하나 정의해야 한다. 여기서는 상태 코드를 200으로 정의했고 응답의 모델은 기본으로 만들어지는 Empty 모델을 application/json으로 응답하도록 정의했다. aws_api_gateway_integration_response에서는 Lambda와 응답을 통합하는 역할을 하는데 여기서 application/json 형식으로 응답하도록 정의했고 그 내용에는 아무것도 넣지 않았다.

$ apex infra plan

-/+ aws_api_gateway_deployment.status_api_test
    created_date:      "2017-05-07T17:59:46Z" => "<computed>"
    description:       "Deployed at 2017-05-07T17:59:45Z" => "Deployed at 2017-05-07T18:00:01Z"
    execution_arn:     "arn:aws:execute-api:ap-northeast-1:410000000000:1w03z7dzmi/test" => "<computed>"
    invoke_url:        "https://1w03z7dzmi.execute-api.ap-northeast-1.amazonaws.com/test" => "<computed>"
    rest_api_id:       "1w03z7dzmi" => "1w03z7dzmi"
    stage_description: "2017-05-07T17:59:45Z" => "2017-05-07T18:00:01Z" (forces new resource)
    stage_name:        "test" => "test"

+ aws_api_gateway_integration_response.status_api_resource_check_post_200
    http_method:                         "POST"
    resource_id:                         "txi2jt"
    response_templates.%:                "1"
    response_templates.application/json: ""
    rest_api_id:                         "1w03z7dzmi"
    status_code:                         "200"

+ aws_api_gateway_method_response.status_api_resource_check_post_200
    http_method:                      "POST"
    resource_id:                      "txi2jt"
    response_models.%:                "1"
    response_models.application/json: "Empty"
    rest_api_id:                      "1w03z7dzmi"
    status_code:                      "200"


Plan: 3 to add, 0 to change, 1 to destroy.

이를 적용한 뒤에 API를 다시 테스트하면 Lambda가 반환하는 결과가 200 OK로 반환된 것을 확인할 수 있다.

$ curl -i \
  -X POST https://1w03z7dzmi.execute-api.ap-northeast-1.amazonaws.com/test/check \
  -d '{ "url": "https://status.github.com/api/last-message.json"}'
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 107
Connection: keep-alive
Date: Sun, 07 May 2017 18:04:37 GMT
x-amzn-RequestId: a1f41398-334f-11e7-b887-d166a220b6ab
X-Amzn-Trace-Id: sampled=0;root=1-590f61b5-8338679ddbc8cf6feeadade5
X-Cache: Miss from cloudfront
Via: 1.1 5d1ba4039f11e793a35923f543e4f02a.cloudfront.net (CloudFront)
X-Amz-Cf-Id: Q4kIiAq7w8GK3SVVm3fmSmf3qzNlSZ-bsBElSl8rYzT7mGJi-HZXaA==

"{\"status\":\"good\",\"body\":\"Everything operating normally.\",\"created_on\":\"2017-05-06T19:04:12Z\"}"

이제 정상적인 동작은 마무리되었다. 이 API에 인증은 추가하지 않았지만, 이는 여기서는 다루지 않는다.

예외 응답 추가

200 OK는 잘 반환하게 되었는데 HTTP API라면 요청 형식이 잘못되거나 Lambda에서 오류가 발생한 경우 4xx, 5xx 응답을 줄 수 있어야 한다. 이를 위해서 200 응답처럼 응답 매핑을 추가해야 한다.

// infrastructure/github-status.tf

// 400 응답 매핑
resource "aws_api_gateway_method_response" "status_api_resource_check_post_400" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "400"
  response_models = {
    "application/json" = "Error"
  }
}

resource "aws_api_gateway_integration_response" "status_api_resource_check_post_400" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "${aws_api_gateway_method_response.status_api_resource_check_post_400.status_code}"
  selection_pattern = ".*BadRequest.*"
  response_templates = {
    "application/json" = <<EOF
#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))

{
  "message": "$errorMessageObj.message",
  "stack": "$errorMessageObj.stackTrace",
  "requestId": "$errorMessageObj.requestId"
}
EOF
  }
}

// 500 응답 매핑
resource "aws_api_gateway_method_response" "status_api_resource_check_post_500" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "500"
  response_models = {
    "application/json" = "Error"
  }
}

resource "aws_api_gateway_integration_response" "status_api_resource_check_post_500" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "${aws_api_gateway_method_response.status_api_resource_check_post_500.status_code}"
  selection_pattern = ".*InternalServerError.*"
  response_templates = {
    "application/json" = <<EOF
#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))

{
  "message": "$errorMessageObj.message",
  "stack": "$errorMessageObj.stackTrace",
  "requestId": "$errorMessageObj.requestId"
}
EOF
  }
}

앞에 200과 같이 400, 500 용으로 aws_api_gateway_method_responseaws_api_gateway_integration_response를 선언했다. 200과 다른 부분은 selection_pattern = ".*\BadRequest\]*"부분인데 이는 Lambda에서 넘어온 응답의 패턴을 정규식으로 비교해서 매치되는 경우 기본 응답 대신 이 응답을 사용한다. 앞에 Lambda에서 요청 오류이면 [BadRequest]를, 다른 오류가 생기면 [InternalServerError] 오류 메시지에 추가한 것을 기억하는가? 여기서 이 메시지를 비교한 것이다. 앞에서도 설명했듯이 오류 객체 JSON을 문자열로 반환했으므로 전체가 문자열로 받게 되는데 여기서 정규식으로 원하는 부분을 비교해서 응답과 매칭시키면 된다.

response_templates에서는 application/json로 반환하는데 받은 객체를 그대로 주는 대신 응답 데이터를 구성했다. API Gateway에서 제공하는 유틸리티 함수가 있는데 $util.parseJson($input.path('$.errorMessage'))에서 Lambda에서 받은 $.errorMessage를 JSON으로 변환해서 $errorMessageObj로 할당했다. 그리고 이 객체를 이용해서 원하는 JSON을 새로 구성해서 응답하도록 설정했다.

$ apex infra plan

-/+ aws_api_gateway_deployment.status_api_test
    created_date:      "2017-05-07T18:00:26Z" => "<computed>"
    description:       "Deployed at 2017-05-07T18:00:25Z" => "Deployed at 2017-05-07T18:16:54Z"
    execution_arn:     "arn:aws:execute-api:ap-northeast-1:410000000000:1w03z7dzmi/test" => "<computed>"
    invoke_url:        "https://1w03z7dzmi.execute-api.ap-northeast-1.amazonaws.com/test" => "<computed>"
    rest_api_id:       "1w03z7dzmi" => "1w03z7dzmi"
    stage_description: "2017-05-07T18:00:25Z" => "2017-05-07T18:16:54Z" (forces new resource)
    stage_name:        "test" => "test"

+ aws_api_gateway_integration_response.status_api_resource_check_post_400
    http_method:                         "POST"
    resource_id:                         "txi2jt"
    response_templates.%:                "1"
    response_templates.application/json: "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))\n\n{\n  \"message\": \"$errorMessageObj.message\",\n  \"stack\": \"$errorMessageObj.stackTrace\",\n  \"requestId\": \"$errorMessageObj.requestId\"\n}\n"
    rest_api_id:                         "1w03z7dzmi"
    selection_pattern:                   ".*BadRequest.*"
    status_code:                         "400"

+ aws_api_gateway_integration_response.status_api_resource_check_post_500
    http_method:                         "POST"
    resource_id:                         "txi2jt"
    response_templates.%:                "1"
    response_templates.application/json: "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))\n\n{\n  \"message\": \"$errorMessageObj.message\",\n  \"stack\": \"$errorMessageObj.stackTrace\",\n  \"requestId\": \"$errorMessageObj.requestId\"\n}\n"
    rest_api_id:                         "1w03z7dzmi"
    selection_pattern:                   ".*InternalServerError.*"
    status_code:                         "500"

+ aws_api_gateway_method_response.status_api_resource_check_post_400
    http_method:                      "POST"
    resource_id:                      "txi2jt"
    response_models.%:                "1"
    response_models.application/json: "Error"
    rest_api_id:                      "1w03z7dzmi"
    status_code:                      "400"

+ aws_api_gateway_method_response.status_api_resource_check_post_500
    http_method:                      "POST"
    resource_id:                      "txi2jt"
    response_models.%:                "1"
    response_models.application/json: "Error"
    rest_api_id:                      "1w03z7dzmi"
    status_code:                      "500"

+ aws_api_gateway_method_settings.status_api_resource_check_post
    method_path:                "check/POST"
    rest_api_id:                "1w03z7dzmi"
    settings.#:                 "1"
    settings.0.logging_level:   "INFO"
    settings.0.metrics_enabled: "true"
    stage_name:                 "test"


Plan: 6 to add, 0 to change, 1 to destroy.

이를 적용하고 실제로 각 오류를 테스트해보자.

$ curl -i \
  -X POST https://1w03z7dzmi.execute-api.ap-northeast-1.amazonaws.com/test/check \
  -d '{ }'
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 234
Connection: keep-alive
Date: Sun, 07 May 2017 18:17:57 GMT
x-amzn-RequestId: 7ef3703e-3351-11e7-9007-39d9f5af8bce
X-Amzn-Trace-Id: sampled=0;root=1-590f64d5-c36878c3f02a3717ec66c3d3
X-Cache: Error from cloudfront
Via: 1.1 e16834aaac4fd814ae3edf9e633f4567.cloudfront.net (CloudFront)
X-Amz-Cf-Id: GrkJP5YTUc1LfwQO6Sc4cimOzKVFFAXDfj3_chP7_PHCpJDjYyQBmw==


{
  "message": "[BadRequest] URL required",
  "stack": "Error: [BadRequest] URL required
    at ping (/var/task/index.js:6:25)
    at exports.handle (/var/task/index.js:18:3)",
  "requestId": "7ef40ba9-3351-11e7-b596-5dd2f1ad2723"
}

요청에 url 속성을 전달하지 않으면 400 Bad Request가 반환된다.

$ curl -i \
  -X POST https://1w03z7dzmi.execute-api.ap-northeast-1.amazonaws.com/test/check \
  -d '{ "url": "noturl" }'
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Content-Length: 448
Connection: keep-alive
Date: Sun, 07 May 2017 18:18:12 GMT
x-amzn-RequestId: 87efd9cd-3351-11e7-8a79-7fd16e045aa1
X-Amzn-Trace-Id: sampled=0;root=1-590f64e4-033e3c311bef8a2e208d0a5f
X-Cache: Error from cloudfront
Via: 1.1 11a0261328fa2fb9ebe64a59fd132104.cloudfront.net (CloudFront)
X-Amz-Cf-Id: rt2k9xQFGIxKQB1z4wuapWD5gasgBsR05V9APGlTLEyRGHlTs5Nh_g==


{
  "message": "[InternalServerError] Invalid URI "noturl"",
  "stack": "Error: Invalid URI "noturl"
    at Request.init (/var/task/node_modules/request/request.js:276:31)
    at new Request (/var/task/node_modules/request/request.js:130:8)
    at request (/var/task/node_modules/request/index.js:54:10)
    at ping (/var/task/index.js:8:3)
    at exports.handle (/var/task/index.js:18:3)",
  "requestId": "87ef8bdc-3351-11e7-82ab-d92b9a17bfda"
}

그리고 다른 오류가 발생하도록 url이 아닌 다른 문자열을 전달하자 500 오류가 정상적으로 반환되었다.

마무리

물론 API Gateway가 많은 기능을 제공하므로 제대로 사용하려면 이외에도 많은 설정이 필요하지만, 이 기본 골격과 각 리소스에 대한 이해를 하기 위해서 꽤 많은 시간을 소비했다. 이 구성이 가장 좋은 구성이라는 것은 아니지만, 이 기본 구조를 찬찬히 살펴보면 각 리소스를 정의해서 사용하는 방법을 이해할 수 있다고 생각한다. 물론 API Gateway도 Terraform도 계속 발전하고 있으므로 변경사항에 대한 추적도 필요하긴 하다.

다음은 infrastructure/github-status.tf 파일의 전체 내용이다.

// API 정의
resource "aws_api_gateway_rest_api" "status_api" {
  name        = "StatusAPI"
  description = "상태를 조회하는 API"
}

// API 리소스 정의 (/check 같은 경로)
resource "aws_api_gateway_resource" "status_api_resource_check" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  parent_id   = "${aws_api_gateway_rest_api.status_api.root_resource_id}"
  path_part   = "check"
}

// API 리소스의 메서드 정의 (GET, POST 등)
resource "aws_api_gateway_method" "status_api_resource_check_post" {
  rest_api_id   = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id   = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method   = "POST"
  authorization = "NONE"
}

// API와 Lambda 통합
resource "aws_api_gateway_integration" "status_api_resource_check_post" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  type = "AWS"
  integration_http_method = "POST"
  uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/${var.apex_function_github-status}/invocations"
}

// API 배포
resource "aws_api_gateway_deployment" "status_api_test" {
  depends_on = ["aws_api_gateway_integration.status_api_resource_check_post"]
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  stage_name  = "test"
  stage_description = "${timestamp()}"
  description = "Deployed at ${timestamp()}"
}

// Lambda에 호출할 권리
resource "aws_lambda_permission" "status_api_resource_check_post" {
  statement_id = "AllowInvokeFromAPIGateway"
  action = "lambda:InvokeFunction"
  function_name = "${var.apex_function_github-status_name}"
  principal = "apigateway.amazonaws.com"
  source_arn = "${aws_api_gateway_deployment.status_api_test.execution_arn}/${aws_api_gateway_integration.status_api_resource_check_post.integration_http_method}${aws_api_gateway_resource.status_api_resource_check.path}"
}

// API 메서드 설정
resource "aws_api_gateway_method_settings" "status_api_resource_check_post" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  stage_name  = "${aws_api_gateway_deployment.status_api_test.stage_name}"
  method_path = "${aws_api_gateway_resource.status_api_resource_check.path_part}/${aws_api_gateway_method.status_api_resource_check_post.http_method}"

  settings {
    metrics_enabled = true
    logging_level = "INFO"
  }
}

// 200 응답 매핑
resource "aws_api_gateway_method_response" "status_api_resource_check_post_200" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "200"
  response_models = {
    "application/json" = "Empty"
  }
}

resource "aws_api_gateway_integration_response" "status_api_resource_check_post_200" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "${aws_api_gateway_method_response.status_api_resource_check_post_200.status_code}"

  response_templates = {
    "application/json" = ""
  }
}

// 400 응답 매핑
resource "aws_api_gateway_method_response" "status_api_resource_check_post_400" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "400"
  response_models = {
    "application/json" = "Error"
  }
}

resource "aws_api_gateway_integration_response" "status_api_resource_check_post_400" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "${aws_api_gateway_method_response.status_api_resource_check_post_400.status_code}"
  selection_pattern = ".*BadRequest.*"
  response_templates = {
    "application/json" = <<EOF
#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))

{
  "message": "$errorMessageObj.message",
  "stack": "$errorMessageObj.stackTrace",
  "requestId": "$errorMessageObj.requestId"
}
EOF
  }
}

// 500 응답 매핑
resource "aws_api_gateway_method_response" "status_api_resource_check_post_500" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "500"
  response_models = {
    "application/json" = "Error"
  }
}

resource "aws_api_gateway_integration_response" "status_api_resource_check_post_500" {
  rest_api_id = "${aws_api_gateway_rest_api.status_api.id}"
  resource_id = "${aws_api_gateway_resource.status_api_resource_check.id}"
  http_method = "${aws_api_gateway_method.status_api_resource_check_post.http_method}"
  status_code = "${aws_api_gateway_method_response.status_api_resource_check_post_500.status_code}"
  selection_pattern = ".*InternalServerError.*"
  response_templates = {
    "application/json" = <<EOF
#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))

{
  "message": "$errorMessageObj.message",
  "stack": "$errorMessageObj.stackTrace",
  "requestId": "$errorMessageObj.requestId"
}
EOF
  }
}
2017/05/08 04:18 2017/05/08 04:18