MVC design pattern in Terraform
Tags: terraform, tips, tricks, bestpractice
Published: 2020-09-14 12:33:18

In this blog post, I'm going to share how to use [Input Variables](https://www.terraform.io/docs/configuration/variables.html) a.k.a variables, and [Local Values](https://www.terraform.io/docs/configuration/locals.html) a.k.a locals, to form a well-maintained infrastructure in Terraform while following the MVC design pattern.

## The Approach

- Local Values (Model) are altered behind the scenes by architects, regardless of the application's Modules and Resources (View)
- Modules and Resources (View) are implemented according to the Local Values (Model)
- Input Variables (Controller) get user input and manipulate the Local Values (Model). These changes are reflected in Modules and Resources (View)

<table><thead><tr><th></th><th>Description <a href="https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller">(Source)</th><th>In Terraform</th></tr></thead><tbody><tr><td>Model</td><td>Managing the data of the application</td><td>Local Values</td></tr><tr><td>View</td><td>Presentation of the model in a particular format<br></td><td>Modules and Resources</td></tr><tr><td>Controller</td><td>Receives the input, optionally validates it, and then passes the input to the model</td><td>Input Variables</td></tr></tbody></table>

## Input Variables (Controller)

Mainly used for getting a dynamic input from architects and CI/CD processes. Here's a classic snippet of how Input Variables are used

`variables.tf`
```terraform
variable "app_name" {
  type = string
}

variable "region" {
  type = string
}

variable "environment" {
  type        = string
  description = "dev, stg, prd"
}
```

- Avoid setting `default` values to "information variables". This enables other architects (or future you) to use the same Infrastructure-as-Code (IaC), without worrying about predefined names. Examples:
  - `app_name`
  - `region`
  - `domain_name`
- Set `default` values to "infrastructure and application variables". Examples:
  - `desired_tasks = 1`
  - `firewall_enabled = true`
  - `docker_image = "nginx:1.19.2"`

### Variables As Maps

The combination of Input Variables, the default attribute, and the [lookup](https://www.terraform.io/docs/configuration/functions/lookup.html) function is amazing. For example, according to a given value, `environment`, get the relevant `default` value from a map. A more practical example:

`variables.tf`
```terraform
variable "environment" {
  type        = string
  description = "dev, stg, prd"
}

variable "cidr_ab" {
  type = map
  default = {
    dev = "10.1"
    stg = "10.2"
    prd = "10.3"
  }
}
```

`main.tf`
```terraform
module "vpc" {
  source             = "terraform-aws-modules/vpc/aws"

  # Later on we'll set cidr to a Local Value
  cidr               = "${lookup(var.cidr_ab, var.environment)}.0.0/16"
  
  # ... removed other attributes for brevity
}
```

## Local Values (Model)

Any value that has even the slightest chance of changing in the future should be set to a Local Value. This means that most of the IaC will include Local Values. [Terraform's official docs](https://www.terraform.io/docs/configuration/locals.html#when-to-use-local-values) recommendations:

> ... _if overused, Local Values can also make a configuration hard to read by future maintainers by hiding the actual values used ... Use local values only in moderation, in situations where a single value or result is used in many places, and that value is likely to be changed in the future. The ability to easily change the value in a central place is the key advantage of local values._

Local Values are "overused" to make the code modular and easier to maintain. To share the true powers of this approach, here's another practical example:

`main.tf`
```terraform
module "vpc" {
  source             = "terraform-aws-modules/vpc/aws"
  version            = "~>2.0"
  name               = local.prefix
  cidr               = local.vpc_cidr
  private_subnets    = local.private_subnets
  public_subnets     = local.public_subnets
  azs = local.availability_zones

  tags = local.tags
}
```

**Do you realize what this means?** it's possible to manage all the values in a single place

`variables.tf`
```terraform
# Input Variables (Controller)
variable "app_name" {
  type = string
}

variable "region" {
  type = string
}

variable "environment" {
  type        = string
  description = "dev, stg, prd"
}

variable "cidr_ab" {
  type = map
  default = {
    dev = "10.1"
    stg = "10.2"
    prd = "10.3"
  }
}

# Local Values (Model)
locals {
  prefix   = "${var.app_name}-${var.environment}"
  vpc_cidr = "${lookup(var.cidr_ab, var.environment)}.0.0/16"
  public_subnets = [
    "${lookup(var.cidr_ab, var.environment)}.1.0/24",
    "${lookup(var.cidr_ab, var.environment)}.2.0/24",
  ]
  private_subnets = [
    "${lookup(var.cidr_ab, var.environment)}.10.0/24",
    "${lookup(var.cidr_ab, var.environment)}.20.0/24",
  ]
  availability_zones = ["${var.region}a", "${var.region}b"]

  tags = {
    "Environment" : var.environment,
    "Terraform" : "true"
  }
}
```

## Practical implementation

I've created a GitHub repository that implements the described MVC design pattern in Terraform. This repository also includes a CI/CD process for promoting environments. Here it is:

{% github unfor19/terraform-multienv %}

## Final Words

I hope you find this useful, and if you do, don't forget to clap/heart and share it with your friends and colleagues.
Got any questions or doubts? Let's start a discussion! Feel free to comment below.