How to Succeed at Container Migrations on AWS
How to Succeed at Container Migrations on AWS
Terraform is a powerful tool for managing infrastructure as code, and as your usage grows, so does the complexity of your modules.
This article is aimed at intermediate to advanced users who are familiar with terraform basics, but want to refine their approach to developing robust, maintainable modules. We’ll delve into design considerations, the importance of defensive programming, and provide practical examples to illustrate key points.
These are a handful of select mentions that we consider when we hand solution to our clients.
As modules grow larger, grouping related variables into objects can be beneficial. Using sets or maps for variable grouping allows for cleaner configuration and easier management.
An engineer may traditionally define the following variables as such:
variable "ec2_name" {
type = string
description = "The friendly name of a EC2 instance that should be created."
}
variable "ec2_replica_count" {
type = number
description = "The number of EC2 instances to create."
}
variable "ec2_ami" {
type = string
description = "The image that should be used to create the EC2 instances."
}
variable "ec2_instance_type" {
type = string
description = "The instance type or size that we should use."
}
If we were to continue down this path of individually defining variables rather than grouping, we would find it hard to continue to manage the solution if it were to continue to grow. We can work around this by performing something similar to the following:
variable "ec2" {
type = object({
name = string
ami_id = string
replica_count = number
instance_type = string
})
}
We can eliminate a lot of the repetitive boilerplate code by using the count argument to tell terraform that we want
to loop over a resource. In this case, we can provide an number (var.ec2.replica_count
) that represents the number
of resources of this exact configuration that we want to create.
resource "aws_instance" "company_webservers" {
count = var.ec2.replica_count
ami = var.ec2.ami_id
instance_type = var.ec2.instance_type
tags = {
"Name" = "company_webserver_${count.index}"
}
}
Providing default values for variables can simplify module reuse.
When defining a variable that is a set or a map and attempting to set a default value, a common gripe si that you have to populate all the keys instead of the individual ones that you would like to have a default.
It may be practical to either use terraform's coalesce or lookup function to define a local variable in circumstances where you are going to use the module that you are writing more than a handful of times.
locals {
defaults = {
ec2 = {
# Example using Coalesce Function.
instance_type = coalesce(var.ec2.instance_type, "t2.micro")
# Example using Lookup Function
name = lookup(var.ec2, "name", "default_webserver_name")
}
}
}
One effective practice is to avoid using data sources within modules. Instead, call data sources in the root module and pass them as a variable to the child modules.
This approach can significantly reduce the number of calls made during the state and data refresh stage, which can become time-consuming as the number of managed resources and data sources grows.
There is an inevitable tradeoff of convenience, but if your module does grow significantly it may be more beneficial to implement this consideration if your deployment times are larger than desirable.
Preconditions and Postconditions can be used to guarantee that resources follow a strictly defined behavior. This can be useful when performing changes later in a module when unexpected behavior change can manifest, as it could prevent a deployment where something may be broken.
resource "aws_instance" "company_web_server" {
ami = var.ec2.ami_id
instance_type = var.ec2.instance_type
lifecycle {
precondition {
condition = startswith(var.ec2.instance_type, "t2.")
error_message = "Instance type must be of t2 family."
}
}
tags = {
"Name": "company_web_server"
}
}
resource "null_resource" "webserver_is_running_check" {
lifecycle {
postcondition {
condition = aws_instance.company_web_server.instance_state == "running"
error_message = "Instance must be running."
}
}
}
In the above example, we ensure that this particular resource can only be created with a instance type of the t2
family. This could be a realistic situation for someone who wants to ensure that a specific kind of instances can only
be used for a particular deployment, or for cost saving reasons.
We can also make use out of a null resource in instances where resources cannot interact with itself - We reference
the aws_instance
here to guarantee the state.
Read more about the latest and greatest work Rearc has been up to.
How to Succeed at Container Migrations on AWS
Rearc at AWS re:Invent 2024: A Journey of Innovation and Inspiration
A People-First Vocation: People Operations as a Calling
Use Azure's Workload Identity Federation to provide an Azure Pipeline the ability to securely access AWS APIs.
Tell us more about your custom needs.
We’ll get back to you, really fast
Kick-off meeting