DEV Community

Giacomo Grangia
Giacomo Grangia

Posted on • Edited on

Simple AWS API Gateway with OpenAPI and Terraform

Introduction

We've all been there. AWS pitches API Gateway as this simple solution to get your API code up and running. And it's partly true. Starting from the Console is quick, once you get the hang of stages, resources, and integrations. It sets up inter-service permissions for you, making everything seem effortless.

But, as your API grows, managing it becomes a bit of a headache. I mean, who wants to handle production infrastructure manually? Right? That's where Infrastructure as Code (IaC) tools come into play. Some are tailor-made for serverless services (like SAM and the serverless framework), while others are more all-purpose (think Terraform, CDK, Pulumi and so forth).

If you're a Terraform fan like me, you might have realized by now that Terraform and API Gateway aren't exactly besties.

In this post, I show a quick method to define your API Gateway with container-based lambdas (with alias) and automated deployments via CodeDeploy.

API Gateway and Terraform

I've never been fond of using Terraform to create and manage API Gateways, and I'm sure I'm not alone. I often find myself needing to utilize Usage Plans, leading me to opt for REST API. Fortunately, it's entirely possible to define our resources via OpenAPI. Thanks to extensions, we can specify every aspect of the API, including authorizers, integrations, validators, and so on. While it's not a brand-new feature, I always feel delighted when I have the opportunity to use it.

Show me the code

Here is the link to the GitHub repository I've created for this post. I believe that using OpenAPI to define API Gateways is not only straightforward but also highly effective for automation. I utilized a template (.tpl) file to facilitate compatibility with Terraform. I won't delve into the OpenAPI standard here, but you can discover much more information on their repository.

Now, let's explore the definition.

Lambda Proxy Integration



paths:
  /endpoint/v1/hello:
    get:
      summary: One of my wonderful endpoint
      parameters:
        - $ref: "#/components/parameters/authorization"
      x-amazon-apigateway-integration:
        type: aws_proxy
        httpMethod: POST # <--- always POST if using aws_proxy (lambda proxy). It is independent of your path method
        uri: ${endpoint_api1_lambda}
        credentials: ${endpoint_api1_role}
      security:
        - authorizerRandom: []



Enter fullscreen mode Exit fullscreen mode

As illustrated in the snippet above, defining resources (under the path key) follows the conventional OpenAPI methodology. The extension utilized here, x-amazon-apigateway-integration, specifies a Lambda integration, as indicated by the type field (aws_proxy). It's important to note the httpMethod: in lambda proxy integrations, you should always use POST, regardless of your path method (my resource method is a get).
In the uri field, you have the option to specify the lambda invoke ARN. I strongly recommend creating an alias and using the alias invoke ARN. This approach binds the API to a specific version of your Lambda, allowing you to update which version the alias points to later on.
The credentials field is for specifying a role that will be used to invoke the Lambda.



resource "aws_iam_role" "apigw_resource_role" {
  name = var.role_name

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "apigateway.amazonaws.com"
        }
      }
    ]
  })

  inline_policy {
    name = "${var.role_name}_invoke_private_policy"

    policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action   = ["lambda:InvokeFunction"]
          Resource = var.lambda_arns
          Effect   = "Allow"
        }
      ]
    })
  }
}


Enter fullscreen mode Exit fullscreen mode

The security array receives a list of resources defined under components/securitySchemes



components:
  securitySchemes:
    authorizerRandom:
      name: authorizerRandom
      type: apiKey
      in: header
      x-amazon-apigateway-authtype: Custom scheme with corporate claims
      x-amazon-apigateway-authorizer:
        type: request
        authorizerUri: ${authorizer_lambda}
        authorizerCredentials: ${authorizer_credentials}
        authorizerResultTtlInSeconds: 0
        authorizerPayloadFormatVersion: "1.0"
        identitySource: "method.request.header.mykey"


Enter fullscreen mode Exit fullscreen mode

I recommend reading the detailed description of each field in the extensions link I posted above.

We are done with the OpenAPI specification, it's time to deploy it!



resource "aws_api_gateway_rest_api" "apigw" {
  name = "myapiv1"

  body = templatefile("${path.root}/openapi.yml.tpl", {
    endpoint_api1_lambda = aws_lambda_alias.hello_api_prd.invoke_arn
    endpoint_api1_role   = module.hello_resource_role.role_arn

    authorizer_lambda      = module.authorizer.lambda_function_invoke_arn
    authorizer_credentials = module.authorizer_resource_role.role_arn
  })

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}



Enter fullscreen mode Exit fullscreen mode

In this snippet, I am uploading the stringified template as the body of the API gateway. Terraform will substitute in the template the variables I am passing.

The final elements needed are the deployment and stage definitions.



resource "aws_api_gateway_deployment" "prd" {
  rest_api_id = aws_api_gateway_rest_api.apigw.id

  triggers = {
    redeployment = sha1(jsonencode(aws_api_gateway_rest_api.apigw.body))
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_stage" "prd" {
  deployment_id = aws_api_gateway_deployment.prd.id
  rest_api_id   = aws_api_gateway_rest_api.apigw.id
  stage_name    = "prd"
}


Enter fullscreen mode Exit fullscreen mode

Full Architecture

At this stage of our journey, we have a functioning API Gateway with protected endpoints. We lack an automated method to update our Lambdas when new images tagged in a certain way are pushed to our registry.

The complete architecture is as follows:

Image description

Instead of using the invoke ARN of the Lambda function, which points to the $LATEST version, I created an alias called "prd". This approach allows me to update the container to which the Lambda points without changing my API because the alias is linked to a specific version. The API changes only when I update the alias. To achieve this, I set up an EventBridge rule that listens for any push to ECR with a tag that begins with "v".



resource "aws_cloudwatch_event_rule" "api_push_version" {
  name        = "api_push_version"
  description = "Capture each v* tag pushed to ECR"

  event_pattern = jsonencode({
    source = ["aws.ecr"]
    detail-type = [
      "ECR Image Action"
    ]
    detail = {
      action-type     = ["PUSH"]
      result          = ["SUCCESS"]
      repository-name = [aws_ecr_repository.hello_api.name]
      image-tag       = [{ prefix = "v" }]
    }
  })
}


Enter fullscreen mode Exit fullscreen mode

One of the targets of the event rule is a Lambda function that performs the following tasks:

  • Publishes a new Lambda version
  • Retrieves the current alias version
  • Initiates a CodeDeploy deployment that updates the alias version to the newly published one

The final component is the CodeDeploy resource. The only required thing is a Codedeploy application and a deployment group. The deployment group specifies how lambdas are updated (CodeDeployDefault.LAmbdaAllAtOnce) along with rollback configurations. Two additional deployment configuration types are linear and canary. Further information on these configurations can be found here.



resource "aws_codedeploy_app" "this" {
  compute_platform = "Lambda"
  name             = var.lambda_name
}

resource "aws_codedeploy_deployment_group" "this" {
  app_name               = aws_codedeploy_app.this.name
  deployment_group_name  = var.lambda_name
  deployment_config_name = "CodeDeployDefault.LambdaAllAtOnce"
  service_role_arn       = aws_iam_role.this_codedeploy_role.arn

  deployment_style {
    deployment_option = "WITH_TRAFFIC_CONTROL"
    deployment_type   = "BLUE_GREEN"
  }

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_STOP_ON_ALARM", "DEPLOYMENT_FAILURE"]
  }

}


Enter fullscreen mode Exit fullscreen mode

And that's it! If you're interested in delving deeper into the code, I suggest checking out the repository, as some nuances would have made this blog post too lengthy.

Conclusion

In this blog post, I've shown how I like to define and manage API Gateway via OpenAPI and Terraform. The solution proposed utilizes container-based lambda with aliases. An EventBridge rule listens for pushes to the ECR repositories and triggers lambda functions that initiate a Codedeploy deployment to update the aliases to the last version.

Thank you for reading.

Top comments (0)