Simple AWS API Gateway with OpenAPI and Terraform
Tags: terraform, openapi, codedeploy, aws
Published: 2024-03-11 08:27:11

## 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](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions.html), 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](https://github.com/ggrangia/aws-terraform-collection/tree/main/api-gw-lambda-ecr) 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](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.1.md).

Now, let's explore the definition.
### Lambda Proxy Integration
```yml
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: []

```
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.

```terraform
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"
        }
      ]
    })
  }
}
```
The *security* array receives a list of resources defined under *components/securitySchemes*

```yml
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"
```
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!

```terraform
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"]
  }
}

```

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.

```terraform
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"
}
```


## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xuzvlhzjmqdcu8kcmrm7.jpg)


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"*.

```terraform
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" }]
    }
  })
}
```

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](https://docs.aws.amazon.com/codedeploy/latest/userguide/deployment-configurations.html).

```terraform
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"]
  }

}
```

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.