Modular IaC Design with Reusable Modules and Variables

Infrastructure as code (IaC) is a key practice in modern cloud computing, enabling organizations to automate the provisioning and management of infrastructure resources. However, as infrastructure environments grow in size and complexity, managing IaC code can become increasingly challenging. One approach to addressing this challenge is to adopt a modular design, using reusable modules and variables.

Reusable modules are self-contained units of IaC code that can be used to create infrastructure resources. By creating reusable modules, organizations can reduce the amount of code required to provision infrastructure resources, improve code consistency, and simplify code maintenance.

Variables are used to parameterize IaC code, enabling organizations to customize infrastructure resources without modifying the underlying code. By using variables, organizations can create flexible and reusable IaC code that can be easily adapted to different environments and use cases.

In this blog post, we will explore how to design modular IaC code using reusable modules and variables.

Designing Reusable Modules

To design reusable modules, the following steps can be followed:

  1. Identify common infrastructure patterns: Identify common infrastructure patterns that can be abstracted into reusable modules. For example, a module for creating a virtual private cloud (VPC) with subnets and security groups can be used across multiple environments.

  2. Define module inputs: Define the inputs required by the module, such as the number of subnets, the IP address range, and the security group rules. Inputs can be defined using variables, which can be passed to the module when it is called.

  3. Define module outputs: Define the outputs generated by the module, such as the VPC ID, subnet IDs, and security group IDs. Outputs can be used by other modules or by the main IaC code to reference the infrastructure resources created by the module.

  4. Implement the module: Implement the module using the IaC tool of choice, such as Terraform or CloudFormation. The module should be designed to be self-contained and should not rely on external resources or variables.

  5. Test the module: Test the module in different environments to ensure it is working as expected. This can be done using unit tests, integration tests, or manual testing.

Here is an example of a Terraform module for creating a VPC with subnets and security groups:

variable "cidr_block" {
  type        = string
  description = "The CIDR block for the VPC"
}

variable "subnet_count" {
  type        = number
  description = "The number of subnets to create"
}

variable "security_group_rules" {
  type        = list(map(string))
  description = "The security group rules"
}

resource "aws_vpc" "vpc" {
  cidr_block = var.cidr_block
}

resource "aws_subnet" "subnets" {
  count             = var.subnet_count
  cidr_block        = cidrsubnet(var.cidr_block, 8, count.index)
  vpc_id            = aws_vpc.vpc.id
  availability_zone = "${data.aws_availability_zones.available.names[count.index]}"
}

resource "aws_security_group" "security_group" {
  name_prefix = "sg-"
  vpc_id      = aws_vpc.vpc.id

  dynamic "ingress" {
    for_each = var.security_group_rules
    content {
      from_port   = ingress.value["from_port"]
      to_port     = ingress.value["to_port"]
      protocol    = ingress.value["protocol"]
      cidr_blocks = ingress.value["cidr_blocks"]
    }
  }
}

output "vpc_id" {
  value = aws_vpc.vpc.id
}

output "subnet_ids" {
  value = aws_subnet.subnets\*\.id
}

output "security_group_id" {
  value = aws_security_group.security_group.id
}

In this example, the module defines inputs for the CIDR block, the number of subnets, and the security group rules. The module creates a VPC, subnets, and a security group using the AWS provider for Terraform. The module also defines outputs for the VPC ID, subnet IDs, and security group ID.

Using Variables to Parameterize IaC Code

To use variables to parameterize IaC code, the following steps can be followed:

  1. Define variables: Define variables for the inputs required by the IaC code. For example, a variable for the environment name can be used to customize the naming of resources.

  2. Assign values to variables: Assign values to the variables using a variables file or command-line arguments.

  3. Use variables in IaC code: Use the variables in the IaC code to customize the infrastructure resources.

Here is an example of using variables to parameterize the Terraform module for creating a VPC with subnets and security groups:

variable "environment" {
  type        = string
  description = "The environment name"
}

module "vpc" {
  source = "../modules/vpc"

  cidr_block = "10.0.0.0/16"
  subnet_count = 3
  security_group_rules = [
    {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]

  tags = {
    Environment = var.environment
  }
}

In this example, the module is called with the "source" argument, which specifies the location of the module code. The module inputs are defined using variables, and the "tags" argument is used to apply tags to the infrastructure resources.

Creating Reusable Modules with Conditional Logic

Reusable modules can also include conditional logic to handle different use cases. For example, a module for creating an Amazon RDS database instance can include logic to create a multi-AZ deployment or a read replica based on input variables.

Here is an example of a Terraform module for creating an Amazon RDS database instance with conditional logic:

variable "db_instance_class" {
  type        = string
  description = "The database instance class"
}

variable "db_name" {
  type        = string
  description = "The name of the database"
}

variable "db_engine" {
  type        = string
  description = "The database engine"
}

variable "db_engine_version" {
  type        = string
  description = "The database engine version"
}

variable "multi_az" {
  type        = bool
  description = "Whether to create a multi-AZ deployment"
}

variable "read_replica_count" {
  type        = number
  description = "The number of read replicas to create"
}

resource "aws_db_instance" "db_instance" {
  allocated_storage    = 20
  storage_type         = "gp2"
  engine               = var.db_engine
  engine_version       = var.db_engine_version
  instance_class       = var.db_instance_class
  name                 = var.db_name
  username             = "admin"
  password             = "password"
  parameter_group_name = "default.${var.db_engine}"
  vpc_security_group_ids = ["sg-12345678"]

  tags = {
    Environment = "dev"
  }

  skip_final_snapshot_before_delete = true
}

resource "aws_db_instance_read_replica" "read_replicas" {
  count = var.read_replica_count

  source_db_instance_identifier = aws_db_instance.db_instance.id
  db_instance_class             = var.db_instance_class
  availability_zone             = data.aws_availability_zones.available.names[count.index]

  tags = {
    Environment = "dev"
  }
}

resource "aws_db_instance" "multi_az_db_instance" {
  count = var.multi_az ? 1 : 0

  allocated_storage    = 20
  storage_type         = "gp2"
  engine               = var.db_engine
  engine_version       = var.db_engine_version
  instance_class       = var.db_instance_class
  name                 = var.db_name
  username             = "admin"
  password             = "password"
  parameter_group_name = "default.${var.db_engine}"
  vpc_security_group_ids = ["sg-12345678"]
  availability_zone     = data.aws_availability_zones.available.names[0]

  tags = {
    Environment = "dev"
  }

  backup_retention_period = 0
  publicly_accessible      = false

  multi_az = true
}

In this example, the module defines inputs for the database instance class, name, engine, engine version, multi-AZ deployment, and read replica count. The module creates an Amazon RDS database instance using the AWS provider for Terraform. If the "multi_az" input variable is set to true, the module creates a multi-AZ deployment using the "aws_db_instance" resource with the "multi_az" argument set to true. If the "read_replica_count" input variable is set to a value greater than zero, the module creates read replicas using the "aws_db_instance_read_replica" resource.

Using Modules in a Main IaC Code

Once reusable modules have been created, they can be used in a main IaC code to provision infrastructure resources. The main IaC code can call the modules and pass input variables to them.

Here is an example of a Terraform main IaC code that uses the VPC and RDS modules:

provider "aws" {
  region = "us-east-1"
}

module "vpc" {
  source = "../modules/vpc"

  cidr_block = "10.0.0.0/16"
  subnet_count = 3
  security_group_rules = [
    {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]

  tags = {
    Environment = "dev"
  }
}

module "rds" {
  source = "../modules/rds"

  db_instance_class = "db.t2.micro"
  db_name           = "mydb"
  db_engine         = "postgres"
  db_engine_version = "12.5"
  multi_az          = false
  read_replica_count = 0

  tags = {
    Environment = "dev"
  }

  vpc_security_group_ids = [module.vpc.security_group_id]
  subnet_ids             = module.vpc.subnet_ids
}

In this example, the main IaC code calls the VPC and RDS modules and passes input variables to them. The VPC module creates a VPC with subnets and a security group, and the RDS module creates an Amazon RDS database instance. The "vpc_security_group_ids" and "subnet_ids" arguments are used to pass the VPC and subnet IDs to the RDS module.

Conclusion

Modular IaC design with reusable modules and variables is a powerful approach to managing complex infrastructure environments. By creating reusable modules and parameterizing IaC code with variables, organizations can improve code consistency, reduce code duplication, and simplify code maintenance. By following the steps outlined in this blog post, organizations can design modular IaC code that is flexible, scalable, and easy to maintain. Additionally, using conditional logic in reusable modules can handle different use cases and further reduce the amount of code required to provision infrastructure resources.