Over the last few months I have been writing a lot of Terraform code. One of the main issues that I’ve been dealing with is how to break apart modules. For example, I wrote a module for deploying an Application Load Balancer. In addition to creating an ALB, I also create listeners, listener rules, DNS entries, security groups, and certificates. It seemed easy enough when I started out, but as I progress, and requirements changed, I started running into issues. What happens when I need to add a non-standard port listener (something like 8443 instead of just 80 or 443)? What about needing a listener rule that used a different, non-AWS generated certificate?

As these changes were introduced, the code became more convoluted, with a lot of conditionals and local variables. I did not even have a good way to create new listeners without just adding more code for each specific listener type, which just feels dirty. I could break each of them up into seperate, individual modules, but they still feel like they went together. I didn’t want to manage four or five individual modules and the repositories that go with them, especially since they all depend on each other. That’s why I was excited to find out about sub-modules.

While I was doing some research aroound various testing strategies in the Terraform Registry I started noticing something called sub-modules while browsing various repositories. Digging in more deeply, I found that you could now add a single module to the registry that has multiple sub-modules and then call them directly from Terraform. As I started to dig deeper, I found that some allowed the parent module to do some piece of work while others only had individual sub-modules.

With that as a starting place, I decided to rewrite my ALB module to follow this new pattern. I broke my module up into five sub-modules:

├── modules
│   ├── alb-certificate
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   ├── versions.tf
│   │   └── README.md
│   ├── alb-dns
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   ├── versions.tf
│   │   └── README.md
│   ├── alb-listener-rule
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   ├── versions.tf
│   │   └── README.md
│   ├── alb-listener
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   ├── versions.tf
│   │   └── README.md
│   └── alb
│       ├── main.tf
│       ├── outputs.tf
│       ├── variables.tf
│       ├── versions.tf
│       └── README.md
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE 
└── README.md

This allowed me to break the code up into smaller chuncks while keeping it all together. I can call the individual submodules as need like this:

module "https-listener" {
  source            = "AustinCloudGuru/alb/aws//modules/alb-listener"
  version           = "1.0.2"
  load_balancer_arn = module.alb.alb_arn
  security_group_id = module.alb.security_group_id
  cidr_blocks       = ["0.0.0.0/0"]
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = module.default-certificate.arn
}

I’m really excited about the opportunities that this pattern gives me. I don’t like having a lot of different but interdependent modules and this solves that problem for me. I’m really looking forward to revisiting some of my other modules.