Outsider's Dev Story

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

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

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

AWS에서는 API Gateway를 제공하고 있다. Lambda로 서버리스 아키텍처를 구성해서 앞에 API Gateway를 연결하면 간단한 기능부터 좀 더 복잡한 API까지 구현해서 운영 걱정 없이 내부에서 사용하거나 외부에 노출할 수 있다.

전에 API Gateway를 Lambda와 연결하는 방법에 대해서 설명했지만 여기서는 API Gateway를 소개하고 간단한 사용법을 설명했을 뿐이고 Lambda 관리 도구인 Apex를 소개할 때도 API Gateway를 Apex에서 지원하는 Terraform으로 API Gateway를 구성하는 방법을 설명했지만 여기에서 목적도 Apex와 Terraform을 설명하는 것이었으므로 기능은 간단하게만 설명했다.

Apex

Lambda를 사용할 때 Apex를 거의 이용하는 편이다. Lambda는 간단한 함수 위주로 관리하면서 다양한 AWS 자원과 연동해서 사용해야 하므로 Lambda만 볼 때 이 Lambda가 어떻게 동작하고 어떤 AWS 자원과 연동되어 있는지 확인하기가 무척 어렵다. 요즘은 대부분의 AWS 자원에서 비슷한 느낌을 받지만 작은 단위로 사용하기 쉬운 Lambda를 사용할 때 관리가 어려운 상태가 되기 훨씬 쉽다고 생각한다.

Apex 말고도 Serverless도 있지만 나는 Apex를 사용하고 있다. Serverless는 빠르게 버전이 올라가고 기능이 추가되고 있고 스터디에서 간단히만 살펴보았지만, Lambda에 API Gateway URL 연결해서 간단히 적용하기에는 Serverless가 훨씬 간단해 보였다.

Apex는 Serverless와는 달리 Lambda 함수의 배포, 버저닝, 테스트 등은 지원하지만 이전 글에서 설명했듯이 Apex를 Lambda 외 AWS 자원을 관리할 때 Terraform을 이용한다. 명령어는 apex infra를 사용하지만 실제로 이는 terraform의 별칭일 뿐이다. 그래서 실제로 Apex로 AWS 자원을 관리하려면 Terraform을 이용해야 한다. 나는 Terraform이란 도구를 맘에 들어 하지만 실제로 Serverless같은 도구보다 Apex를 Terraform에 대해서도 지식이 꽤 필요하다. 그런데도 Terraform을 이용하면 상세 설정을 할 수 있고 원하는 대로 구성이 가능하므로 Terraform을 더 선호하는 편이다.

API Gateway

Lambda를 배포하고 API Gateway를 구성하기 위해서 Terraform으로 설정하려면 꽤 복잡한 설정을 추가해야 한다. Apex로 실제 Lambda 함수 작성하기에서 Apex에서 Terraform을 이용하는 방법을 설명했으니 이전에 한 번도 사용하지 않아 봤다면 이 글이나 Terraform 관련 글을 먼저 보는 게 좋다.

대부분의 AWS 서비스가 복잡하지만, API Gateway는 특히 복잡하다고 느끼는 서비스 중 하나이다. API Gateway를 이용해서 뭔가 구성해 봤다면 대부분 느낄 것 같은데 설정이 꽤 복잡해서 제대로 연동하려면 삽질을 많이 하게 된다. AWS 웹 콘솔에서 작업해도 좀 어렵지만, GUI이므로 설정에 대한 도움을 꽤 받을 수 있지만 Terraform으로 API Gateway를 구성하려면 모든 요소를 직접 정의해야 하므로 각 요소에 대한 이해도가 높지 않으면 작업하기가 꽤 어렵다.

전에 한 번 작성해서 적용했는데 한 6개월 지나서 다시 해보려니 안되는 부분도 있고 해서 다시 테스트하면서 적용을 해보았다. 전에는 대충 이해하고 있었던 부분도 있고 해서...

그래서 이 글은 큰 범위에서는 Apex를 이용해서 Lambda로 API Gateway를 연결하는 방법에 관한 내용이지만 집중하는 부분은 Terraform으로 API Gateway를 설정하는 방법에 관한 내용이다.

간단한 Lambda 함수

API Gateway에 Lambda 함수를 연동하기 위한 예시로 간단한 Lambda 함수를 만들어 보자. 이 Lambda는 단순히 전달받은 URL에 요청을 보내서 그 결과를 반환하는 함수이다.

// index.js
const request = require('request');

console.log('starting function');

const ping = (url, cb) => {
  if (!url) { return cb(new Error('[BadRequest] URL required')); }

  request(url, (err, response, body) => {
    console.log('ping statusCode:', response && response.statusCode);
    console.log('ping body:', body);
    cb(err, body);
  });
};

exports.handle = (event, ctx, cb) => {
  console.log('processing event: %j', event);

  ping(event.url, (err, result) => {
    if (err) {
      const error = {
        message: err.message,
        stackTrace: err.stack,
        event: event,
        requestId : ctx.awsRequestId,
      }
      if (!err.message.match(/BadRequest/)) {
        error.message = `[InternalServerError] ${err.message}`
      }
      console.log('err: %j', error);
      return cb(JSON.stringify(error));
    }

    console.log('result: %j', result);
    return cb(null, result);
  });
};

이 Lambda는 Node.js 6.10으로 작성된 코드이다. 간단히 설명하면 ping()request 모듈을 이용해서 HTTP 요청을 보내고 받은 응답을 돌려주는 함수이다. exports.handle가 Lambda의 진입점인데 요청으로 들어온 이벤트가 event변수로 들어온다. 예를 들어 이 Lambda 함수에 { "url": "https://status.github.com/api/last-message.json" }를 전달하면 {"status":"good","body":"Everything operating normally.","created_on":"2017-05-06T19:04:12Z"} 응답을 돌려준다.(저 URL은 github 사이트의 상태를 알려주는 API 이다.)

몇 가지 API Gateway와 연동할 이 Lambda에 관해서 설명할 부분이 있다.

  • 여기서는 작업이 완료되었을 때 cb 즉, 콜백으로 오류와 결과를 반환했다. Node.js의 관례대로 첫 인자는 오류이고 두 번째가 결과이다. 콜백 방식 성공했을 때는 ctx.succeed(result)를 실행하고 실패했을 때는 ctx.fail(JSON.stringify(error))를 사용해도 같다. 필요 한대로 사용하면 된다.
  • API Gateway가 Lambda 전용이 아니므로 이 Node.js로 작성된 Lambda와 직접 연동되지 않는다. 그래서 보통 작성하듯이 callback(error)를 반환해도 API Gateway가 오류 객체를 받지를 못한다.(Lambda가 모두 Node.js도 아니고 API Gateway도 그렇지 않으니까) 그래서 callback(new Error('something worng!'))과 같이 전달하면 API Gateway에서는 { "errorMessage": "something worng!" }로 받게 된다. 오류 객체가 결국 문자열로 변경이 되는데 결과에 따라 API Gateway에서 처리가 필요하므로 좀 더 정보를 많이 주어야 한다. 그래서 여기서는 다른 객체를 만들어서 오류 정보를 넣은 뒤에 이를 JSON.stringify로 문자열로 바꾸어서 반환한다. 이를 사용하는 방법은 뒤에서 좀 더 살펴볼 것이다.
  • 이 Lambda에 필수값인 url을 전달하지 않으면 [BadRequest] URL required 오류를 발생시킨다. 이 부분도 API Gateway에서 처리를 위해서 의도적으로 [BadRequest]를 넣은 것이고 마지막에 오류를 처리하기 전에 BadReqeust가 아니라면 [InternalServerError] 접두사를 오류 메시지에 붙여 주었다. 이 오류 메시지도 뒤에서 설명할 API Gateway에서 사용하는 방식을 설명하겠다.
  • console.log로 로깅을 해놓으면 CloudWatch에서 로그 메시지를 볼 수 있으므로 디버깅할 때 좀 더 편하다.

작성한 Lambda 함수를 Apex로 배포한다.

$ apex deploy
   • creating function         env= function=github-status
   • created alias current     env= function=github-status version=1
   • function created          env= function=github-status name=github-status_github-status version=1

여기서는 Apex 자체에 사용법은 자세히 설명하지 않으므로 Apex - AWS Lambda 관리 도구를 참고하기 바란다.

apex infra로 API Gateway 구성하기

앞에 길게 설명했지만, 이게 이 글의 본론이다. API Gateway가 복잡해서 이해가 잘하지 못한 부분도 있지만 Terraform에서 문서가 아직 제대로 안 된 부분 등을 테스트해보고 확인하느라 고생을 꽤 많이 했다. 앞에서도 설명했지만, 명령어는 apex infra를 사용하지만 실제로는 Terraform이다. 하지만 apex infra를 이용하면 배포한 Lambda에 대한 정보를 Terraform 변수로 제공한다.

apex infra를 이용하려면 infrastructure 폴더를 만들고 이 안에 Terraform 파일을 만들어야 한다.

// infrastructure/variables.tf

variable "aws_region" {}

variable "apex_function_github-status" {}
variable "apex_function_github-status_name" {}

이 파일은 테라폼에서 사용할 변수를 모아놓은 파일이다. apex로 Lambda를 사용하기 위해 설정한 AWS 정보와 배포한 Lambda 함수에 대한 정보를 테라폼내에서 사용하려면 변수를 정의해놓아야 한다. 내용은 알아서 apex가 채워주지만, 선언은 해야 한다. apex_function_github-status가 배포한 Lambda 함수의 ARN이다.(그래서 배포를 먼저 해놓아야 한다.) apex_function_github-status_name는 배포한 Lambda의 이름이다. 이 변수는 apex list --tfvars로 확인할 수 있는데 여기서는 apex_function_github-status만 출력되므로 나머지는 문서를 보고 찾아야 한다.

API Gateway 생성

AWS 웹 콘솔에서라도 API Gateway를 만들어 본 적이 없다면 일단 웹 콘솔에서 만들어 보기 바란다. 열심히 설명하겠지만 만들어 본 적이 없다면 아마 이 글을 이해하기가 무척 어려울 것으로 생각한다.

API Gateway의 웹 콘솔

API 게이트웨이는 위처럼 생겼다. 이는 새로운 API를 하나 만들고 /check라는 리소스를 하나 만들고 여기에 POST 메서드를 추가해서 Lambda와 연결하면 이렇게 만들어진다. 여기서 크게 3단계 정도로 하나의 API 리소스를 만들었지만 테라폼으로 작성하려면 요소를 다 지정해 주어야 한다.

// 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"
}

이 파일은 infrastructure/github-status.tf에 API Gateway를 설정한 것이다. 일부러 Lambda 함수와 파일명을 맞추었다. 앞에 GUI에서 만든것과 동일한 과정이다. aws_api_gateway_rest_api는 API 자체를 만드는 것이고 여기에 aws_api_gateway_resource로 리소스를 추가하는데 경로는 check이므로 실제로는 /check가 된다. 그리고 이 리소스에 aws_api_gateway_methodPOST 메서드를 추가한다.

마지막으로 aws_api_gateway_integration에서 이 메서드를 배포한 Lambda와 통합한다. 여기서 AWS 자원과 통합하므로 typeAWS로 지정하고 Lambda는 항상 POST로 호출해야 하므로 integration_http_method로 지정했고 uri는 HTTP나 AWS 일때는 필수 값인데 AWS에서는 arn:aws:apigateway:{region}:{subdomain.service|service}:{path|action}/{service_api}같은 형식이 된다. 이 URI를 만들기 위해서 변수에서 지정했던 var.aws_regionvar.apex_function_github-status를 이용했다. 이 값은 배포한 Lambda의 리전과 ARN을 apex가 넣어준다.

$ apex infra plan

+ aws_api_gateway_integration.status_api_resource_check_post
    http_method:             "POST"
    integration_http_method: "POST"
    passthrough_behavior:    "<computed>"
    resource_id:             "${aws_api_gateway_resource.status_api_resource_check.id}"
    rest_api_id:             "${aws_api_gateway_rest_api.status_api.id}"
    type:                    "AWS"
    uri:                     "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:410655858509:function:github-status_github-status/invocations"

+ aws_api_gateway_method.status_api_resource_check_post
    api_key_required: "false"
    authorization:    "NONE"
    http_method:      "POST"
    resource_id:      "${aws_api_gateway_resource.status_api_resource_check.id}"
    rest_api_id:      "${aws_api_gateway_rest_api.status_api.id}"

+ aws_api_gateway_resource.status_api_resource_check
    parent_id:   "${aws_api_gateway_rest_api.status_api.root_resource_id}"
    path:        "<computed>"
    path_part:   "check"
    rest_api_id: "${aws_api_gateway_rest_api.status_api.id}"

+ aws_api_gateway_rest_api.status_api
    created_date:     "<computed>"
    description:      "상태를 조회하는 API"
    name:             "StatusAPI"
    root_resource_id: "<computed>"


Plan: 4 to add, 0 to change, 0 to destroy.

이를 plan으로 확인해 보면 4개의 리소스가 설정되는 것을 볼 수 있다. 이제 apex infra apply로 적용하면 아래와 같이 API Gateway가 새로 만들어진 것을 볼 수 있다.

생성된 API Gateway

API Gateway


API Gateway 배포

이어서 계속 설정을 해보자. API Gateway는 위처럼 리소스와 메서드를 정의한 뒤에 실제 서비스에서 사용하듯이 test, stage, production 등의 스테이지를 만들어서 배포할 수 있다. API를 수정하거나 작업을 한 뒤에 정해진 스테이지에 배포를 하면 실제 서비스에 적용이 된다. 아래 내용은 이 배포를 위해서 infrastructure/github-status.tf에 추가한 부분이다. 참고로 테라폼은 파일 간 순서 등이 상관없으므로 다른 파일에 작성해도 상관없지만 여기서는 한 파일에 모두 작성을 했다.

// infrastructure/github-status.tf

// 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}"
}

여기선 두 가지 리소스를 정의했다.

aws_api_gateway_deployment는 위에서 설명한 배포이다. 배포는 위에서 만든 aws_api_gateway_integration.status_api_resource_check_post가 완성된 후에 이루어져야 하므로 depends_on으로 지정을 했다.(여기선 하나뿐이지만 여러 개가 있다면 배열 안에 추가하면 된다.) 그리고 스테이지 이름은 여기서는 test로 지정했다. 이렇게 하면 test 스테이지가 생성된다. 테라폼 문서를 보면 aws_api_gateway_stage 리소스가 정의되어 있는데 온갖 테스트를 다 해봤지만, 이 리소스를 사용하는 방법을 모르겠다. aws_api_gateway_deployment로도 스테이지가 만들어지고 aws_api_gateway_stage로도 스테이지가 만들어지므로 같은 이름을 지정하면 이미 존재한다고 오류가 발생하고 aws_api_gateway_stage를 정의해서 aws_api_gateway_deployment에서 가져다가 쓰려고 해도 aws_api_gateway_stage에서 aws_api_gateway_deployment의 ID가 필수값이라 순환참조 오류가 발생한다.

여기서 배포에 대해 좀 더 설명해야 하는데 리소스, 메서드, 스테이지 등은 AWS의 정의된 리소스이지만 배포는 리소스가 아니라 행위이다. 말이 모호하기는 한데 API Gateway를 구성하고 구성이 완료되면 배포를 하는 것이다. 그러므로 앞에서 정의한 API Gateway의 리소스나 메서드를 새로 추가하거나 수정을 하면 배포도 다시 해야 한다. 테라폼 문서에는 안 나와 있지만, 테스트를 해보고 찾아본 결과 description = "Deployed at ${timestamp()}"을 추가해야 한다. 이를 추가하지 않으면 API에 변경을 했을 때 새로 배포하지 않는다. 이는 뒤에서 API를 수정할 때 보여주겠다.
꼭 시간을 지정할 필요는 없지만, 확인을 위해서 배포된 시간을 기록하도록 했다. 추가로 stage_description = "${timestamp()}"를 지정했는데(이 부분도 문서에 없었다. ㅠ) stage_description를 지정하지 않으면 새로 배포를 하기는 하지만 배포 히스토리가 쌓이지 않고 기존 배포를 업데이트해버린다. 좀 당황스러운 동작이지만 기존 배포에 ID가 할당되고 테라폼이 이를 기억하고 있으므로 description를 지정하면 새로 배포하면서 기존 배포의 내용을 업데이트한다. stage_description를 지정하면 각 배포가 구분되므로 언제 배포했는지를 히스토리로 쌓을 수 있다. 대신 apex infra plan을 실행할 때마다 변경사항이 없어도 항상 배포를 새로 하려고 시도한다. API를 변경할 때만 배포되도록 하는 방법은 아직 찾지 못했다.

aws_lambda_permission는 API Gateway에서 Lambda를 실행할 수 있도록 권한을 주는 것이다. 언제부터 있었는지 모르겠지만, 전에는 IAM role을 하나 만들어서 이를 통해서 실행했는데 Lambda에 바로 권한을 추가해버릴 수 있게 되었다. 여기서 function_name을 지정하기 위해 var.apex_function_github-status_name 변수를 사용했다.

여기서 지정한 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}라는 아주 긴 값을 사용했다. 이는 arn:aws:execute-api:ap-northeast-1:410655858509:5psac8pgq9/test/POST/check같은 형태로 지정하기 위해서 각 변수의 값을 조합한 것이다. 여기서 test 스테이지에 배포했으므로 여기 배포한 POST /check에서 앞에서 배포한 Lambda를 호출할 수 있도록 권한을 부여하는 것이다. 이렇게 하면 개별 API나 스테이지 별로 특정 Lambda만 실행할 수 있게 권한을 제어할 수 있다.

다음 두 ARN을 보자.

  1. arn:aws:execute-api:ap-northeast-1:410655858509:5psac8pgq9/test/POST/check
  2. arn:aws:execute-api:ap-northeast-1:410655858509:5psac8pgq9/*/POST/check

여기서 지정한 것은 1번 ARN이고 2번 ARN은 스테이지 부분(여기서는 test)을 와일드카드 처리했다. 이렇게 하면 스테이지에 상관없이 접근 권한을 줄 수 있다. 이게 왜 중요하냐 하면 1번 ARN으로 지정하면 API Gateway 웹 콘솔에서 테스트 버튼을 눌러서 테스트할 수가 없다. 테스트는 스테이지에서 하는 게 아니라 리소스 밑의 메뉴에서 하므로 권한 없음 오류가 발생한다.

source_arn = "arn:aws:execute-api:${var.aws_region}:${var.aws_account_id}:${aws_api_gateway_rest_api.status_api.id}/*/${aws_api_gateway_integration.status_api_resource_check_post.integration_http_method}${aws_api_gateway_resource.status_api_resource_check.path}"

테스트에 불편하므로 2번 형태로 ARN을 지정하고 싶다면 위처럼 훨씬 복잡하게 지정해야 한다.(나중에 테라폼에서 지원해 줄지 모르지만...) 여기서 경로에 ${var.aws_account_id}를 사용했는데 이는 계정의 ID이고 이 예시에서 410655858509가 계정 ID이다. AWS에서 자신의 계정 메뉴에 들어가면 이 ID 값을 볼 수 있다. 이는 가져올 수 있는 곳이 없으므로 미리 찾아서 변수로 지정한 것인데 사용하는 패턴에 따라 변수든 다른 방식이든 이 값을 가져와야 한다. Apex는 이 값을 주지 않는다.

이 설정을 배포해보자.

$ apex infra plan

+ aws_api_gateway_deployment.status_api_test
    created_date:      "<computed>"
    description:       "Deployed at 2017-05-07T17:22:09Z"
    execution_arn:     "<computed>"
    invoke_url:        "<computed>"
    rest_api_id:       "1w03z7dzmi"
    stage_description: "2017-05-07T17:22:09Z"
    stage_name:        "test"

+ aws_lambda_permission.status_api_resource_check_post
    action:        "lambda:InvokeFunction"
    function_name: "github-status_github-status"
    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}"
    statement_id:  "AllowInvokeFromAPIGateway"


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

이를 적용하면 다음과 같이 스테이지가 만들어지고 배포된 것을 볼 수 있다.

웹 콘솔에서 API가 배포된 화면

그리고 Lambda 함수에 가서 Triggers 메뉴에 가면 Terraform으로 지정한 대로 API Gateway의 권한 설정이 된 것을 볼 수 있다.

Lambda에서 권한이 추가된 화면

실제 API가 동작하는지 테스트를 해보자.

$ 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 500 Internal Server Error
Content-Type: application/json
Content-Length: 36
Connection: keep-alive
Date: Sun, 07 May 2017 17:27:04 GMT
x-amzn-RequestId: 62ba1cb4-334a-11e7-b673-1d08c1f2e7b5
X-Cache: Error from cloudfront
Via: 1.1 561f6699cf8751ed16dd95a8118df9bb.cloudfront.net (CloudFront)
X-Amz-Cf-Id: CSz_S04uofasovxiV4FgPJuIyvchQUxLdwAxvwKctA49UXuCYvg6dw==

{"message": "Internal server error"}

오류 응답이 왔고 앞에서 코드를 보았듯이 이 오류는 Lambda에서 온 게 아니라 API Gateway에서 준 것이다.


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

2017/05/08 04:10 2017/05/08 04:10