DEV Community

Cover image for MVC design pattern in Terraform
Meir Gabay for ProdOps

Posted on • Updated on • Originally published at meirg.co.il

MVC design pattern in Terraform

In this blog post, I'm going to share how to use Input Variables a.k.a variables, and Local Values 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)
Description (Source) In Terraform
Model Managing the data of the application Local Values
View Presentation of the model in a particular format
Modules and Resources
Controller Receives the input, optionally validates it, and then passes the input to the model Input Variables

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

variable "app_name" {
  type = string
}

variable "region" {
  type = string
}

variable "environment" {
  type        = string
  description = "dev, stg, prd"
}
Enter fullscreen mode Exit fullscreen mode
  • 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 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

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

variable "cidr_ab" {
  type = map
  default = {
    dev = "10.1"
    stg = "10.2"
    prd = "10.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

main.tf

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
}
Enter fullscreen mode Exit fullscreen mode

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 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

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
}
Enter fullscreen mode Exit fullscreen mode

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

variables.tf

# 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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 logo unfor19 / terraform-multienv

A template for maintaining a multiple environments infrastructure with Terraform. This template includes a CI/CD process, that applies the infrastructure in an AWS account.

terraform-multienv

A template for maintaining a multiple environments infrastructure with Terraform. This template includes a CI/CD process, that applies the infrastructure in an AWS account.

environment drone.io GitHub Actions Circle Ci Travis CI
dev
stg
prd

Assumptions

  • Branches names are aligned with environments names, for example dev, stg and prd

  • The CI/CD tool supports the variable ${BRANCH_NAME}, for example ${DRONE_BRANCH}

  • The directory ./live contains infrastructure-as-code files - *.tf, *.tpl, *.json

  • Multiple Environments

    • All environments are maintained in the same git repository
    • Hosting environments in different AWS account is supported (and recommended)
  • Variables

    • ${app_name} = tfmultienv
    • ${environment} = dev or stg or prd

Getting Started

  1. We're going to create the following resources per environment

    • AWS VPC, Subnets, Routes and Routing Tables, Internet Gateway
    • S3 bucket (website) and an S3 object (index.html)
    • Terraform remote backend - S3 bucket and DynamoDB table
  2. Create a new GitHub repository by…





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.

Top comments (0)