Architecture
| Julian Alarcon |
25 June 2019
I created this post for people who plan to start using Terraform on a project, in the hope it may help to save some time by sharing some of my lessons learned. And yes, the title is true – I wish I had known most of these lessons before starting to work with Terraform. I have split 11 lessons across two posts - here is part 1.
1. Always use Terraform
Terraform is a tool for building, changing and versioning infrastructure safely and efficiently, helping you to define your Infrastructure as Code (IaC). Using Terraform and then making changes with other tools besides Terraform (eg Web Consoles, CLI Tools, or SDK) will create inconsistencies and affect the stability and confidence of the infrastructure.
Terraform will try to maintain the previously defined state, and any of the manual changes won't be on the defined VCS, so if a redeployment is required, those changes will be lost.
Exceptions can be necessary, but these are only for specific needs like security restrictions (Key Pairs) or the specific debugging of issues (security group rules). But keep in mind that these changes should affect controlled components.
2. Use modules to avoid repetitive work
How do you avoid having to copy and paste the code for the same app deployed in multiple environments, such as stage/services/frontend-app and prod/services/frontend-app?
Modules in Terraform allow you to reuse predefined resource structures. Using modules will decrease the snowflake effect and provide a great way to reuse existing infrastructure code.
Modules have some variables as inputs, which are located in different places (eg. A different folder, or even a different repository). They define elements from a provider and can define multiple resources in themselves:
# my_module.tf:
resource "aws_launch_configuration" "launch_configuration" {
name = "${var.environment}-launch-configuration-instance"
image_id = "ami-04681a1dbd79675a5"
instance_type = "t3.micro"
}
resource "aws_autoscaling_group" "autoscaling_group" {
launch_configuration = "${aws_launch_configuration.launch_configuration.id}"
availability_zones = ["us-east-1a"]
min_size = "${var.min_size}"
max_size = "${var.max_size}"
}
Modules are called using the module block in our Terraform configuration file, variables are defined according to the desired requirement. In the example above, we call the module twice but with different values for each different environment.
This uses the module my_module and creates an AutoScaling Group with a minimum instance size of 1 and maximum of 2 and a Launch Configuration. Both resources are defined with a specific prefix name, in this case dev:
# my_dev.tf
module "develpment_frontend" {
source = "./modules/my_module"
min_size = 1
max_size = 2
environment = "dev"
}
Afterwards, we can reuse the module. In our production environment, we call the same module my_module, and create the ASG with a minimum size of instances of 2 and maximum of 4, and the Launch Configuration, both with the specified prod prefix.
# my_prod.tf
module "development_frontend" {
source = "./modules/my_module"
min_size = 2
max_size = 4
environment = "prod"
}
Figure 1. Relation Terraform config files and modules
It’s recommended to define and use different versions of a specific module which allows us to work using a version control system.
If we store our modules in a VCS, for example git, we can use tags or branches names to call a specific version using the ?ref= option:
module "my-db-module" {
source = "git::ssh://git@mygitserver.com/my-modules.git//modules/my_module?ref=feature-branch-001"
allocated_storage = "200"
instance_class = "db.t2.micro"
engine = "postgres"
3. Manage Terraform State
The Terraform state file is important for Terraform because all the current states of our Infrastructure are stored here. It's a .json file normally located in the hidden folder .terraform inside your Terraform configuration files (.terraform/terraform.tfstate) and is autogenerated when you execute the command terraform apply. The direct file editing of the state file is not recommended.
This is an example of a terraform.tfstate file:
{
"version": 3,
"terraform_version": "0.11.8",
"lineage": "35a9fcf6-c658-3697-9d74-480408535ce6",
"modules": [
{
"path": ["root"],
……………………………………
"depends_on": []
}
]&
}
As we work on our infrastructure, other collaborators might need to modify the infrastructure and apply their changes, changing the Terraform state file, which is why we recommend that this file be stored in a shared storage. Terraform supports multiple Backends to store this file, like etcd, azurem, S3 or Consul.
This is an example of how to define the path of the Terraform State file using the S3 provider. A DynamoDB is also used to control the lock access to the file, needed in case someone else is editing the Infrastructure at the same time. This will lock the write access to just one user at a time.
terraform {
required_version = ">= 0.11.7"
backend "s3" {
encrypt = true
bucket = "bucket-with-terraform-state"
dynamodb_table = "terraform-state-lock"
region = "us-east-1"
key = "locking_states/terraform.tfstate"
}
}
As the Terraform state file could store delicate information, it's recommended to encrypt the Storage using the options provided by the Backends.
Also, as your Infrastructure grows and you need to define multiple environments, you might need to split your Terraform state by environments and by components inside each environment. This way you will be able to work on different environments at the same time and multiple collaborators could work on different components of the same Infrastructure without being locked (one user modifying Databases and another modifying Load Balancers).
This can be achieved using the specific key component in the Backend definition:
# my_infra/prod/database/main.tf:
...
key = "prod/database/terraform.tfstate"
...
# my_infra/dev/database/main.tf:
...
key = "dev/database/terraform.tfstate"
...
# my_infra/dev/loadbalancer/main.tf:
...
key = "dev/loadbalancer/terraform.tfstate"
...
Figure 2 - Terraform backend files
4. Split everything
I mentioned earlier, splitting the Terraform state by Environments and by Components will help you build all the different components of you infrastructure in isolation from each other. What kind of division should you manage? That depends on the size of the project, its complexity and the size of your team.
For example, some components options can be defined inside themselves as inline blocks. But sometimes it's recommended to define these structures in a different resource. In this example an AWS Route Table has the routes definition inline:
resource "aws_route_table" "route_table" {
vpc_id = "${aws_vpc.vpc.id}"
route {
cidr_block = "10.0.1.0/24"
gateway_id = "${aws_internet_gateway.example.id}"
}
}
Alternatively, you can create the exact same route as a separate AWS Route resource:
resource "aws_route_table" "route_table" {
vpc_id = "${aws_vpc.vpc.id}"
}
resource "aws_route" "route_1" {
route_table_id = "${aws_route_table.route_table.id}"
destination_cidr_block = "10.0.1.0/24"
gateway_id = "${aws_internet_gateway.example.id}"
}
This allows you to be more flexible in the definition of your Infrastructure as it increases in complexity. Just keep in mind that it's easier to group components once they are defined, rather than splitting them after you have already deployed your Infrastructure.
As the level of complexity increases, you can deploy all your infrastructure with one command, using Bash scripts or tools like Ansible or Terragrunt.
I hope you found this helpful! Stay tuned to read the second part of the lessons I’ve learned through working with Terraform and in the meantime you can consult the Terraform documentation to help you get started with the basics.
Julian Alarcon
DevOps Engineer
Julian is a DevOps engineer who loves open source software culture, sharing his knowledge and working on weird and wonderful projects that get him to think outside the box. He also enjoys learning from new cultures and tries to experience one new thing every day. With a technical background spanning almost 10 years, Julian and his team help bring big ideas to life. He is also a coffee lover from a coffee country, an amateur photographer and a great conversationalist, especially when beer is involved.All Categories
Related Articles
-
07 February 2022
Using Two Cloud Vendors Side by Side – a Survey of Cost and Effort
-
25 January 2022
Scalable Microservices Architecture with .NET Made Easy – a Tutorial
-
24 August 2021
EHR to HL7 FHIR Integration: The Software Developer’s Guide – Part 3
-
20 July 2021
EHR to HL7 FHIR Integration: The Software Developer’s Guide – Part 2
-
29 June 2021
EHR to HL7 FHIR Integration: The Software Developer’s Guide – Part 1