Creating Your First Terraform Module: Launching Google Compute Instances
Tags: devops, webdev, tutorial, googlecloud
Published: 2023-08-11 15:14:47

In our previous blog post, we delved into the world of Terraform and explored how to use it to automate the provisioning of infrastructure resources on cloud platforms like AWS. This time, we're going to take it a step further and learn about Terraform modules, a powerful way to encapsulate and reuse your infrastructure code. Modules allow you to define, package, and distribute your infrastructure resources as reusable components, making your infrastructure code more organized, maintainable, and scalable.

## What is a Terraform Module?
A Terraform module is a collection of related resources and configurations that can be used as a single entity. It acts as a building block for your infrastructure, abstracting away complex details and enabling you to create consistent and reusable patterns. Modules promote code reuse and separation of concerns, making it easier to collaborate with team members and manage infrastructure at scale.

## Creating Your First Module
To demonstrate how to create a Terraform module, we'll use a practical example: provisioning a Google Cloud Compute instance. In this example, we'll create a module that defines a compute instance, its associated resources, and some customizable variables.

Let's break down the example module structure:

```
├── modules/
│   └── compute-instance/
│       │── main.tf
│       ├── variables.tf
│       └── output.tf
└── main.tf
```

#### Module Files
- __main.tf__: This file contains the actual resource definitions and configurations. It specifies the compute instance, its disk, network interface, and firewall rules.
- __variables.tf__: Here, we define input variables that allow customization of the module's behavior. Users of the module can provide values for these variables when they use the module.
- __output.tf__: Output variables allow us to expose specific values from the module to the calling code. In our case, we're exposing the instance's public IP address if a public IP is required.

## Example Module Code
Here's the module code for creating a Google Cloud Compute instance:
> In the example module, we've leveraged several advanced Terraform expressions to create a flexible and adaptable compute instance provisioning solution. To deepen your understanding and explore more expressions and features, I highly recommend exploring the [official Terraform documentation](https://developer.hashicorp.com/terraform/language)

```tf
# main.tf

terraform {
  required_providers {
    google = {
      source = "hashicorp/google"
    }
  }
}

/* 
Here, we define a Google Cloud Compute address resource.
This address will be associated with the instance if a public IP 
is required.
The count parameter allows us to conditionally create this resource 
based on the value of the require_public_ip variable. 
*/
resource "google_compute_address" "instance-ip" {
  count = var.require_public_ip ? 1 : 0
  name = "${var.name}-ip"
}


/*
This section defines the Google Cloud Compute instance resource.
It specifies various attributes such as the instance name,
machine type, availability zone, tags, and more.
The boot_disk block defines the instance's boot disk,
including the image to use, size, and disk type.
*/
resource "google_compute_instance" "compute-instance" {
  name =  "${var.name}-instance"
  machine_type = var.machine_type
  zone = var.zone
  tags = google_compute_firewall.instance-firewall.target_tags
  allow_stopping_for_update = true

  boot_disk {
    initialize_params {
      image = var.image
      size = var.size
      type = var.disk_type
    }
  }

  network_interface {
    network = var.network_name
    subnetwork = var.subnetwork_name
    dynamic "access_config" {
      for_each = var.require_public_ip ? [1] : []
      content {
        nat_ip = google_compute_address.instance-ip[0].address
      }
    }
  }

  metadata = var.ssh_keys_path != null ? {ssh-keys = "${var.name}:${file(var.ssh_keys_path)}"} : {}
}

/*
This part of the code creates a Google Cloud Compute firewall.
It allows incoming traffic on specified TCP ports, 
and the source_ranges parameter allows traffic from any
source (0.0.0.0/0). 
The firewall is associated with the instance through tags.
*/
resource "google_compute_firewall" "instance-firewall" {
  name = "${var.name}-firewall"
  network = var.network_name
  source_ranges = ["0.0.0.0/0"]

  allow {
    protocol = "tcp"
    ports = tolist(var.firewall_rule)
  }

  target_tags = ["${var.name}-firewall"]
}
```

```tf
# variables.tf

variable "name" {
  type = string
  description = "instance name"
}

variable "machine_type" {
  type = string
  description = "instance machine type"
  default = "n2-standard-2"
}

variable "zone" {
  type = string
  description = "the zone that the machine should be created in"
}

variable "image" {
  type = string
  description = "the image from which to initialize this disk"
  default = "ubuntu-1804-bionic-v20230510"
}

variable "size" {
  type = number
  description = "the size of the image in gigabytes"
  default = 50
}

variable "disk_type" {
  type = string
  description = "the GCE disk type. Such as pd-standard, pd-balanced or pd-ssd."
  default = "pd-standard"
}

variable "network_name" {
  type = string
  description = " The name of the network."
  default = "default"
}

variable "subnetwork_name" {
  type = string
  description = " The name of the sub-network."
  default = "default"
}

variable "require_public_ip" {
  type = bool
  description = "set to false if you don't want public IP"
  default = true
}

variable "ssh_keys_path" {
  type = string
  description = "path to your credential"
}

variable "firewall_rule" {
  type = set(string)
  description = "allow TCP ports"
  default = ["80", "443"]
}

```

```tf
# output.tf

output "instance_ip" {
  value = local.require_public_ip ? google_compute_address.instance-ip[0].address : null
}

locals {
  require_public_ip = var.require_public_ip
}
```

## How to Use the Module
Using the module you've created is simple and follows the same process as using any other Terraform resource. Let's walk through the steps:

- __Module Initialization__: Just like initializing a Terraform project, you need to initialize the module. Create a new directory for your module, copy the module files into it, and run `terraform init`.

- __Module Configuration__: In your main Terraform configuration (`main.tf` outside the module), you can use your module by calling it like any other resource:

```tf
module "my-instance" {
  source = "./modules/compute-instance"
  name = "my-instance"
  machine_type = "n2-standard-2"
  zone = "asia-southeast1-a"
  require_public_ip = true
  firewall_rule = ["80", "443", "3000", "4000"]
  ssh_keys_path = "./path/to/your/credential/key.pub"
  # ...
  # Provide other variable values as needed
}
```
1. __Apply__: Run `terraform apply` to create the compute instance using your module.

2. __Destroy__: When you're done, run `terraform destroy` to clean up the resources.

## Conclusion
Congratulations! You've just created your first Terraform module. By modularizing your infrastructure code, you're paving the way for better organization, reusability, and scalability. As you gain more experience with Terraform, you can create more sophisticated modules and share them with your team or the broader community.

Remember, modules are a key tool for making your infrastructure codebase more efficient and manageable. Experiment, explore, and keep building to take full advantage of Terraform's capabilities.