Day 9/30 of terraform: Module Management.
In day 8 modules come in handy to help avoid duplication of infrastructure logic while allowing us to share logic across environments. Today we are taking it a step further to build a reusable web server module, version it using git and deploy to different environments that is dev and production.
1. Our project structure;
We are going to group the reusable infrastructure logic into its own folder called modules and have another folder called live which will contain various environments/ deployments which are dev and production. Here environments only define configuration not infrastructure logic.
terraform-webserver-cluster/
├── modules/
├── live/2. Module (Core infrastructure)
Here we will create security group, launch EC2 instances and configure startup script. For the security group we see every server needs controlled access: HTTP (port 80) SSH (port 22)
First gotcha
resource "aws_security_group" "web_sg" {
name = "web-sg"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}In the above we use inline rules at first it looks okay but looking at it again it says the resource controls all rules for this security group. Adding another separate rule now terraform sees the aws_security_group owns all the rules and aws_security_group_rule also owns rules its like we have two controllers with one system.
type = "ingress"
security_group_id = aws_security_group.web_sg.id
}The above becomes a pain point as rules keeps on changing every run, terraform never stabilizes and which could lead to production outage. The fix: Each rule is independently managed and explicitly defined and can be extended by module users.
resource "aws_security_group" "web_sg" {
name = "web-sg"
}
resource "aws_security_group_rule" "http" {
type = "ingress"
security_group_id = aws_security_group.web_sg.id
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}3. Handling file paths in modules
Modules will often include template files, configuration files or user scripts which will require us to handle file paths properly. At first we may think why not use the file function and then include the path to the user script like below.
user_data = file("./user-data.sh")A problem arises as terraform resolves path from where you run terraform not from the modules folder. Module may work locally but on CI/CD it fails and of course file not found errors are unavoidable using that approach. The fix: use path.module which points to the actual module location.
server_port = var.server_port
})4. Adding EC2 Instances.
Here we create EC2 that scales based on input variables, reuses security_group and runs a start up script.
count = var.min_size
instance_type = var.instance_type
ami = "ami-0c55b159cbfafe1f0"
vpc_security_group_ids = [aws_security_group.web_sg.id]
user_data = templatefile("${path.module}/user-data.sh", {
server_port = var.server_port
})
}5. What to expose from module.
Its a good practice to expose only what matters that is only what caller need. Its a bad practice to expose the whole module as below since it causes unnecessary rebuilds and treats entire module as a depedency.
depends_on = [module.web]Only expose what is needed.
output "instance_ids" {
value = aws_instance.web[*].id
}6. Deploying across multiple environments
In real world we have dev for testing and production for stable releases. The source is changed to a git version of the module with production most likely sticking to the stable release and dev fetching the latest version of the module. Remember we need control over deployments. dev configuration
source = "...?ref=v0.0.2"production config
source = "...?ref=v0.0.1"7. Versioning modules
Without module versioning changes affects everyone instantly dev and production thus infra becomes unpredictable. Inside the modules folder we create a git hub repo and push to it so that we can now fetch it on running terraform init either in dev or production environment, with versioning we have a safe reference point and a frozen version of the module.
git init
git add .
git commit -m "initial release"
git tag -a v0.0.1 -m "stable version"
git push origin main --tags
upgrading a version
git tag -a v0.0.2 -m "new changes"
git push origin --tagsfor semantic versioning used there 0.0.1 we have major : minor: patch where major is a version with incompatible API changes, minor is a version whose functionality is backwards compatible while patch is a bug fix that is backward compatible.
Lets meet on day 10.