Reusable infrastructure with Terraform modules

Reusable infrastructure with Terraform modules

A module is a container for multiple resources that are used together. Modules can be used to create lightweight abstractions, so that you can describe your infrastructure in terms of its architecture, rather than directly in terms of physical objects.

In today's post, we will understand the benefits of using Terraform modules through a simple example, but first, we need to fulfill some prerequisites:

We will build an API to get quotes from two popular animes using AWS API Gateway as a proxy against the AnimeChan API:

  • https://<AWS_RANDOM_DOMAIN>/anime/naruto
  • https://<AWS_RANDOM_DOMAIN>/anime/inuyasha

Create a main.tf file with the following content:

terraform {
  required_providers {
    aws = {
        source = "hashicorp/aws"
        version = "4.22.0"
    }
  }
}

provider "aws"{
  region = "us-east-2"
}

resource "aws_api_gateway_rest_api" "my_api" {
  name        = "my-anime-quotes-api"
  description = "My Anime Quotes API"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

resource "aws_api_gateway_resource" "naruto_resource" {
  rest_api_id = aws_api_gateway_rest_api.my_api.id
  parent_id   = aws_api_gateway_rest_api.my_api.root_resource_id
  path_part   = "naruto"
}

resource "aws_api_gateway_method" "naruto_resource_get_method" {
  rest_api_id      = aws_api_gateway_rest_api.my_api.id
  resource_id      = aws_api_gateway_resource.naruto_resource.id
  http_method      = "GET"
  authorization    = "NONE"
  api_key_required = false
}

resource "aws_api_gateway_integration" "naruto_resource_get_method_integration" {
  rest_api_id             = aws_api_gateway_rest_api.my_api.id
  resource_id             = aws_api_gateway_resource.naruto_resource.id
  http_method             = aws_api_gateway_method.naruto_resource_get_method.http_method
  type                    = "HTTP"
  uri                     = "https://animechan.vercel.app/api/quotes/anime?title=naruto"
  integration_http_method = aws_api_gateway_method.naruto_resource_get_method.http_method
}

resource "aws_api_gateway_method_response" "naruto_200_method_response" {
  rest_api_id = aws_api_gateway_rest_api.my_api.id
  resource_id = aws_api_gateway_resource.naruto_resource.id
  http_method = aws_api_gateway_method.naruto_resource_get_method.http_method
  status_code = "200"
}

resource "aws_api_gateway_integration_response" "naruto_200_integration_response" {
  rest_api_id       = aws_api_gateway_rest_api.my_api.id
  resource_id       = aws_api_gateway_resource.naruto_resource.id
  http_method       = aws_api_gateway_method.naruto_resource_get_method.http_method
  status_code       = aws_api_gateway_method_response.naruto_200_method_response.status_code
  selection_pattern = "2\\d{2}"
}

resource "aws_api_gateway_deployment" "deployment_api" {
  rest_api_id = aws_api_gateway_rest_api.my_api.id

  triggers = {
    redeployment = sha1(jsonencode([
      aws_api_gateway_resource.naruto_resource.id,
      aws_api_gateway_method.naruto_resource_get_method.id,
      aws_api_gateway_integration.naruto_resource_get_method_integration.id,
      aws_api_gateway_method_response.naruto_200_method_response.id,
      aws_api_gateway_integration_response.naruto_200_integration_response.id
    ]))
  }
    lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_stage" "stage_api" {
  rest_api_id   = aws_api_gateway_rest_api.my_api.id
  stage_name    = "anime"
  deployment_id = aws_api_gateway_deployment.deployment_api.id
  description   = "anime"
}

resource "aws_api_gateway_method_settings" "method_settings" {
  rest_api_id = aws_api_gateway_rest_api.my_api.id
  stage_name  = aws_api_gateway_stage.stage_api.stage_name
  method_path = "*/*"

  settings {
    caching_enabled        = false 
    cache_ttl_in_seconds   = 120
    metrics_enabled        = true
    logging_level          = "INFO"
  }
}

Let's review each Terraform resource to have a general idea of them:

  • aws_api_gateway_rest_api: Manages an API Gateway REST API.
  • aws_api_gateway_resource: Provides an API Gateway Resource.
  • aws_api_gateway_method: Provides a HTTP Method for an API Gateway Resource.
  • aws_api_gateway_integration: Provides an HTTP Method Integration for an API Gateway Integration.
  • aws_api_gateway_method_response: Provides an HTTP Method Response for an API Gateway Resource.
  • aws_api_gateway_integration_response: Provides an HTTP Method Integration Response for an API Gateway Resource.
  • aws_api_gateway_deployment: Manages an API Gateway REST Deployment. A deployment is a snapshot of the REST API configuration.
  • aws_api_gateway_stage: Manages an API Gateway Stage. A stage is a named reference to a deployment.
  • aws_api_gateway_method_settings: Manages API Gateway Stage Method Settings.

To create the resources, run the following commands:

terraform init
terraform plan -out app.tfplan
terraform apply 'app.tfplan'

At this point, if we review the AWS Console, we will have our first URL created:

naruto.PNG

To add the remaining anime we could copy and paste the resources aws_api_gateway_resource, aws_api_gateway_method, aws_api_gateway_integration, aws_api_gateway_method_response and aws_api_gateway_integration_response or build a Terrform module.

Basics

A Terraform module is any set of Terraform files in a folder. Create the following structure:

|-- modules
|   |-- api_gateway_get_resource
|   |     |-- main.tf
|   |     |-- variables.tf
|   |     `-- outputs.tf
|    `--
`-- main.tf

Go the api_gateway_get_resource folder level and open the main.tf file and copy the following content:

resource "aws_api_gateway_resource" "anime_resource" {
  rest_api_id = var.rest_api_id
  parent_id   = var.root_resource_id
  path_part   = var.anime_name
}

resource "aws_api_gateway_method" "anime_resource_get_method" {
  rest_api_id      = var.rest_api_id
  resource_id      = aws_api_gateway_resource.anime_resource.id
  http_method      = "GET"
  authorization    = "NONE"
  api_key_required = false
}

resource "aws_api_gateway_integration" "anime_resource_get_method_integration" {
  rest_api_id             = var.rest_api_id
  resource_id             = aws_api_gateway_resource.anime_resource.id
  http_method             = aws_api_gateway_method.anime_resource_get_method.http_method
  type                    = "HTTP"
  uri                     = "https://animechan.vercel.app/api/quotes/anime?title=${var.anime_name}"
  integration_http_method = aws_api_gateway_method.anime_resource_get_method.http_method
}

resource "aws_api_gateway_method_response" "anime_200_method_response" {
  rest_api_id = var.rest_api_id
  resource_id = aws_api_gateway_resource.anime_resource.id
  http_method = aws_api_gateway_method.anime_resource_get_method.http_method
  status_code = "200"
}

resource "aws_api_gateway_integration_response" "anime_200_integration_response" {
  rest_api_id       = var.rest_api_id
  resource_id       = aws_api_gateway_resource.anime_resource.id
  http_method       = aws_api_gateway_method.anime_resource_get_method.http_method
  status_code       = aws_api_gateway_method_response.anime_200_method_response.status_code
  selection_pattern = "2\\d{2}"
}

Inputs

The module entries will be in the variable.tf file. Copy the following content there:

variable "anime_name" {
  description = "anime"
  type        = string
}

variable "rest_api_id" {
  description = "rest_api_id"
  type        = string
}

variable "root_resource_id" {
  description = "root_resource_id"
  type        = string
}

Outputs

The module outputs will be in the outputs.tf file. Copy the following content there:

output "aws_api_gateway_resource_id" {
  value       = aws_api_gateway_resource.anime_resource.id
}

output "aws_api_gateway_method_id" {
  value       = aws_api_gateway_method.anime_resource_get_method.id
}

output "aws_api_gateway_integration_id" {
  value       = aws_api_gateway_integration.anime_resource_get_method_integration.id
}

output "aws_api_gateway_method_response_id" {
  value       = aws_api_gateway_method_response.anime_200_method_response.id
}

output "aws_api_gateway_integration_response_id" {
  value       = aws_api_gateway_integration_response.anime_200_integration_response.id
}

Usage

Go back to the root level and open the main.tf, replace the content with:

terraform {
  required_providers {
    aws = {
        source = "hashicorp/aws"
        version = "4.22.0"
    }
  }
}

provider "aws"{
  region = "us-east-2"
}

resource "aws_api_gateway_rest_api" "my_api" {
  name        = "my-anime-quotes-api"
  description = "My Anime Quotes API"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

module "naruto" {
  source           = "./modules/api_gateway_get_resource"
  anime_name       = "naruto"
  rest_api_id      = aws_api_gateway_rest_api.my_api.id
  root_resource_id = aws_api_gateway_rest_api.my_api.root_resource_id
}

module "inuyasha" {
  source           = "./modules/api_gateway_get_resource"
  anime_name       = "inuyasha"
  rest_api_id      = aws_api_gateway_rest_api.my_api.id
  root_resource_id = aws_api_gateway_rest_api.my_api.root_resource_id
}

resource "aws_api_gateway_deployment" "deployment_api" {
  rest_api_id = aws_api_gateway_rest_api.my_api.id

  triggers = {
    redeployment = sha1(jsonencode([
      module.naruto.aws_api_gateway_resource_id,
      module.naruto.aws_api_gateway_method_id,
      module.naruto.aws_api_gateway_integration_id,
      module.naruto.aws_api_gateway_method_response_id,
      module.naruto.aws_api_gateway_integration_response_id,

      module.inuyasha.aws_api_gateway_resource_id,
      module.inuyasha.aws_api_gateway_method_id,
      module.inuyasha.aws_api_gateway_integration_id,
      module.inuyasha.aws_api_gateway_method_response_id,
      module.inuyasha.aws_api_gateway_integration_response_id
    ]))
  }
    lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_stage" "stage_api" {
  rest_api_id   = aws_api_gateway_rest_api.my_api.id
  stage_name    = "anime"
  deployment_id = aws_api_gateway_deployment.deployment_api.id
  description   = "anime"
}

resource "aws_api_gateway_method_settings" "method_settings" {
  rest_api_id = aws_api_gateway_rest_api.my_api.id
  stage_name  = aws_api_gateway_stage.stage_api.stage_name
  method_path = "*/*"

  settings {
    caching_enabled        = false 
    cache_ttl_in_seconds   = 120
    metrics_enabled        = true
    logging_level          = "INFO"
  }
}

Run the following command to destroy the previous resources:

terraform plan -destroy -out app.tfplan
terraform apply -destroy 'app.tfplan'

Then, run:

terraform plan -out app.tfplan
terraform apply 'app.tfplan'

Now, in the AWS Console, we will see:

inuyasha.PNG

I hope this simple example was enough to understand how powerful Terraform modules can be. The final source code is available here. Thanks, and happy coding.