Beginners Guide to building in GCP with Terraform

Beginners Guide to building in GCP with Terraform

Automate the build of a web server and Ansible on Google Compute Engine with Terraform

ยท

6 min read

Goal

This article will set out what you need in order to be able to standup an automated build of a VM (Google Cloud) a web server (Apache) and Ansible.

What will we cover

  • Deployment of a Google Compute VM instance using Terraform
  • Use of a Compute Instance startup script or user-data
  • Rendering a Terraform template

Dependencies

You have created a GCP account (or trial account). You will receive $300 of credits and also 750 hours per month will be free if you select the e2-micro instance. More information can be found in the tutorial below including the configuration of your gcp-creds.json file

The Google Cloud SDK is installed link here https://cloud.google.com/sdk/docs/install

You can validate this with gcloud --version

gcloud version.png Terraform is installed link here [terraform.io/downloads]

You can validate this with terraform --version terraform version.png

Our Terraform code will build all the firewall rules, network, API dependencies and resources along with ssh key access using your own public key id_rsa.pub

Lets Begin

Creating a Google Compute Engine VM

Create a local directory for your terraform project create directory.png

Define the Google provider in your terraform code along with client.json credentials.

Create all the code in your terraform project directory

vi main.tf

# Main terraform code block
# Author:       Andrew Longmuir
# Date:         March 2022
# Description:  Automate build of CE, Firewall rules & tags. Install web server, ansible & docker
# Platform:     GCP
# Version:      1.0
#########################################################################

provider "google" {
  credentials = file("my-gcp-creds.json") // export GCLOUD_KEYFILE_JSON
  project     = "dev-work-342110"         // export GCLOUD_PROJECT
  region      = "us-west1"                // export GCLOUD_REGION
  zone        = "us-west1-c"              // export GCLOUD_ZONE
}

**NOTE** this can also be set as variables see addendum for additional improvements.

We are now going to create the resources in our code:

Firewall rules

resource "google_compute_firewall" "web" {
  name          = "web-access"
  network       = "default"
  source_ranges = ["0.0.0.0/0"]
  allow {
    protocol = "tcp"
    ports    = ["80", "443", "22"]
  }
  target_tags = ["http-server", "https-server", "allow-ssh"]
  priority    = 1000
}

Enable APIs

resource "google_project_service" "api" {
  for_each = toset([
    "cloudresourcemanager.googleapis.com",
    "compute.googleapis.com"
  ])
  disable_on_destroy = false
  service            = each.value
}

Configure Compute Engine instance, network and bootstrapping

resource "google_compute_instance" "gcp-dev-build" {
  name         = "gcp-server-dev"
  machine_type = "e2-micro"
  boot_disk {
    initialize_params {
      image = "ubuntu-1804-bionic-v20220131"
    }
  }
  network_interface {
    network = "default" // This enables Private IP Address
    access_config {}    // This enables Public IP Address
  }

Apply the firewall rule to allow external IPs to access this instance

  tags = ["http-server", "https-server", "gcp-dev-build"]

To use the latest debian disk image we can use the data source and any type of instance can be selected:

    data "google_compute_image" "debian" {
    family  = "ubuntu-1804-lts"
    project = "gce-uefi-images"
    }

You can play around with different build installations

Invoke our build script

  metadata_startup_script = file("/Users/alongmuir/coding/terraform-project/gcp-user-data.sh")

  depends_on = [google_project_service.api, google_compute_firewall.web]
}

The contents of the gcp-user-data.sh are as follows.

  #!/bin/bash
  # Test Basic Web Page. Install updates, apache and enable/start
  sudo apt-get update
  sudo apt-get install -y apache2
  sudo systemctl start apache2
  sudo systemctl enable apache2
  sudo echo "<h1>Instance deployed by Terraform in GCP instance build time</h1> `date`" | sudo tee /var/www/html/index.html

  # Install pip and ansible
  sudo curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
  sudo python3 get-pip.py --user
  sudo python3 -m pip install --user ansible
  sudo python3 get-pip.py
  sudo python3 -m pip install ansible

  # Upgrade to latest pip
  sudo python3 -m pip install --upgrade pip
  sudo apt install ansible -y

Pass public RSA key via terraform to VM instance

# Pass public RSA key via terraform to VM instance
resource "google_compute_project_metadata" "my_ssh_key" {
  metadata = {
    ssh-keys = <<EOF
      < Paste the contents of your id_rsa.pub file here>
    EOF
  }
}

This will hardcode they key into your code there are ways you can obfuscate this. Some additional research for you ๐Ÿ˜‰ but worth noting not to push your code to any public GIT repos.

Lets display our outputs

data "google_project" "project" {
}

output "instance_ip" {
  value       = join(" ", google_compute_instance.gcp-dev-build.*.network_interface.0.access_config.0.nat_ip)
  description = "The public IP address of the newly created instance"
}

output "project_name" {
  value = data.google_project.project.name
}

Lets Initialise our Terraform Project

To do this run terraform init see output below.

alongmuir@Mac-mini terraform-project % terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/google...
- Installing hashicorp/google v4.13.0...
- Installed hashicorp/google v4.13.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.
alongmuir@Mac-mini terraform-project %

This will download your provider resources defined earlier. You can view this under .terraform folder in your project directory.

Now lets validate the syntax of our code with terraform validate

validate.png

Running terraform plan will inform you of what will be created or changed when running your code. Terraform remembers this by its state file. I'll do an additional article on how to configure this best practice.

OK ... lets build our resource in GCP. To do this execute terraform apply and answer yes

If you run terraform apply -auto-approve this will omit the need to validate with a prompt.

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

Changes to Outputs:
  + instance_ip  = (known after apply)
  + project_name = "Dev-Work"
google_project_service.api["compute.googleapis.com"]: Creating...
google_project_service.api["cloudresourcemanager.googleapis.com"]: Creating...
google_compute_project_metadata.my_ssh_key: Creating...
google_compute_firewall.web: Creating...
google_project_service.api["compute.googleapis.com"]: Creation complete after 4s [id=dev-work-342110/compute.googleapis.com]
google_project_service.api["cloudresourcemanager.googleapis.com"]: Creation complete after 4s [id=dev-work-342110/cloudresourcemanager.googleapis.com]
google_compute_project_metadata.my_ssh_key: Still creating... [10s elapsed]
google_compute_firewall.web: Still creating... [10s elapsed]
google_compute_firewall.web: Creation complete after 12s [id=projects/dev-work-342110/global/firewalls/web-access]
google_compute_instance.gcp-dev-build: Creating...
google_compute_project_metadata.my_ssh_key: Creation complete after 12s [id=dev-work-342110]
google_compute_instance.gcp-dev-build: Still creating... [10s elapsed]
google_compute_instance.gcp-dev-build: Creation complete after 13s [id=projects/dev-work-342110/zones/us-west1-c/instances/gcp-server-dev]

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

Outputs:

instance_ip = "34.83.92.251"
project_name = "Dev-Work"
alongmuir@Mac-mini terraform-project %

Now if you navigate to the Google Console and navigate to Compute Engine --> VM Instance, you will see an instance coming up. Once the instance is up successfully, browse the webserver_ip. In this case, go to http://34.83.92.251

gcp console.png

NOTE You can see the terraform output showing the public IP address

Here we can see the web server is provisioned by code and build time

web server build.png

To remove all resources that have been created just run terraform destroy you can also apply the same switch if you do not wish to be prompted

Additional Improvements

Given the size and complexity of the terraform code it isn't worth the effort to break this down into modules and separate .tf files. However this is good practice so for extra practice attempt the following:

Remember Terraform is a declarative language so this will help in modularisation of your code.

  • Create separate .tf module files
  • Create custom variables
  • Running terraform fmt will ensure your code is nicely formatted.
  • Test the Ansible deployment by configuring a local Inventory.txt file and running the following command.
ansible gcp_servers -m script -a "update_web_server.sh"

TIP

The contents of your update_web_server.sh file should be something like

sudo echo "<h1>Instance Built Using Terraform in GCP</h1> `date`" | sudo tee /var/www/html/index.html

Thats all until next time ๐Ÿ’ฅ

Good luck and Happy Coding ๐Ÿ’ป

ย