Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.

기술 뉴스 #78 : 17-05-16

웹개발 관련

그 밖의 프로그래밍 관련

  • Node.js 8.0.0 has been delayed and will ship on or around May 30th : 원래 LTS 일정에 따르면 4월에 Node.js 8.0.0이 나오고 10월에 LTS에 진입해야 하는데 아직 8.0.0이 나오지 않은 배경을 설명한 글이다. 현재 V8 엔진이 Crankshaft에서 TurboFan + Ignition 조합으로 바꾸는 단계이므로 8.0.0이 LTS에 들어가서 3년 가까이 관리상태에 들어가게 될 때 구버전에 맞추면 관리가 너무 어려우므로 V8 5.8에 ABI 6.0 호환성을 가진 버전으로 Node.js 8.0.0을 릴리스 하기로 해서 5월 말 정도에 8.0.0이 나올 거라는 글이다. LTS 일정에는 영향을 주지 않고 10월에 LTS가 나올 예정이다.(영어)
  • Spring Framework 5.0 goes RC1 : Spring 프레임워크 5.0의 릴리스 후보가 나왔다. Spring 5는 Java 8+에 기반을 두고 있고 Reactive 프로그래밍, Kotlin 지원 증이 추가되었다.(영어)
  • 마이크로소프트, 맥용 비주얼 스투디오 2017 공식 배포 : 맥용 비주얼 스튜디오가 공개되었다.(한국어)

볼만한 링크

  • HTTPS는 HTTP보다 빠르다 : 현재 HTTPS를 써야 하는 이유를 잘 설명하고 HTTP/2에서 속도가 얼마나 차이가 나는지 잘 설명한 글이다. HTTPS는 보안의 문제이긴 하지만 이는 비밀번호를 가로채는 문제도 있지만, 인터넷 검열의 문제도 중요하고 지금은 HTTP/2가 있어서 속도 면에서 HTTP보다 속도 면에서 큰 차이가 없다고 설명하고 있다.(한국어)
  • 실리콘밸리의 IPO 열풍 : 실리콘밸리에서 주목받는 회사들이 IPO를 하면서 진행되는 일을 설명한 글이다. IPO 과정에서 직원들은 어떤 식을 주식을 받고 금융권에서 이를 어떻게 처리하는지 단계별로 설명되어 있다. IPO에 낄 일이 흔치는 않아서 안에서는 이런 일이 벌어진다는 게 재미있다.(한국어)
  • 소심한 실리콘밸리의 한국인 : 국내에서 정중함을 표현하기 위해서 사과하는 문화가 서양에서는 소심해 보일 수 있다는 부분을 지적하면서 사과로 표현하는 부분을 감사함으로 표현하는 것을 제안하고 있다. 국내에서 커뮤니케이션하더라도 괜찮은 방법이라고 생각한다.(한국어)

프로젝트

  • Prepack : Facebook에서 공개한 JavaScript 코드 최적화 도구로 현재는 초기 개발상태로 프로덕션용은 아직 아니다.
  • Atlassian Design : 아틀라시안의 디자인 관련 자료를 한곳에 모아놓은 사이트로 디자인 가이드 및 브랜드 철학 등의 자료를 한곳에 모아두었다.
  • k6 : JavaScript로 작성해서 부하 테스트를 할 수 있는 CLI 도구.
  • Britecharts : EventBrite에서 D3.js v4 기반으로 만든 차트 라이브러리.
  • Gixy : Nginx의 설정 파일의 취약점을 분석해 주는 Python 도구.

버전 업데이트

2017/05/16 04:06 2017/05/16 04:06

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