Terraform with GCP

GCP Terraform

I wanted to share some of my experiences learning Terraform and managing infrastructure as code across different cloud providers, including Google Cloud Platform (GCP).

If you're new to Terraform, like I am, it's a tool that allows you to define and manage your infrastructure as code. This means you can automate the process of configuring and deploying resources in the cloud, making it faster and easier to manage complex environments over time.

In my journey learning Terraform, I've discovered some tips and tricks that have been particularly useful when working with GCP. I'll be sharing some of those tips, along with code snippets and insights into how Terraform can help you manage infrastructure across cloud providers.

Whether you're a seasoned developer or just starting out, I hope you'll find something useful in this post. Let's explore Terraform and GCP together!

Infrastructure As Code

Terraform HCL

At the heart of Terraform lies HCL - HashiCorp Configuration Language. HCL is a domain-specific language used for describing infrastructure as code. It is designed to be human-readable and easy to write, allowing you to define complex infrastructure resources in a concise and clear way.

HCL Syntax

The syntax of HCL is based on sections, blocks, attributes, and values. Each Terraform configuration file is divided into one or more sections, each containing one or more blocks. Each block defines a single resource or data object, and is made up of one or more attributes that define the resource's properties.

Here's an example of an HCL block that defines a local_file resource:

resource "local_file" "example" {
  content = "Hello, World!"
  filename = "/tmp/example.txt"
}

In this example, the local_file block defines a resource of type local_file, with an identifier of example. The block has two attributes: content and filename, each with a specific value.

Basic Shell Commands for Terraform

When working with Terraform, you'll need to use the command line interface to execute various operations such as initializing, planning, applying, and destroying infrastructure. Here are some basic shell commands that are commonly used in Terraform:

  • terraform init: This command initializes a new or existing Terraform working directory. It downloads and installs any required plugins, creates the .terraform directory, and generates the terraform.tfstate file that tracks the state of your infrastructure.

  • terraform plan: This command generates an execution plan that shows what changes Terraform will make to your infrastructure. It does not make any actual changes, but instead provides a preview of what will happen when you apply your configuration.

  • terraform apply: This command applies the changes described in your Terraform configuration files. It creates, modifies, or deletes resources as necessary to achieve the desired state. Use this command with caution, as it can make changes to your infrastructure that cannot be easily undone.

  • terraform destroy: This command destroys all resources that were created by Terraform. Use this command with caution, as it permanently deletes infrastructure and cannot be undone.

These are some of the most basic Terraform commands that you'll use frequently. As you become more experienced with Terraform, you may need to use additional commands or customize the behavior of these commands using flags and options. You can find more information about Terraform commands and options in the official documentation.

Variables

Variables in Terraform are used to parameterize your configuration and make it more flexible and reusable. You can define variables in your configuration files or in separate files, and then reference them in your resources using interpolation syntax.

Defining Variables

You can define variables in your Terraform configuration by using the variable block. Here's an example of how to define a variable for a GCP instance:

variable "zone" {
  type    = string
  default = "us-central1-a"
}

In this example, we define a variable called zone that has a type of string and a default value of "us-central1-a". You can also define variables with other types, such as number, bool, list, or map.

Using Variables

Once you have defined a variable, you can use it in your resources by referencing it with interpolation syntax. Here's an example:

resource "google_compute_instance" "example" {
  name         = "example-instance"
  machine_type = "e2-medium"
  zone         = var.zone
}

In this example, we use the var.zone syntax to reference the zone variable that we defined earlier. This allows us to specify the zone when we apply our configuration, without hardcoding it in our resources.

Assigning Values to Variables

When you apply your Terraform configuration, you can assign values to your variables using various methods such as command line flags, environment variables, or .tfvars files. Here's an example of how to assign a value to a variable using a .tfvars file:

zone = "us-west1-b"

In this example, we create a file called terraform.tfvars that sets the zone variable to "us-west1-b". When we apply our configuration, Terraform will automatically load this variable from the file and use it in our resources.

Variable Types

In Terraform, variables are used to parameterize your infrastructure code and allow you to reuse and customize your modules. There are several types of variables that you can use in Terraform:

  • string: a string of characters, such as "hello world"
  • number: a numerical value, such as 42
  • bool: a boolean value that can be either true or false
  • list: a sequence of values of the same type, such as [1, 2, 3]
  • tuple: a fixed-size sequence of values of different types, such as ["hello", 42, true]
  • set: an unordered collection of unique values of the same type, such as ["a", "b", "c"]
  • object: a collection of key-value pairs with string keys and values of any type, such as { name = "Alice", age = 30 }
  • map: a collection of key-value pairs with any type of keys and values, such as { "name" = "Alice", "age" = 30 }

Here's an example of how you can define and use variables of different types in Terraform:

variable "my_string" {
  type = string
  default = "hello world"
}

variable "my_number" {
  type = number
  default = 42
}

variable "my_bool" {
  type = bool
  default = true
}

variable "my_list" {
  type = list(number)
  default = [1, 2, 3]
}

variable "my_tuple" {
  type = tuple([string, number, bool])
  default = ["hello", 42, true]
}

variable "my_set" {
  type = set(string)
  default = ["a", "b", "c"]
}

variable "my_object" {
  type = object({
    name = string,
    age = number,
  })
  default = {
    name = "Alice",
    age = 30,
  }
}

variable "my_map" {
  type = map(any)
  default = {
    "name" = "Alice",
    "age" = 30,
    "is_admin" = true,
  }
}

resource "google_compute_instance" "my_instance" {
  name         = "my-instance"
  machine_type = "n1-standard-1"
  zone         = "us-central1-a"

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
    }
  }

  network_interface {
    network = "default"

    access_config {
      // Use the variable values here
      nat_ip       = var.my_bool ? null : google_compute_address.my_address.address
      network_tier = var.my_string
    }
  }
}

output "my_output" {
  // Use the variable values here
  value = {
    string = var.my_string
    number = var.my_number
    bool = var.my_bool
    list = var.my_list
    tuple = var.my_tuple
    set = var.my_set
    object = var.my_object
    map = var.my_map
  }
}

In this example, we defined variables of different types and assigned default values to them. We then used these variables in a Google Cloud Platform (GCP) compute instance resource and an output block. Note how we used the var. prefix to reference the variable values in the resource and output blocks.

Variable Best Practices

When working with variables in Terraform, it's important to follow these best practices:

  • Define variables in a separate file or module, rather than inline in your resources.
  • Use descriptive and meaningful names for your variables.
  • Provide default values for your variables whenever possible, to make your configuration more flexible and easier to use.
  • Use variable interpolation to reference your variables in your resources, rather than hardcoding values.

By following these best practices, you can make your Terraform configuration more flexible, maintainable, and reusable.

Providers in Terraform

Providers are plugins that Terraform uses to interact with various infrastructure and service providers, such as cloud providers (e.g. Google Cloud Platform, Amazon Web Services), DNS providers (e.g. Cloudflare, AWS Route53), and more.

When you define a provider in your Terraform configuration, you are essentially telling Terraform which plugin to use and how to connect to the corresponding infrastructure or service. For example, you might use the Google Cloud provider to manage resources in the Google Cloud Platform.

Here's an example of how you might declare a provider in your Terraform configuration for the Google Cloud Platform:

provider "google" {
  project = "my-project"
  region  = "us-central1"
  zone    = "us-central1-a"
}

In this example, we are using the Google Cloud provider and specifying the project, region, and zone that we want to interact with.

Specifying Provider Versions in Terraform

When you declare a provider in your Terraform configuration, it's important to specify a version number. This ensures that your configuration is compatible with the provider and that the provider's behavior won't change unexpectedly.

For example, let's say you are using the Google Cloud provider in your Terraform configuration. You would declare the provider like this:

provider "google" {
  project = "my-project"
  region  = "us-central1"
  zone    = "us-central1-a"
  version = "~> 3.0"
}

In this example, we are specifying version 3.0 of the Google Cloud provider using the version argument. The ~> symbol is a constraint operator that means "use any version in the 3.x range, but not version 4.0 or higher". This allows us to receive updates to the provider within the 3.x range, but ensures that we don't accidentally upgrade to version 4.0, which might have breaking changes that could break our configuration.

It's important to keep in mind that provider versions can change over time, so it's a good practice to regularly check for updates to the provider you're using and update your configuration accordingly.

In summary, specifying provider versions in Terraform is a key practice for ensuring that your configuration is compatible with the provider and that you won't experience unexpected behavior due to provider updates.

Terraform Lifecycle

Terraform allows you to specify the behavior of the resources you manage with lifecycle hooks. These hooks give you the ability to control when resources are created, updated, or deleted. There are four lifecycle blocks: create_before_destroy, prevent_destroy, ignore_changes, and lifecycle.

create_before_destroy

By default, Terraform will first destroy the existing resource and then create a new one. However, you may need to create a new resource before destroying the existing one. You can use the create_before_destroy lifecycle block to accomplish this. Here's an example using GCP storage buckets:

resource "google_storage_bucket" "example_bucket" {
  name          = "example-bucket"
  location      = "US"
  force_destroy = true

  lifecycle {
    create_before_destroy = true
  }
}

In this example, Terraform will create a new GCP storage bucket before destroying the existing one.

prevent_destroy

Sometimes you want to prevent a resource from being destroyed. For example, you may have a database instance that should never be deleted. You can use the prevent_destroy lifecycle block to accomplish this. Here's an example:

resource "google_sql_database_instance" "example_instance" {
  name             = "example-instance"
  database_version = "MYSQL_5_7"
  region           = "us-central1"

  lifecycle {
    prevent_destroy = true
  }
}

In this example, Terraform will prevent the deletion of the GCP SQL database instance.

ignore_changes

The ignore_changes lifecycle block is used to specify which resource attributes should be ignored when creating or updating a resource. Here's an example:

resource "google_sql_database_instance" "example_instance" {
  name             = "example-instance"
  database_version = "MYSQL_5_7"
  region           = "us-central1"

  lifecycle {
    ignore_changes = [
      settings,
      service_account_email_addresses
    ]
  }
}

In this example, Terraform will ignore changes to the settings and service_account_email_addresses attributes of the GCP SQL database instance.

lifecycle

The lifecycle block allows you to specify custom settings for the resource lifecycle. Here's an example:

resource "google_compute_instance" "example_instance" {
  name         = "example-instance"
  machine_type = "f1-micro"
  zone         = "us-central1-a"

  lifecycle {
    create_before_destroy = true
    prevent_destroy       = true
    ignore_changes        = [
      boot_disk
    ]
  }
}

In this example, Terraform will create a new GCP Compute Engine instance before destroying the existing one, prevent the deletion of the instance, and ignore changes to the boot_disk attribute.

Terraform data source

In Terraform, data is a way to retrieve information from external sources, such as an API or a remote service, and make it available to your configuration. Data sources are defined using the data block in your Terraform code, and the result of a data source is exposed as an attribute that can be used elsewhere in your code.

For example, suppose you want to retrieve information about a Google Cloud Storage bucket that already exists in your project, and use that information to configure a different resource, such as a Compute Engine instance. You can use the google_storage_bucket data source to retrieve the information about the bucket and expose its attributes as variables that can be used elsewhere in your configuration:

data "google_storage_bucket" "my_bucket" {
  name = "my-bucket"
}

resource "google_compute_instance" "my_instance" {
  name         = "my-instance"
  machine_type = "e2-medium"
  zone         = "us-central1-a"

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-10"
    }
  }

  network_interface {
    network = "default"
  }

  metadata = {
    foo = data.google_storage_bucket.my_bucket.url
  }
}

In this example, the google_storage_bucket data source retrieves information about a bucket named my-bucket, and exposes its url attribute as a variable named data.google_storage_bucket.my_bucket.url. That variable is then used in the metadata block of the google_compute_instance resource to set the value of the foo key to the url of the bucket.

Using data sources can help you avoid hardcoding values in your configuration and make it more dynamic and reusable.

What is GCP, and why should we use Terraform to manage it?

Google Cloud Platform (GCP) is a suite of cloud computing services provided by Google. It offers a wide range of services, including computing, storage, networking, machine learning, and more. As a cloud platform, it allows businesses to build and deploy applications and services quickly and easily, with the scalability and flexibility that cloud computing provides.

GCP Provider

However, managing resources on GCP can be challenging, especially as your infrastructure grows. This is where Terraform comes in. Terraform allows you to manage your GCP resources using code, making it easy to version control and automate your infrastructure.

With Terraform, you can define your GCP resources using HCL syntax, as we've seen in previous sections. For example, you can create a Google Cloud Storage bucket using the following Terraform code:

resource "google_storage_bucket" "my_bucket" {
  name          = "my-bucket"
  location      = "US"
  force_destroy = true

  lifecycle {
    prevent_destroy = true
  }
}

This code creates a Google Cloud Storage bucket named "my-bucket" in the "US" region and specifies force_destroy and prevent_destroy lifecycle rules. With this code, you can easily create and manage your GCP resources in a consistent and repeatable manner.

In summary, GCP is a powerful cloud platform that offers a wide range of services for businesses. However, managing these resources can be difficult without the right tools. Terraform provides a way to manage your GCP resources using code, making it easy to version control and automate your infrastructure.

Conclusion

In this article, we've explored the fundamentals of Terraform, a powerful tool for infrastructure management that allows us to define and provision infrastructure as code. We've seen how to create resources in GCP using Terraform, including instances, networks, and buckets, and how to manage their lifecycle using the appropriate attributes and settings.

We've also discussed how to use variables and data sources to dynamically configure our infrastructure, how to version our provider dependencies to ensure compatibility, and how to use lifecycle hooks to control when resources are created, updated, or deleted.

Through this exploration, we've gained a deeper understanding of the principles of infrastructure as code, the benefits of using Terraform to manage infrastructure, and the potential of GCP as a cloud provider. We've seen how Terraform enables us to automate and standardize our infrastructure, increasing our efficiency and reducing the likelihood of errors and misconfigurations.

Of course, this is just the tip of the iceberg - there's so much more to learn and explore in the world of Terraform and cloud infrastructure management. But armed with the knowledge we've gained here, we're well on our way to becoming more proficient and effective in our roles as infrastructure engineers and architects.

So go forth and continue learning, experimenting, and building! With Terraform and GCP, the possibilities are endless.