Intro

In this post I will show you how to setup Terraform to connect to your Google Cloud Platform (GCP) tenancy to manage your GCP infrastructure as code.

Software

The following software was used in this post.

  • Terraform - 1.1.7
  • Ubuntu - 2004
  • gcloud - 378.0.0

Pre-Flight Check

Google Account

You will need a Google account to work with GCP. If you don't already have one. Go and create one now.

gcloud

gcloud is a CLI tool that is used to manage GCP resources. If you don't already have it installed, see the docs for the latest instructions.

Confirm gcloud is installed and accessible.

cmd
gcloud -v

# Output
Google Cloud SDK 378.0.0
alpha 2022.03.18
beta 2022.03.18
bq 2.0.74
bundled-python3-unix 3.8.11
core 2022.03.18
gsutil 5.8

Terraform

This post assumes that you already have Terraform installed. If you don't already have it installed, see the docs for the latest instructions.

Confirm Terraform is installed and accessible.

cmd
terraform -v    

# Output
Terraform v1.1.7
on linux_amd64

Setup GCP

Create Project

First things first, we will need to create a GCP project. In the web console, naviagte to:

Enter the project name test-project and press create .

Create a Service Account

I don't have a web browser on my dev machine where I am running Terraform so I will be using a service account to authenticate to GCP. To create a service account navigate to:

Enter the name terraform and press CREATE AND CONTINUE .

In the Role drop down select Editor and click CONTINUE .

Finally, click DONE .

Create Authentication Keys

Under the Actions field, press the three dots ( ) and press Manage Keys .

Press ADD KEY and then Create new key .

Select JSON as the format and then press CREATE

Store Keys

Download the keys to a secure location for use on your machine. I am using ~/.gcp and naming the file terraform.json . Once there, be sure the adjust the permissions so they are RW only by you.

cmd
chmod 0600 ~/.gcp/terraform.json

Activate Service Account

Finally, activate the service account by running the following command.

cmd
gcloud auth activate-service-account <service-account-email> --key-file=/path/to/.gcp/terraform.json

# Output
Activated service account credentials for: [<service-account-email>]
Note
The service account email can be found in the service accounts list on the web console or in the keyfile under the client_email field.

Enable API Services

To allow access to a GCP service via the API we need to enable the service for API access.

For this post we will need to enable the compute.googleapis.com service.

cmd
gcloud services enable compute.googleapis.com --project test-project-<id>

# Output
Operation "operations/acf.p2-<some-id>" finished successfully.
Note

Be aware this can take a minute or two to complete.

Note

The project id can be found in the keyfile under the project_id field.

OK phew, that's it for the gcloud setup. Let's move onto the Terraform section.

Terraform

Link Credentials

To allow Terraform to use the servie account credentials we need to set an environment variable in our shell config. I am using ZSH so my environment variables live in the ~/.zshrc file. Adjust accordingly for your environment.

~/.zshrc
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/.gcp/terraform.json"

Again, make sure this file is RW only by yourself.

cmd
chmod 0600 ~/.zshrc

Now, source the file to load the variable into your shell environment.

cmd
source ~/.zshrc

Terraform Project

Let's create a terraform project. I am creating mine in the ~/code/terraform/gcp-test/ directory.

cmd
mkdir -p ~/code/terraform/gcp-test/ && cd ~/code/terraform/gcp-test/

Now, create a file called main.tf with the following contents.

main.tf
provider "google" {
  project = "test-project-<id>"
  region  = "australia-southeast1"
  zone    = "australia-southeast1-a"
}

resource "google_compute_instance" "vm_instance" {
  name         = "terraform-instance"
  machine_type = "f1-micro"

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

  network_interface {
    # A default network is created for all GCP projects
    network = google_compute_network.vpc_network.self_link
    access_config {
    }
  }
}

resource "google_compute_network" "vpc_network" {
  name                    = "terraform-network"
  auto_create_subnetworks = "true"
}

This code specifies the GCP project , region details and a compute instance (VM) . I am using the australia-southeast1 region. Adjust according to your needs.

Region information can be found here

Next, initialize the Terraform project with the terraform init command. This will setup the project and install any required plugins.

cmd
terraform init

# Output
Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/google...
- Installing hashicorp/google v4.15.0...


- Installed hashicorp/google v4.15.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Plan

Almost there, lets test our deployment with the terraform plan command. This will perform a dry-run and verify all the resources that need to be created.

cmd
terraform plan

# Output
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # google_compute_instance.vm_instance will be created
  + resource "google_compute_instance" "vm_instance" {
      + can_ip_forward       = false
      + cpu_platform         = (known after apply)
      + current_status       = (known after apply)
      + deletion_protection  = false
      + guest_accelerator    = (known after apply)
      + id                   = (known after apply)
      + instance_id          = (known after apply)
      + label_fingerprint    = (known after apply)
      + machine_type         = "f1-micro"
      + metadata_fingerprint = (known after apply)
      + min_cpu_platform     = (known after apply)
      + name                 = "terraform-instance"
      + project              = (known after apply)
      + self_link            = (known after apply)
      + tags_fingerprint     = (known after apply)
      + zone                 = (known after apply)

      + boot_disk {
          + auto_delete                = true
          + device_name                = (known after apply)
          + disk_encryption_key_sha256 = (known after apply)
          + kms_key_self_link          = (known after apply)
          + mode                       = "READ_WRITE"
          + source                     = (known after apply)

          + initialize_params {
              + image  = "debian-cloud/debian-9"
              + labels = (known after apply)
              + size   = (known after apply)
              + type   = (known after apply)
            }
        }

      + confidential_instance_config {
          + enable_confidential_compute = (known after apply)
        }

      + network_interface {
          + ipv6_access_type   = (known after apply)
          + name               = (known after apply)
          + network            = "default"
          + network_ip         = (known after apply)
          + stack_type         = (known after apply)
          + subnetwork         = (known after apply)
          + subnetwork_project = (known after apply)

          + access_config {
              + nat_ip       = (known after apply)
              + network_tier = (known after apply)
            }
        }

      + reservation_affinity {
          + type = (known after apply)

          + specific_reservation {
              + key    = (known after apply)
              + values = (known after apply)
            }
        }

      + scheduling {
          + automatic_restart   = (known after apply)
          + min_node_cpus       = (known after apply)
          + on_host_maintenance = (known after apply)
          + preemptible         = (known after apply)

          + node_affinities {
              + key      = (known after apply)
              + operator = (known after apply)
              + values   = (known after apply)
            }
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didnt use the -out option to save this plan, so Terraform cant guarantee to take exactly these actions if
you run "terraform apply" now.

Deploy

It's looking good, now lets deploy our Terraform plan with the terraform apply -auto-approve command.

cmd
terraform apply -auto-approve

# Output
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # google_compute_instance.vm_instance will be created
  + resource "google_compute_instance" "vm_instance" {
      + can_ip_forward       = false
      + cpu_platform         = (known after apply)
      + current_status       = (known after apply)
      + deletion_protection  = false
      + guest_accelerator    = (known after apply)
      + id                   = (known after apply)
      + instance_id          = (known after apply)
      + label_fingerprint    = (known after apply)
      + machine_type         = "f1-micro"
      + metadata_fingerprint = (known after apply)
      + min_cpu_platform     = (known after apply)
      + name                 = "terraform-instance"
      + project              = (known after apply)
      + self_link            = (known after apply)
      + tags_fingerprint     = (known after apply)
      + zone                 = (known after apply)

      + boot_disk {
          + auto_delete                = true
          + device_name                = (known after apply)
          + disk_encryption_key_sha256 = (known after apply)
          + kms_key_self_link          = (known after apply)
          + mode                       = "READ_WRITE"
          + source                     = (known after apply)

          + initialize_params {
              + image  = "debian-cloud/debian-9"
              + labels = (known after apply)
              + size   = (known after apply)
              + type   = (known after apply)
            }
        }

      + confidential_instance_config {
          + enable_confidential_compute = (known after apply)
        }

      + network_interface {
          + ipv6_access_type   = (known after apply)
          + name               = (known after apply)
          + network            = "default"
          + network_ip         = (known after apply)
          + stack_type         = (known after apply)
          + subnetwork         = (known after apply)
          + subnetwork_project = (known after apply)

          + access_config {
              + nat_ip       = (known after apply)
              + network_tier = (known after apply)
            }
        }

      + reservation_affinity {
          + type = (known after apply)

          + specific_reservation {
              + key    = (known after apply)
              + values = (known after apply)
            }
        }

      + scheduling {
          + automatic_restart   = (known after apply)
          + min_node_cpus       = (known after apply)
          + on_host_maintenance = (known after apply)
          + preemptible         = (known after apply)

          + node_affinities {
              + key      = (known after apply)
              + operator = (known after apply)
              + values   = (known after apply)
            }
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.
google_compute_instance.vm_instance: Creating...
google_compute_instance.vm_instance: Still creating... [10s elapsed]
google_compute_instance.vm_instance: Creation complete after 18s [id=projects/test-project-345109/zones/australia-southeast1-a/instances/terraform-instance]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

It's looking like it's deployed, let's verify.

List the deployed instances with the gcloud compute instances list command.

cmd
gcloud compute instances list --project test-project-<id>

# Output
NAME                ZONE                    MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP    STATUS
terraform-instance  australia-southeast1-a  f1-micro                   10.152.0.2   X.X.X.X  RUNNING

!!! WE GOT ONE !!!

Destroy

Let's clean up so we don't have to pay any unnessecary bills.

Kill the infrastructure with the terraform destroy command and type yes at the prompt.

cmd
terraform destroy

# Output
google_compute_instance.vm_instance: Refreshing state... [id=projects/test-project-345109/zones/australia-southeast1-a/instances/terraform-instance]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # google_compute_instance.vm_instance will be destroyed
  - resource "google_compute_instance" "vm_instance" {
      - can_ip_forward       = false -> null
      - cpu_platform         = "Intel Broadwell" -> null
      - current_status       = "RUNNING" -> null
      - deletion_protection  = false -> null
      - enable_display       = false -> null
      - guest_accelerator    = [] -> null
      - id                   = "projects/test-project-345109/zones/australia-southeast1-a/instances/terraform-instance" -> null
      - instance_id          = "6552850478757173275" -> null
      - label_fingerprint    = "42WmSpB8rSM=" -> null
      - labels               = {} -> null
      - machine_type         = "f1-micro" -> null
      - metadata             = {} -> null
      - metadata_fingerprint = "43DNKhxlZco=" -> null
      - name                 = "terraform-instance" -> null
      - project              = "test-project-345109" -> null
      - resource_policies    = [] -> null
      - self_link            = "https://www.googleapis.com/compute/v1/projects/test-project-345109/zones/australia-southeast1-a/instances/terraform-instance" -> null
      - tags                 = [] -> null
      - tags_fingerprint     = "42WmSpB8rSM=" -> null
      - zone                 = "australia-southeast1-a" -> null

      - boot_disk {
          - auto_delete = true -> null
          - device_name = "persistent-disk-0" -> null
          - mode        = "READ_WRITE" -> null
          - source      = "https://www.googleapis.com/compute/v1/projects/test-project-345109/zones/australia-southeast1-a/disks/terraform-instance" -> null

          - initialize_params {
              - image  = "https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-9-stretch-v20220317" -> null
              - labels = {} -> null
              - size   = 10 -> null
              - type   = "pd-standard" -> null
            }
        }

      - network_interface {
          - name               = "nic0" -> null
          - network            = "https://www.googleapis.com/compute/v1/projects/test-project-345109/global/networks/default" -> null
          - network_ip         = "10.152.0.2" -> null
          - queue_count        = 0 -> null
          - stack_type         = "IPV4_ONLY" -> null
          - subnetwork         = "https://www.googleapis.com/compute/v1/projects/test-project-345109/regions/australia-southeast1/subnetworks/default" -> null
          - subnetwork_project = "test-project-345109" -> null

          - access_config {
              - nat_ip       = "34.116.64.143" -> null
              - network_tier = "PREMIUM" -> null
            }
        }

      - scheduling {
          - automatic_restart   = true -> null
          - min_node_cpus       = 0 -> null
          - on_host_maintenance = "MIGRATE" -> null
          - preemptible         = false -> null
        }
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only yes will be accepted to confirm.

  Enter a value: yes

google_compute_instance.vm_instance: Destroying... [id=projects/test-project-345109/zones/australia-southeast1-a/instances/terraform-instance]
google_compute_instance.vm_instance: Still destroying... [id=projects/test-project-345109/zones/aust...theast1-a/instances/terraform-instance, 10s elapsed]
google_compute_instance.vm_instance: Still destroying... [id=projects/test-project-345109/zones/aust...theast1-a/instances/terraform-instance, 20s elapsed]
google_compute_instance.vm_instance: Destruction complete after 22s

Destroy complete! Resources: 1 destroyed.

And one last check to confirm the instance is gone.

cmd
gcloud compute instances list --project test-project-<id>

# Output
Listed 0 items.

I believe this means we are devops now.

Outro

If you made it this far, thanks for following along.

In this post, we setup our GCP environment with a service account that is used by Terraform to create resources via the GCP API.

Smoke me a kipper, i'll be back for breakfast!