Chapter 4. Terraform Modules and Providers

Modules in Terraform are self-contained packages of Terraform configurations that are managed as a group. They are used to encapsulate code into reusable components and to organize code to facilitate teamwork and collaboration. Each module can include several resources that are configured to work together.

Modules can be compared to functions in traditional programming languages. Like a function, a module encapsulates a code block with a specific purpose and can be reused in different contexts. However, unlike a function, a module manages a collection of resources rather than performing computation.

Terraform modules are created using the same language syntax as the root-level Terraform configurations (.tf files). Using the module configuration block, they can be called from within other modules or the root module.

While modules help us organize resources and reuse code, Terraform Providers interact with external APIs to create, read, update, and delete those resources. Each provider is responsible for understanding API interactions and exposing resources.

Terraform is compatible with many cloud providers, including AWS, GCP, Microsoft Azure, and other service providers such as GitHub, Datadog, and many others.

4.1 Using Public Modules to Create an EKS Cluster

Problem

You have set up a VPC within AWS, and now you’d like to start hosting some applications on Kubernetes. You want to set up an Elastic Kubernetes Service (EKS) cluster and get it configured for the first time.

Solution

Using the public AWS EKS module on the Terraform Registry, you can efficiently set up your infrastructure with code configuration, which is what you need to get your Kubernetes cluster running on AWS. First, ensure you have an AWS account and the necessary permissions to create EKS clusters. Then, browse the Terraform Registry to find the module we’ll be using by searching for “terraform-aws-modules eks.”

The Terraform Registry shows documentation for the module (see Figure 4-1), what inputs are required, what outputs are provided, and other dependencies and resources available to us as we use the module.

Figure 4-1. Terraform Registry AWS EKS Terraform module listing

Since we’re building off the previous recipe we used for the VPC module in Chapter 3, we don’t have to respecify the AWS provider or create the VPC module again.

Our variables.tf file is mainly set up, but there are more variables we have to add, like the Kubernetes version we want to use and how we want to configure our worker node pool that is going to host our applications as we add them to our EKS cluster.

First, let’s add these values to our variables.tf file, which will be necessary for all the inputs we need for using the EKS module:

variable "cluster_version" {
  type        = string
  description = "The Kubernetes version for our clusters"
  default     = "1.30"
}

variable "cluster_instance_type" {
  type        = string
  description = "EC2 instance type for the EKS autoscaling group."
  default     = "m5.large"
}

variable "cluster_asg_desired_capacity" {
  type        = number
  description = "The default number of EC2 instances our EKS cluster runs."
  default     = 3
}

variable "cluster_asg_max_size" {
  type        = number
  description = "The maximum number of EC2 instances our EKS cluster will have."
  default     = 5
}

variable "cluster_enabled_log_types" {
  type        = list(string)
  description = "The log types that will be enabled for the EKS cluster."
  default     = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
}

variable "cluster_write_kubeconfig" {
  type        = bool
  description = "Toggle to output a Kubernetes configuration file. "
  default     = false
}

Next, let’s set up a Key Management Service (KMS) key for encrypting and securing our secrets within Kubernetes. Add a kms.tf file to your project with the following configuration:

resource "aws_kms_key" "eks" {
  description             = "EKS Secret Encryption Key"
  deletion_window_in_days = 7
  enable_key_rotation     = true
}

Now, let’s set up the EKS cluster by passing the defined variables and the KMS key details and working with Terraform’s data sources to authenticate to our EKS cluster:

data "aws_eks_cluster" "cluster" {
  name = module.eks.cluster_id
}

data "aws_eks_cluster_auth" "cluster" {
  name = module.eks.cluster_id
}

provider "kubernetes" {
  host                   = data.aws_eks_cluster.cluster.endpoint
  cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.ca.0.data)
  token                  = data.aws_eks_cluster_auth.cluster.token
}

module "eks" {
  source          = "terraform-aws-modules/eks/aws"
  version         = "20.24.0"
  cluster_name    = var.project_name
  cluster_version = var.cluster_version
  subnets         = module.vpc.private_subnets
  vpc_id          = module.vpc.vpc_id

  cluster_enabled_log_types = var.cluster_enabled_log_types
  write_kubeconfig          = var.cluster_write_kubeconfig

  cluster_encryption_config = [
    {
      provider_key_arn = aws_kms_key.eks.arn
      resources        = ["secrets"]
    }
  ]

  worker_groups = [
    {
      asg_desired_capacity = var.cluster_asg_desired_capacity
      asg_max_size         = var.cluster_asg_max_size
      instance_type        = var.cluster_instance_type
    }
  ]
}

Now that we have all our Terraform configurations, we can call terraform init to download our dependencies and create our Terraform lockfile.

Discussion

In this example, we’ve used Terraform’s public module registry to create an EKS cluster that we can use and extend. Here’s a breakdown of what we’ve done:

  • We’ve defined variables for our EKS cluster, including the Kubernetes version, instance type, autoscaling group settings, and logging configurations.

  • We’ve set up a KMS key for encrypting secrets within Kubernetes, enhancing the security of our cluster.

  • We’ve used the terraform-aws-modules/eks/aws module to create our EKS cluster, passing in our defined variables and configurations.

  • We’ve set up the Kubernetes provider to authenticate with our newly created EKS cluster.

One important note about this cluster is that we shouldn’t use Terraform for configuration management or deploying applications to the cluster. It’s best to keep the EKS authentication and instantiation separate from the configuration of applications running on the cluster. This separation allows for easier maintenance and upgrades.

If you apply this code, you can use terraform destroy to remove the cluster when you’re done experimenting or if you need to make significant changes. Remember to always review the created resources and associated costs before applying Terraform configurations in a production environment.

4.2 Linting Terraform with GitHub Actions

Problem

Ensuring that your Terraform configuration is linted correctly and runs as expected can be challenging, especially as you maintain your code over time or work with larger teams. You need an automated way to check your Terraform files for potential errors, best practices adherence, and code style consistency. Figure 4-2 illustrates a typical linting process for Terraform code, showing how automated checks can identify issues before they make it into production.

Figure 4-2. Linting Terraform with GitHub Actions

Solution

Using GitHub Actions and a dash of YAML configuration, we can create a workflow that automatically lints your Terraform code, along with other types of files in your repository. We’ll use Super-Linter, a robust tool that includes linters for multiple languages and file types, including Terraform.

To start this process, you must have a GitHub account and have GitHub Actions enabled on the repository where you’ll be pushing your code. You will need an existing Terraform configuration to test this recipe. Once you have chosen your Terraform configuration repository, create a .github/workflows directory at the root of your repository. Then, create a superlinter.yml file in the workflows folder with the following content:

name: "Code Quality: Super-Linter"

on:
  pull_request:

jobs:
  superlinter:
    name: Super-Linter
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3
        with:
          fetch-depth: 1

      - name: Lint Code
        uses: github/super-linter:v4
        env:
          VALIDATE_ALL_CODEBASE: true
          DEFAULT_BRANCH: "main"
          DISABLE_ERRORS: false
          VALIDATE_TERRAFORM: true
          VALIDATE_YAML: true
          VALIDATE_JSON: true
          VALIDATE_MD: true

This GitHub Action workflow will ensure that your Terraform, YAML, JSON, and Markdown files have been formatted correctly. This code will run during pull requests, helping to catch formatting issues before they enter your main branch. Next, let’s add a specific Terraform validation step. Create a terraform.yml file in the same workflows directory:

name: "Terraform Validation"

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  terraform:
    name: Terraform
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v1

      - name: Terraform Format
        run: terraform fmt -check -recursive

      - name: Terraform Init
        run: terraform init

      - name: Terraform Validate
        run: terraform validate

This workflow will run Terraform-specific checks, including formatting, initialization, and validation. Please see the visual in Figure 4-3.

Figure 4-3. GitHub Actions workflows providing Terraform feedback loops

Discussion

These two GitHub Actions workflows add significant value by providing automated oversight on your Terraform configuration. Here’s what each does:

  • The Super-Linter workflow:

    • Checks multiple file types, including Terraform, for style and syntax issues

    • Runs on pull requests to catch issues early in the development process

    • Can be extended to check additional file types or apply different rules

  • The Terraform-specific workflow:

    • Checks Terraform formatting using terraform fmt

    • Initializes the Terraform working directory

    • Validates the Terraform files for correctness

    • Runs on both pushes to the main branch and pull requests

By implementing these workflows, you can:

  • Catch errors and style issues early in the development process

  • Ensure consistency across your Terraform configurations

  • Reduce the time spent on manual code reviews for formatting and basic validation issues

  • Improve the overall quality and maintainability of your IaC

Remember, while these automated checks are valuable, they don’t replace the need for thorough code reviews and testing. They should be part of your Terraform configurations’ comprehensive quality assurance process.

Note

Implementing linting and validation early in your Terraform development process is a good practice. To ensure code quality, consider setting up these GitHub Actions workflows when creating your Terraform repository.

4.3 Authentication for Terraform Providers

Problem

You need to configure Terraform to authenticate with your infrastructure provider securely.

Solution

While this section appears later in the chapter, authentication is a fundamental concept typically set up at the beginning of your Terraform project. Let’s look at how to authenticate with the AWS provider using Terraform variables:

provider "aws" {
  region     = "us-west-2"
  access_key = var.aws_access_key
  secret_key = var.aws_secret_key
}

variable "aws_access_key" {
  description = "Access key for AWS provider"
  type        = string
  sensitive   = true
}

variable "aws_secret_key" {
  description = "Secret key for AWS provider"
  type        = string
  sensitive   = true
}

Discussion

Providers are a fundamental element of Terraform and are responsible for understanding API interactions and exposing resources. Terraform requires authentication to interact with an infrastructure provider.

In this HCL code, we’re authenticating with the AWS provider. AWS uses an access key and a secret key for programmatic access. For security purposes, we are using Terraform variables (var.aws_access_key and var.aws_secret_key) for the access_key and secret_key arguments to avoid hardcoding sensitive data into our Terraform configuration.

These variables are marked as sensitive to prevent the values from being shown in logs or console output. The values for these variables should be provided through the CLI when running Terraform or via a .tfvars file not committed to version control.

However, while this method is better than hardcoding credentials, it’s not the most secure or recommended practice for production environments. Here are some better practices for managing provider authentication:

Environment variables

Use environment variables to set credentials. For AWS, you can use AWS_ACCESS​_KEY_ID and AWS_SECRET_ACCESS_KEY.

Shared credentials file

AWS CLI uses a shared credentials file at ~/.aws/credentials. Terraform can use this same file.

IAM roles

If running Terraform from an EC2 instance, you can assign an IAM role to the instance, which Terraform can use automatically.

AWS Vault

A tool that securely stores and accesses AWS credentials in a development environment.

HashiCorp Vault

A secrets management tool that can dynamically generate AWS credentials.

Here’s an example of using shared credentials:

provider "aws" {
  region                   = "us-west-2"
  shared_credentials_file  = "~/.aws/credentials"
  profile                  = "dev"
}

Remember, each provider has its way of handling authentication. For example, the Azure provider uses either a service principal or Managed Service Identity (MSI). Always refer to the specific provider’s documentation for authentication best practices.

Finally, never commit sensitive information like access keys to version control. Always use a combination of variables, environment variables, or secure secret management tools to handle sensitive data in your Terraform configurations.

4.4 Authentication for Private Modules

Problem

You need to configure Terraform to authenticate with a private module registry to consume private modules.

Solution

To use private modules in Terraform, you need to authenticate with the private registry. Here’s how you can set this up:

  1. Specify the private module in your Terraform configuration:

    # Specify the required providers
    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.0"
        }
      }
    }
    
    # Use a private module from Terraform Cloud
    # Replace 'my-org' with your organization name
    # Replace 'my-module' with your module name
    module "my_module" {
      source  = "app.terraform.io/my-org/my-module"
      version = "1.0.0"
    }
    
    # Example usage of a private VPC module
    module "vpc" {
      source  = "app.terraform.io/my-company/vpc/aws"
      version = "1.0.0"
    
      vpc_cidr = "10.0.0.0/16"
      azs      = ["us-west-2a", "us-west-2b", "us-west-2c"]
    }
  2. To authenticate, you need to provide credentials. This can be done in two ways:

    1. Using a credentials block in the CLI configuration file (~/.terraformrc on Linux/MacOS, %APPDATA%\terraform.rc on Windows):

      credentials "app.terraform.io" {
        token = "your-api-token"
      }
    2. By setting the TERRAFORM_CONFIG environment variable to point to a separate credentials file:

      export TERRAFORM_CONFIG=/path/to/terraform.rc

      where /path/to/terraform.rc contains:

      credentials "app.terraform.io" {
        token = "your-api-token"
      }

Discussion

Private modules provide reusable, standard infrastructure patterns that are only accessible to you and your organization. They are usually stored in a private module registry such as HCP Terraform’s private module registry.

When using private modules, Terraform needs to authenticate with the private registry. This is typically done using an HCP Terraform API token. Here are some key points to remember:

Token security

The API token is sensitive information. Never commit it to version control or share it insecurely. Use environment variables or secure secret management tools to handle the token.

Token permissions

Ensure the token has the necessary permissions to access the private modules you’re using. In HCP Terraform, you can create tokens with specific permissions.

CI/CD integration

If you’re using Terraform in a CI/CD pipeline, you’ll need to securely provide the token to your build environment. Many CI/CD platforms offer secure ways to manage secrets.

Token rotation

Regularly rotate your API tokens as a security best practice. Update your Terraform configurations or environment variables when you do.

Least privilege

Use tokens with the minimum necessary permissions. For example, if you’re only reading modules, use a token that only has read permissions.

Remember, the authentication method may vary if you use a private registry other than HCP Terraform. Always consult the documentation of your specific registry for the most up-to-date authentication methods.

4.5 Creating a Terraform Module

Problem

You need to create a reusable Terraform module for a specific set of resources to be used in various environments or projects.

Solution

Here’s an example of how to create a basic Terraform module for provisioning an AWS EC2 instance:

# File: main.tf

# Define the EC2 instance resource
resource "aws_instance" "example" {
  ami           = var.ami
  instance_type = var.instance_type
  
  tags = {
    Name = var.instance_name
  }
}

# File: variables.tf

# Define input variables for the module
variable "ami" {
  description = "The AMI to use for the EC2 instance"
  type        = string
}

variable "instance_type" {
  description = "The type of EC2 instance to launch"
  type        = string
  default     = "t2.micro"
}

variable "instance_name" {
  description = "The Name tag for the EC2 instance"
  type        = string
}

# File: outputs.tf

# Define outputs from the module
output "instance_id" {
  description = "The ID of the instance"
  value       = aws_instance.example.id
}

output "instance_public_ip" {
  description = "The public IP address of the instance"
  value       = aws_instance.example.public_ip
}

To use this module, you would call it in another Terraform configuration file:

module "ec2_instance" {
  source        = "./my_module"
  ami           = "ami-abc123"
  instance_type = "t2.micro"
  instance_name = "my-instance"
}

output "instance_id" {
  value = module.ec2_instance.instance_id
}

output "instance_public_ip" {
  value = module.ec2_instance.instance_public_ip
}

Discussion

Modules in Terraform are self-contained packages of Terraform configurations that manage a collection of related resources. They are used to create reusable components, improve organization, and treat pieces of infrastructure as a cohesive unit.

Our example shows a simple module that creates an AWS EC2 instance. The module is composed of three files:

main.tf

This is where the resources that the module will create are defined. In our example, it creates an aws_instance.

variables.tf

This file defines the input variables used in the main.tf. Variables make your module flexible and usable in different contexts.

outputs.tf

This file defines the values the module will return to the calling code. It helps return IDs, names, or other attributes of the resources that the module creates.

Key points to remember when creating modules:

  • Modules should be focused on a specific task or group of related resources.

  • Use variables to make your module flexible and reusable across different environments.

  • Provide useful outputs that allow users of your module to access important information about the created resources.

  • Document your module by including a README.md file that explains what the module does, its inputs, outputs, and any other relevant information.

  • Consider versioning your modules, especially if they’re shared across teams or projects.

For testing modules, you can create a test directory in your module with example configurations that use the module. This allows you to verify that the module works as expected and provides examples for users of your module:

my_module/
├── main.tf
├── variables.tf
├── outputs.tf
├── README.md
└── tests/
    └── example_usage.tf

By creating well-structured, reusable modules, you can significantly improve the maintainability and consistency of your Terraform configurations across different projects and environments.

4.6 Managing GitHub Secrets with Terraform

Problem

You want to manage GitHub secrets for your repository using Terraform.

Solution

Here’s how to set up the GitHub provider and manage secrets using Terraform:

# Configure the GitHub provider
provider "github" {
  token = var.github_token  # Your GitHub personal access token
  owner = var.github_owner  # Your GitHub username or organization name
}

# Define a GitHub secret
resource "github_actions_secret" "example_secret" {
  repository      = var.github_repository
  secret_name     = "MY_SECRET"
  plaintext_value = var.my_secret
}

# Input variables
variable "github_token" {
  description = "GitHub personal access token"
  type        = string
  sensitive   = true
}

variable "github_owner" {
  description = "GitHub owner (username or organization)"
  type        = string
}

variable "github_repository" {
  description = "GitHub repository name"
  type        = string
}

variable "my_secret" {
  description = "The value of the GitHub secret"
  type        = string
  sensitive   = true
}

Discussion

This Terraform configuration allows you to manage GitHub secrets programmatically. Here’s a breakdown of what’s happening:

GitHub provider

The GitHub provider is configured with your personal access token and the owner (your username or organization name). This allows Terraform to interact with the GitHub API.

Secret resource

The github_actions_secret resource defines a secret in your GitHub repository. You specify the repository name, the secret name, and its value.

Variables

We use variables for sensitive information (like the GitHub token and secret value) and for values that might change between different uses of this configuration (like the repository name).

Key points to remember:

Security

The github_token and my_secret variables are marked as sensitive. This prevents their values from being displayed in console output or logs.

Token permissions

Ensure your GitHub token has the necessary permissions to manage secrets in the repository.

Secret management

While this method allows you to manage secrets with Terraform, be cautious about storing secret values in your Terraform state. Consider using a secure secret management system in conjunction with this approach.

Idempotency

Terraform will manage the life cycle of these secrets. If you run terraform apply multiple times, it will only update the secrets if there are changes.

Version control

While you should commit your Terraform configuration files, never commit files containing actual secret values (such as .tfvars files with sensitive data) to version control.

This approach provides a way to version control the existence and names of your secrets while still keeping the actual secret values secure. It’s particularly useful in CI/CD pipelines where you need to ensure certain secrets exist in your GitHub repositories.

Remember to handle the Terraform state file securely, as it will contain the plain-text values of your secrets. Consider using remote state with encryption enabled when managing sensitive data like this.

4.7 Managing GitHub Repositories with Terraform

Problem

You need to create and manage GitHub repositories using Terraform.

Solution

In Recipe 4.6, we set up the GitHub authentication. Now let’s dig into how we can work specifically with GitHub repositories and set up default options for the code we plan to work with:

# Configure the GitHub provider
provider "github" {
  token = var.github_token
  owner = var.github_owner
}

# Create a GitHub repository
resource "github_repository" "example" {
  name        = "example-repo"
  description = "Repository created and managed by Terraform"
  visibility  = "private"
  auto_init   = true

  template {
    owner      = "github"
    repository = "terraform-module-template"
  }

  topics = ["terraform", "infrastructure-as-code"]

  has_issues    = true
  has_wiki      = true
  has_downloads = false

  allow_merge_commit = true
  allow_squash_merge = true
  allow_rebase_merge = false
}

# Input variables
variable "github_token" {
  description = "GitHub personal access token"
  type        = string
  sensitive   = true
}

variable "github_owner" {
  description = "GitHub owner (username or organization)"
  type        = string
}

# Output the repository URL
output "repository_url" {
  value       = github_repository.example.html_url
  description = "URL of the created repository"
}

Discussion

This Terraform configuration allows you to create and manage GitHub repositories programmatically. Here’s a breakdown of the key components:

GitHub provider

The GitHub provider is configured with your personal access token and the owner (your username or organization name). This allows Terraform to interact with the GitHub API.

Repository resource

The github_repository resource defines the characteristics of the GitHub repository you want to create or manage.

Variables

We use variables for sensitive information (like the GitHub token) and for values that might change between different uses of this configuration (like the owner name).

Output

We define an output to easily retrieve the URL of the created repository.

Key points to remember:

Repository settings

The configuration includes various settings for the repository, such as visibility, initialization, issue tracking, wiki, and merge strategies. Adjust these as needed for your specific requirements.

Template

The template block allows you to create the repository based on a template. This can be useful for standardizing repository structures across your organization.

Topics

You can assign topics to your repository, which can help with categorization and discoverability.

Security

The github_token variable is marked as sensitive to prevent its value from being displayed in console output or logs.

Token permissions

Ensure your GitHub token has the necessary permissions to create and manage repositories.

Idempotency

Terraform will manage the life cycle of this repository. If you run terraform apply multiple times, it will only update the repository if there are changes in your configuration.

Existing Repositories

If the repository already exists, Terraform will import it into its state and manage it going forward. Be careful not to overwrite existing settings unintentionally.

This approach provides a way to version control your GitHub repository configurations, ensuring consistency across your organization and enabling easy replication of repository structures. It’s particularly useful for organizations that need to create and manage many repositories with standardized settings.

Remember to handle the Terraform state file securely, especially if managing private repositories or sensitive configurations. Consider using a remote state with encryption enabled when managing GitHub resources.

4.8 Dynamic Configuration with Consul KV

Problem

You must store and retrieve key-value pairs from HashiCorp Consul’s key-value (KV) store to use as dynamic configuration in your Terraform code. This is particularly useful when you need to pass dependencies across different Terraform states or when you want to externalize configuration that might change frequently. Figure 4-4 demonstrates the flow of information in a Consul key-value configuration setup. This diagram illustrates how Terraform interacts with Consul to store and retrieve dynamic configuration data, enabling more flexible and centralized management of infrastructure parameters.

Figure 4-4. Consul key-value configuration flows

Solution

Here’s an example of how to use Consul KV with Terraform to store and retrieve a VPC ID:

# Configure the Consul provider
provider "consul" {
  address = "localhost:8500"
  scheme  = "http"
}

# Store the VPC ID in Consul KV
resource "consul_key_prefix" "vpc" {
  path_prefix = "terraform/vpc/"

  subkeys = {
    "id" = aws_vpc.main.id
  }
}

# Retrieve the VPC ID from Consul KV
data "consul_keys" "vpc" {
  key {
    name = "vpc_id"
    path = "terraform/vpc/id"
  }
}

# Use the retrieved VPC ID
resource "aws_subnet" "example" {
  vpc_id     = data.consul_keys.vpc.var.vpc_id
  cidr_block = "10.0.1.0/24"
}

Discussion

This Terraform configuration demonstrates how to use Consul’s KV store for dynamic configuration. Here’s a breakdown of what’s happening:

Consul provider

We configure the Consul provider with the address of our Consul server. Adjust this as needed for your Consul setup.

Storing data

The consul_key_prefix resource is used to store the VPC ID in Consul’s KV store. In this example, we’re storing the ID of a VPC that we’ve created (represented by aws_vpc.main.id).

Retrieving data

The consul_keys data source is used to retrieve the VPC ID from Consul. This allows us to use the stored value in other parts of our Terraform configuration.

Using retrieved data

We use the retrieved VPC ID when creating a subnet, demonstrating how this data can be used in resource creation.

Key points to remember:

Consul setup

This solution assumes you have Consul set up and running. Setting up Consul is beyond the scope of this recipe, but it’s a prerequisite for using this configuration.

Key namespacing

We use a path prefix (terraform/vpc/) to namespace our keys. This is a good practice to organize your keys, especially if you’re using Consul for multiple purposes.

Data persistence

Consul KV allows you to persist data outside of Terraform’s state. This can be useful for sharing data between different Terraform configurations or runs.

Dynamic configuration

This approach allows you to change values in Consul and have those changes reflected in your Terraform runs without modifying the Terraform code itself.

Security

Ensure that your Consul setup is properly secured, especially if you’re storing sensitive information. Consider using ACLs and encryption.

Consul versus Terraform state

While this approach can be useful, it’s important to understand the trade-offs. Terraform state is designed to track resource dependencies, while Consul KV is more suited for dynamic, shared configuration.

This pattern is particularly useful in scenarios where:

  • You need to share data between different Terraform configurations or modules.

  • You want to externalize configuration that might change frequently.

  • You’re building a system where other processes might update configuration that Terraform needs to consume.

Remember, while this provides flexibility, it also introduces an external dependency to your Terraform runs. Ensure that your Consul cluster is reliable and that you have proper error handling in place in case Consul is unavailable.

4.9 Service-Health-Aware Provider Configuration

Problem

You want to configure Terraform providers based on the health status of your service endpoints, ensuring that your infrastructure deployment uses healthy services. Figure 4-5 provides a visual representation of how service health awareness is integrated into the provider configuration process. This diagram shows the flow of health check information and how it influences the selection of active service endpoints, ensuring that your infrastructure deployment uses only healthy services.

Figure 4-5. Service-health-aware provider configuration

Solution

Here’s a Terraform configuration that uses Consul’s HTTP API to check service health and configures an AWS provider based on the health check status:

# Data source to check service health via Consul HTTP API
data "http" "service_health_check" {
  url = "http://localhost:8500/v1/health/service/my-service"
}

# Local values to process the health check response
locals {
  service_health = jsondecode(data.http.service_health_check.body)[0]
  active_node    = local.service_health.Status == "passing" ? local.service_health.Service.Address : 
    "fallback_address"
}

# Configure the AWS provider using the health check result
provider "aws" {
  region  = "us-west-2"
  endpoint = local.active_node
}

# Example resource using the configured provider
resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
}

Discussion

This configuration demonstrates how to make Terraform provider configuration dynamic based on service health. Here’s a breakdown of the key components:

Health check data source

The http data source is used to query Consul’s HTTP API for the health status of a specific service (my-service in this example).

Response processing

The jsondecode function is used to parse the JSON response from Consul. We assume the first service instance is the one we’re interested in.

Dynamic endpoint selection

A ternary operator is used to select either the address of the healthy service or a fallback address, based on the health check status.

Provider configuration

The AWS provider is configured with the dynamically selected endpoint.

Key points to consider:

Consul dependency

This solution assumes you have Consul running and that your services are registered with Consul. Adjust the URL in the HTTP data source to match your Consul setup.

Error handling

This example doesn’t include robust error handling. In a production scenario, you’d want to add checks for empty responses, multiple service instances, and potential API failures.

Security

The example uses an unsecured HTTP call to Consul. In a production environment, you should use HTTPS and proper authentication.

Fallback strategy

The example uses a simple fallback to a predefined address. In practice, you might want a more sophisticated fallback strategy, possibly querying multiple services or using a default provider configuration.

Provider-specific considerations

The exact configuration will depend on the provider you’re using. Not all providers support dynamic endpoint configuration in the same way.

Timing and caching

Remember that this health check happens during Terraform’s planning and apply phases. It doesn’t continuously monitor service health during execution.

This pattern is particularly useful in scenarios where:

  • You have multiple service endpoints and want to use the healthy ones automatically.

  • You’re implementing a form of failover or high availability in your infrastructure provisioning.

  • You want to ensure that Terraform operations only proceed when dependent services are healthy.

While this approach provides flexibility, it also introduces complexity and external dependencies into your Terraform workflow. Ensure that your health checking mechanism is reliable and that you have appropriate error handling and logging.

4.10 Consuming Terraform State with Providers

Problem

You need to access and use the Terraform state from one configuration in another configuration, allowing you to share information between separate Terraform projects or modules.

Solution

First, you must ensure that the state from the first configuration is stored in a backend that the second configuration can access:

# Configure the backend where the state is stored
terraform {
  backend "s3" {
    bucket = "my-terraform-state-bucket"
    key    = "network/terraform.tfstate"
    region = "us-west-2"
  }
}

# Data source to access the remote state
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-terraform-state-bucket"
    key    = "network/terraform.tfstate"
    region = "us-west-2"
  }
}

# Example resource using data from the remote state
resource "aws_instance" "app_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  subnet_id     = data.terraform_remote_state.network.outputs.subnet_id

  tags = {
    Name = "AppServerInstance"
  }
}

# Output demonstrating access to the remote state
output "vpc_id" {
  value = data.terraform_remote_state.network.outputs.vpc_id
}

Discussion

This configuration shows how to consume Terraform state from one configuration in another. Here’s a breakdown of the key components:

Backend configuration

The terraform block configures the backend where the state is stored. In this example, we’re using an S3 backend.

Remote state data source

The terraform_remote_state data source is used to access the outputs from another Terraform configuration’s state.

Resource creation

We create an AWS EC2 instance, using the subnet_id output from the remote state.

Output

We demonstrate how to access and output a value (vpc_id) from the remote state.

Key points to consider:

State storage

Ensure that your state is stored in a backend that’s accessible to both configurations. S3 is a common choice, but Terraform supports various backend types.

State isolation

While sharing state can be useful, it’s important to maintain proper separation of concerns. Only share what’s necessary between configurations.

Version control

The remote state feature allows you to version control your infrastructure more effectively by breaking it into smaller, manageable pieces.

Security

Be mindful of the permissions required to access the remote state. Ensure that your backend is properly secured and that access is restricted as necessary.

Dependency management

Using remote state creates implicit dependencies between your Terraform configurations. Be aware of these dependencies and manage them carefully.

Read-only access

The terraform_remote_state data source provides read-only access to the state. You can’t modify the remote state through this method.

Performance

Accessing remote state adds some overhead to your Terraform operations. This is usually negligible but could be noticeable for very large states or slow network connections.

This pattern is particularly useful in scenarios where:

  • You want to separate your infrastructure into logical components (e.g., networking, compute, database) while still allowing them to reference each other.

  • You need to share information between different teams or projects without tightly coupling their Terraform configurations.

  • You’re implementing a layered or modular approach to your infrastructure as code.

Remember that while consuming remote state can be powerful, it also introduces coupling between your Terraform configurations. Use this feature judiciously and always consider the trade-offs between sharing state and maintaining isolation between different parts of your infrastructure.

4.11 Using Multiple, Identical Providers

Problem

You need to manage resources across multiple regions or accounts using the same provider type in a single Terraform configuration.

Solution

Here’s a Terraform configuration that demonstrates how to use multiple AWS providers to manage resources in different regions:

# Configure the default AWS provider
provider "aws" {
  region = "us-west-2"
}

# Configure an additional AWS provider for the US East region
provider "aws" {
  alias  = "east"
  region = "us-east-1"
}

# EC2 instance in the default region (us-west-2)
resource "aws_instance" "west_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  tags = {
    Name = "WestServerInstance"
  }
}

# EC2 instance in the US East region
resource "aws_instance" "east_server" {
  provider      = aws.east
  ami           = "ami-0747bdcabd34c712a"
  instance_type = "t2.micro"

  tags = {
    Name = "EastServerInstance"
  }
}

# S3 bucket in the default region
resource "aws_s3_bucket" "west_bucket" {
  bucket = "my-west-bucket-12345"
}

# S3 bucket in the US East region
resource "aws_s3_bucket" "east_bucket" {
  provider = aws.east
  bucket   = "my-east-bucket-67890"
}

Discussion

This configuration demonstrates how to use multiple instances of the same provider (in this case, AWS) to manage resources across different regions. Here’s a breakdown of the key components:

Default provider

The first AWS provider block configures the default provider, which will be used when no specific provider is referenced.

Additional provider

The second AWS provider block sets up an additional provider with an alias ("east") for the US East region.

Resource creation

We create EC2 instances and S3 buckets in both regions, demonstrating how to specify which provider to use for each resource.

Key points to consider:

Provider aliases

The alias argument in the provider block allows you to create multiple instances of the same provider with different configurations.

Resource provider specification

For resources using a nondefault provider, you need to specify the provider using the provider argument, referencing it as aws.east (where "east" is the alias).

Region-specific AMIs

We use different AMI IDs for the EC2 instances in different regions. AMIs are region-specific, so you must ensure you use the correct AMI for each region.

Naming conventions

It’s a good practice to use clear naming conventions for resources created in different regions or accounts to avoid confusion.

State management

While this configuration manages resources across regions, all of these resources are still in the same Terraform state. Consider whether this is appropriate for your use case or if you need separate states.

IAM and permissions

Ensure that the credentials used have the necessary permissions in all regions where you create resources.

Increased complexity

While this approach is powerful, it can increase the complexity of your Terraform configuration. Use it judiciously.

This pattern is particularly useful in scenarios where:

  • You must deploy similar infrastructure across multiple regions for redundancy or global distribution.

  • You’re managing resources across different AWS accounts.

  • You want to compare or test configurations across different regions.

Remember that while this approach allows you to manage multiregion or multiaccount resources in a single configuration, it’s not always the best solution. Consider using separate Terraform configurations for each region or Terraform workspaces for complex multiregion setups.

Get Terraform Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.