Terraform: Handling Errors with try(…)

PROBLEM

Given the following output block:-

output "subnet_uc1" {
  description = "Subnets in `us-central1` region for all 3 products"
  value = {
    artifactory = module.subnet_uc1_artifactory.subnets.name
    xray        = module.subnet_uc1_xray.subnets.name
    mc          = module.subnet_uc1_mc.subnets.name
  }
}

Sometimes, during an apply or destroy, we may get this error:-

Error: Attempt to get attribute from null value

  on outputs.tf line 40, in output "subnet_uc1":
  40:     artifactory = module.subnet_uc1_artifactory.subnets.name
    |----------------
    | module.subnet_uc1_artifactory.subnets is null

This value is null, so it does not have any attributes.


Error: Attempt to get attribute from null value

  on outputs.tf line 41, in output "subnet_uc1":
  41:     xray        = module.subnet_uc1_xray.subnets.name
    |----------------
    | module.subnet_uc1_xray.subnets is null

This value is null, so it does not have any attributes.


Error: Attempt to get attribute from null value

  on outputs.tf line 42, in output "subnet_uc1":
  42:     mc          = module.subnet_uc1_mc.subnets.name
    |----------------
    | module.subnet_uc1_mc.subnets is null

This value is null, so it does not have any attributes.

One way to fix this is to do conditional expressions like this, but it’s not pretty:-

output "subnet_uc1" {
  description = "Subnets in `us-central1` region for all 3 products"
  value = {
    artifactory = module.subnet_uc1_artifactory.subnets != null ? module.subnet_uc1_artifactory.subnets.name: ""
    xray        = module.subnet_uc1_xray.subnets != null ?module.subnet_uc1_xray.subnets.name: ""
    mc          = module.subnet_uc1_mc.subnets != null ? module.subnet_uc1_mc.subnets.name: ""
  }
}

SOLUTION

Since Terraform v0.12.20, we can solve this with try and achieve the same outcome:-

output "subnet_uc1" {
  description = "Subnets in `us-central1` region for all 3 products"
  value = {
    artifactory = try(module.subnet_uc1_artifactory.subnets.name, "")
    xray        = try(module.subnet_uc1_xray.subnets.name, "")
    mc          = try(module.subnet_uc1_mc.subnets.name, "")
  }
}

GCP: Deleting Project with Lien… Quickly

PROBLEM

The whole idea of placing a lien on a project is to prevent accidental deletion.

But, sometimes it’s a little pain in the ass to attempt a project deletion in GCP Console only to find out a lien was set, especially during the development phase.

Then, we grumpily open up Cloud Shell and run a series of commands to delete the project.

SOLUTION

To play fast and loose in the name of Shitty Agile, create a script called delete-project.sh with the following content:-

#!/bin/bash

set -e

project_id="$1"

gcloud config set project "${project_id}"

lien_id=$(gcloud alpha resource-manager liens list --format=json | jq -r '.[0] .name' | sed -e 's/liens\///g')

[[ "${lien_id}" != "null" ]] && gcloud alpha resource-manager liens delete "${lien_id}"

gcloud projects delete "${project_id}" --quiet

This script will delete a project regardless the existence of lien.

From Cloud Shell, run this bad boy:-

./delete-project.sh [PROJECT_ID]

Terraform: Skipping Buggy Provider Version

PROBLEM

Given the following required_providers block…

terraform {
  required_providers {
    google = "~> 3.8"
  }
}

… it will allow the following Google provider version: >= 3.8, < 4.0.

As of today (May 10), the latest Google provider is 3.20.0. A quick terraform init confirms that.

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "google" (hashicorp/google) 3.20.0...

However, sometimes, there’s a need to skip a buggy version. For example, 3.20.0 breaks google_compute_firewall.

SOLUTION

To achieve that, we can do the following…

terraform {
  required_providers {
    google = "~> 3.8, != 3.20.0"
  }
}

To confirm this works, after deleting .terraform/ dir, terraform init now shows the following result…

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "google" (hashicorp/google) 3.19.0...

GCP + Terraform: Running Terraform Commands with a Service Account

PROBLEM

When running these commands…

gcloud auth login
gcloud auth application-default login

… it allows terraform apply to provision the infrastructure using your credential.

However, sometimes there’s a need to run Terraform using a service account.

SOLUTION

First, identify the service account you want to use… for example: my-service-account@my-project.iam.gserviceaccount.com.

Then, create and download the private key for the service account.

Command:

gcloud iam service-accounts keys create --iam-account my-service-account@my-project.iam.gserviceaccount.com  key.json              

Output:

created key [xxxxxxxx] of type [json] as [key.json] for [my-service-account@my-project.iam.gserviceaccount.com]

With this service account’s private key, we can now authorize its access to GCP.

Command:

gcloud auth activate-service-account --key-file key.json  

Output:

Activated service account credentials for: [my-service-account@my-project.iam.gserviceaccount.com]

You can verify whether the right account is being used or not.

Command:

gcloud auth list

Output:

                      Credentialed Accounts
ACTIVE  ACCOUNT
*       my-service-account@my-project.iam.gserviceaccount.com
        user@myshittycode.com

To set the active account, run:
    $ gcloud config set account `ACCOUNT`

In this case, the * marks the active account being used.

Now, you can run terraform apply to provision the infrastructure using the selected service account.

GCP + Kitchen Terraform: Local Development Workflow

INTRODUCTION

Here’s a typical workflow for implementing and running Kitchen Terraform tests outside of the GCP environment, for example, from an IDE on a Mac laptop.

Enable “gcloud” Access

Command:

gcloud auth login

The first step is to ensure we can interact with GCP using the gcloud command using our user credential. This is needed because the tests use the gcloud commands to retrieve GCP resource information in order to do the assertions.

Enable SDK Access

Command:

gcloud auth application-default login

This ensures our Terraform code can run the GCP SDK successfully without a service account. Instead, it will use our user credential.

Without this command, we may get the following error when running the Terraform code:

Response: {
 "error": "invalid_grant",
 "error_description": "reauth related error (invalid_rapt)",
 "error_subtype": "invalid_rapt"
}

Display All Kitchen Test Suites

Command:

bundle exec kitchen list    

This command displays a list of Kitchen test suites defined in kitchen.yml.

The output looks something like this:

Instance                            Driver     Provisioner  Verifier   Transport  Last Action    Last Error
router-all-subnets-ip-ranges-local  Terraform  Terraform    Terraform  Ssh          
router-interface-local              Terraform  Terraform    Terraform  Ssh          
router-no-bgp-no-nat-local          Terraform  Terraform    Terraform  Ssh          
router-with-bgp-local               Terraform  Terraform    Terraform  Ssh          
router-with-nat-local               Terraform  Terraform    Terraform  Ssh          

Run a Specific Test Suite

Command:

bundle exec kitchen test [INSTANCE_NAME]    

# For example:-
bundle exec kitchen test router-with-nat-local

This command allows us to run a specific test suite. This will handle the entire Terraform lifecycle… ie: setting up the infrastructure, running the tests and destroying the infrastructure.

This is helpful especially when we need to run just the test suite that is currently under development. This way, it runs faster because we don’t have to provision/deprovision the cloud infrastructure for other test suites. At the same time, we will also reduce the incurred cost.

Run a Specific Test Suite with Finer Controls

There are times where running bundle exec kitchen test [INSTANCE_NAME] is still very time consuming and expensive, especially when we try to debug any failed assertions or add a little assertions at a time.

To provision the infrastructure once, run the following command:

bundle exec kitchen converge [INSTANCE_NAME]    

# For example:-
bundle exec kitchen converge router-with-nat-local

To run the assertions, run the following command as many times as possible until all the assertions are implemented successfully:

bundle exec kitchen verify [INSTANCE_NAME]    

# For example:-
bundle exec kitchen verify router-with-nat-local

Finally, once the test suite is implemented properly, we can now deprovision the infrastructure:

bundle exec kitchen destroy [INSTANCE_NAME]    

# For example:-
bundle exec kitchen destroy router-with-nat-local