Deploy and configure Google Compute Engine VMs with Terraform

Mihai Bojin
5 min readJul 30, 2022

--

Photo by Alexandre Chambon on Unsplash

I recently wanted to deploy and configure a Virtual Machine on Google Compute Engine using Terraform. I thought I’d document my steps to help anyone looking to achieve similar goals. Read on below and easily recreate all the steps. Good luck in all your automations!

Why Terraform?

By now, my preference for using Terraform to automate deployments is well documented. I like all my environments to be throwaways because it helps me sleep better at night knowing that I can always recreate them fully! “Did my VM crash? No problem, I’ll just delete it and start again!” (note: not always the best approach, but having the option is great!)

Ok, back to the task at hand…

What will this tutorial cover?

  • Provision Compute Engine instances with Terraform
  • Add SSH keys
  • Run cloud-init to create a super user for the Terraform Provider
  • Create a custom network/subnet with IPv6 support
  • Reserve a static IPv4 address
  • Allocate IPv4/IPv6 addresses to the Compute Engine VM
  • Set PTR (reverse DNS) records

This tutorial assumes you have working knowledge of Terraform. If not, check out Google’s tutorial; Hashicorp’s site is also a good resource to peruse!

Let’s begin!

Provision a Google Compute Engine instance

Let’s spin up a standalone Ubuntu 20.04 VM.

provider "google" {
project = "YOUR_PROJECT_ID"
region = "europe-west1"
zone = "europe-west1-c"
}
resource "google_compute_instance" "default" {
provider = google
name = "default"
machine_type = "e2-micro"
network_interface {
network = "default"
}

boot_disk {
initialize_params {
image = "ubuntu-os-cloud/ubuntu-2004-focal-v20220712"
}
}
# Some changes require full VM restarts
# consider disabling this flag in production
# depending on your needs
allow_stopping_for_update = true
}

Only required once: run terraform init to configure the required provider(s).

Run terraform apply, and you’re good to go. That was easy, right!?

But wait, what do I do with this VM? You can’t actually SSH into it yet (unless, of course, you have configured project-level SSH keys).

Let’s manually add some SSH keys!

Add SSH keys to your Compute Engine VM

Change your google_compute_instance block and add a metadata section:

resource "google_compute_instance" "default" {
provider = google
name = "default"
machine_type = "e2-micro"
### ADD THIS BLOCK
metadata = {
ssh-keys = "YOUR_USERNAME:${file("~/.ssh/id_rsa.pub")}"
}
network_interface {
network = "default"
}

boot_disk {
initialize_params {
image = "ubuntu-os-cloud/ubuntu-2004-focal-v20220712"
}
}
}

Replace YOUR_USERNAME with your Google account’s username, i.e.:

  • myuser for myuser@gmail.com
  • my_user for my.user@gmail.com
  • etc. (more details here)

Then rerun terraform apply.

Using cloud-init to provision a super-user account

Cloud-init is the de-facto way for configuring cloud instances. All the major providers support it, Google being one of them. Here’s a simple example that provisions a super user for Terraform, allowing it to perform any necessary steps during instance initialization.

The following example uses the cloud-config format. There are others.

#cloud-config# Create a group
groups:
- hashicorp

# Create users, in addition to the users provided by default
users:
- default
- name: terraform
gecos: terraform
shell: /bin/bash
primary_group: hashicorp
sudo: ALL=(ALL) NOPASSWD:ALL
groups: users, admin
lock_passwd: false
ssh_authorized_keys:
- "ssh-rsa ..." # an SSH public key that is authorized
# to connect to this account
# Run a few commands (update apt's repo indexes and install curl)
runcmd:
- sudo apt-get update
- sudo apt install curl -q -y
- echo "Done"

Save the file above into your Terraform directory as cloud-config.yaml.

Then, reconfigure your Cloud Engine deployment to use this file:

resource "google_compute_instance" "default" {
...
metadata = {
user-data = file("${path.module}/cloud-config.yaml")
}
}

Once the GCP reprovisions the VM, you will notice that the changes requested in cloud-config.yaml, have been applied. This is a useful example that creates a dedicated user, allowing Terraform to SSH in and further set up the instance.

What about IPv6?

To allocate a public IPv6 address, we must add a few more resources to our Terraform config:

# Create a network
resource "google_compute_network" "ipv6net" {
provider = google
name = "ipv6net"
auto_create_subnetworks = false
}
# Create a subnet with IPv6 capabilities
resource "google_compute_subnetwork" "ipv6subnet" {
provider = google
name = "ipv6subnet"
network = google_compute_network.ipv6.id
ip_cidr_range = "10.0.0.0/8"
stack_type = "IPV4_IPV6"
ipv6_access_type = "EXTERNAL"
}
# Allow SSH from all IPs (insecure, but ok for this tutorial)
resource "google_compute_firewall" "firewall" {
provider = google
name = firewall"
network = google_compute_network.ipv6net.name

allow {
protocol = "icmp"
}

source_ranges = ["0.0.0.0/0"]
allow {
protocol = "tcp"
ports = ["22"]
}
}

We must also change our VM’s network settings. Replace the network_interface block in your previous config with the following.

resource "google_compute_instance" "default" {
...
network_interface {
network = google_compute_network.ipv6net.id
subnetwork = google_compute_subnetwork.ipv6subnet.id
stack_type = "IPV4_IPV6"
access_config {
nat_ip = google_compute_address.ipv4.address
network_tier = "PREMIUM"
}

ipv6_access_config {
network_tier = "PREMIUM"
}
}

Rerun terraform apply (note that the VM will restart).

By the way, if you’re wondering why PREMIUM, it’s because of the following requirement “Network tier in IPv6 access config must be PREMIUM”.

What’s next? Oh right…

Reserve a static IPv4 address

Another easy config:

resource "google_compute_address" "static-ip" {
provider = google
name = "static-ip"
address_type = "EXTERNAL"
network_tier = "PREMIUM"
}

Granted, you must also update your instance’s network interface:

resource "google_compute_instance" "default" {
...
network_interface {
...
access_config {
...
nat_ip = google_compute_address.static-ip.address
}
}
}

And then rerun terraform apply.

Set a PTR record

The end is in sight; one last thing to do: add the PTR record!

For example, a reverse DNS record is usually required for SMTP servers (otherwise, any emails received from your IP will end up flagged as spam).

Add the public_ptr_domain_name line in your config and rerun terraform apply.

resource "google_compute_instance" "default" {
...
network_interface {
access_config {
public_ptr_domain_name = "YOUR.DOMAIN.COM."
}
}
}

Defining a PTR record for the instance’s IPv6 address is just as easy:

resource "google_compute_instance" "default" {
...
network_interface {
ipv6_access_config {
public_ptr_domain_name = "YOUR.DOMAIN.COM."
}
}
}

Note that the trailing dot after “YOUR.DOMAIN.COM.” is not a mistake. For DNS records, it signifies that the provided name is a complete FQDN and not an alias (CNAME) defined on the domain’s zone.

To confirm the PTR records have been set, you can run the following command:

# Search for the reverse DNS record of an IP
# using Google's DNS servers
dig -x "$IP" @8.8.8.8
;; ANSWER SECTION:
0.0.0.0.0.0.0.0.1.0.0.0.0.0.0.0.2.5.8.2.0.d.0.4.0.0.9.1.0.0.6.2.ip6.arpa. 60 IN PTR YOUR.DOMAIN.COM.

That was all! A quick incursion into basic Google Compute Engine provisioning, using Terraform for peace of mind. Let me know what you think in the comments!

Until next time, — Mihai

--

--

Mihai Bojin

Software Engineer at heart, Manager by day, Indie Hacker at night. Writing about DevOps, Software engineering, and Cloud computing. Opinions my own.