Getting started with Terraform on Google Cloud Platform (GCP)

Getting started with Terraform on Google Cloud Platform (GCP)

Getting started with Terraform on Google Cloud Platform (GCP)

In this tutorial, you will learn how to create a self-hosted WordPress instance.

Goal

Beginning a journey with any tool, a million and one made a mistake I call “the first time problem”. People look for the instructions or tutorials, but they seek them just to copy and paste the code. They do that because they don’t have goals, and their only desire is to finish the next point in training on the platform, to have a badge on their profile.

The purpose of this article is not to be another copy-paste template. We want to give you a clear path from point A to point B, which means from a bare repository to a finished, ready-to-use WordPress page, but without specifying every move. In this journey, we leave you the freedom to deepen your knowledge about used tools and resources.

Getting started with Terraform on Google Cloud Platform (GCP)

Project

First, we need to configure the Terraform provider, which allows us to connect with Google Cloud API and do the stuff. After that, we can go further. Do not worry about credentials, we will handle this in the other part of the article.

provider "google" {
 project     = “google-demo”
 region      = "europe-west1"
}

provider "google-beta" {
 project     = “google-demo”
 region      = "europe-west1"
}

Second, we need a place where resources could be stored. It is required because without that, we will not be able to create the resources. For this purpose, GCP using for that resource called ‘project’. It works as a container for all of our components. This article will assume that you already have a project created, and its name is “google-demo”. 

Network

Core networking

To work with a machine in our project or display our page in the web browser, we need a virtual network where other Google Cloud resources, like nodes, addresses, firewall rules, could be added.

First of all, create a network and subnetwork with an available IP range for our machines.

resource "google_compute_network" "this" {
  auto_create_subnetworks = false
  name                    = "example"
  routing_mode            = "REGIONAL"
}

resource "google_compute_subnetwork" "this" {
  name          = "example"
  ip_cidr_range = 192.168.24.0/24
  region        = "europe-west1"
  network       = google_compute_network.this.id
}

Secondly, create a private address for our database instance, which we will create later because it should not be publicly available.

resource "google_compute_global_address" "this" {
  provider = google-beta

  name          = "private-ip-db-address"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = 16
  network       = google_compute_network.this.id
}

resource "google_service_networking_connection" "this" {
  provider = google-beta

  network                 = google_compute_network.this.id
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.this.name]
}

DNS

No one uses an IP address to reach sites on the internet, therefore we need a domain address. To get it, create an address resource and bind DNS records of “A” and “CNAME” types to expose our page on domain www.example.com to the world. Of course, Cloud DNS hosted zone with your desired name is required – in this article, we will use an exemplary “example-com” zone and assume that we have registered it previously.

resource "google_compute_address" "this" {
  name   = "example"
  region = "europe-west1"
}

resource "google_dns_record_set" "wordpress" {
  name         = "example.com."
  type         = "A"
  ttl          = 300
  managed_zone = "example-com"

  rrdatas = [google_compute_address.this.address]
}

resource "google_dns_record_set" "wordpress_www" {
  name         = "www.example.com."
  type         = "CNAME"
  ttl          = 300
  managed_zone = "example-com"

  rrdatas = ["example.com."]
}

Firewall

Last but not least, we need to configure the most crucial component of our infrastructure, which is the firewall. It is critical because we don’t want to accidentally open ports or expose instances to the world.

resource "google_compute_firewall" "wordpress_ingress" {
  name    = "example-http"
  network = google_compute_network.this.id

  allow {
    protocol = "icmp"
  }

  allow {
    protocol = "tcp"
    ports    = ["80", "443"]
  }

  source_ranges = ["0.0.0.0/0"]
}

resource "google_compute_firewall" "wordpress_ingress_ssh" {
  name    = "example-ssh"
  network = google_compute_network.this.id

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  source_ranges = ["<your_IP>/32"]
}

Database

To properly launch a functional WordPress instance, we need a database. The most common option chosen by people, is MySQL, because WordPress supports it out of the box. We can create another machine for the database, install it and configure it, but why would we? We can utilize managed services using the cloud and do not need to worry about creating the whole infrastructure from scratch. Instead, relying on resources, with a few lines of code we can deploy a new and shiny MySQL instance ready to work.

resource "google_sql_database_instance" "this" {
  database_version = "MYSQL_5_7"
  name             = example-wordpress
  region           = "europe-west1"

  depends_on = [
  google_service_networking_connection.this]

  settings {
    availability_type = "REGIONAL"
    disk_autoresize   = false
    disk_size         = 50
    disk_type         = "PD_HDD"
    tier              = "db-g1-small"

    backup_configuration {
      enabled            = true
      start_time         = "04:00"
      binary_log_enabled = true
    }

    ip_configuration {
      ipv4_enabled    = false
      private_network = google_compute_network.this.id
    }

    location_preference {
      zone = "europe-west1-a"
    }

    database_flags {
      name  = "max_connections"
      value = 500
    }
  }
}

resource "google_sql_database" "this" {
  name      = "wordpress"
  instance  = google_sql_database_instance.this.name
  charset   = "utf8"
  collation = "utf8_general_ci"
}
resource "random_string" "this" {
 length    = 24
 special   = false
 min_upper = 5
 min_lower = 5
}

resource "random_password" "this" {
 length    = 24
 special   = false
 min_upper = 5
 min_lower = 5
}

resource "google_sql_user" "this" {
 name     = random_string.this.result
 password = random_password.this.result
 instance = google_sql_database_instance.this.name
}

To get username and password created by Terraform, we add output code:

output "sql_db_username" {
 value = random_string.this.result
 sensitive = true
}
output "sql_db_password" {
 value = random_password.this.result
 sensitive = true
}

Compute node

Initialization script

To install WordPress on our machine, we create a simple script, which installs a web server (Apache2) and downloads a package with WordPress after operating system startup.

#!/usr/bin/env bash

apt update
apt install -y apache2 \
            ghostscript \
            libapache2-mod-php \
            php \
            php-bcmath \
            php-curl \
            php-imagick \
            php-intl \
            php-json \
            php-mbstring \
            php-mysql \
            php-xml \
            php-zip

mkdir -p /srv/www
sudo chown www-data: /srv/www
curl --silent https://wordpress.org/latest.tar.gz | sudo -u www-data tar zx -C /srv/www
find /srv/www/ -type d -exec chmod 755 {} \;
find /srv/www/ -type f -exec chmod 644 {} \;
cat << EOF > /etc/apache2/sites-available/000-default.conf
<VirtualHost *:80>
  DocumentRoot /srv/www/wordpress
  <Directory /srv/www/wordpress>
      Options FollowSymLinks
      Require all granted
      DirectoryIndex index.php
      Order allow,deny
      Allow from all
  </Directory>
  <Directory /srv/www/wordpress/wp-content>
      Options FollowSymLinks
      Order allow,deny
      Allow from all
  </Directory>
</VirtualHost>
EOF
a2enmod rewrite
systemctl reload apache2
sudo -u www-data cp /srv/www/wordpress/wp-config-sample.php /srv/www/wordpress/wp-config.php

sudo -u www-data sed -i 's/database_name_here/wordpress/' /srv/www/wordpress/wp-config.php
sudo -u www-data sed -i 's/username_here/${DB_USERNAME}/' /srv/www/wordpress/wp-config.php
sudo -u www-data sed -i 's/password_here/${DB_PASSWORD}/' /srv/www/wordpress/wp-config.php
sudo -u www-data sed -i 's/localhost/${DB_HOST}/' /srv/www/wordpress/wp-config.php

systemctl restart apache2

Machine

After we went through the initial configuration, we are able to create the machine. To do that, we will need some details about:

  • which type of machine we chose, 
  • what image we chose,
  • what size of disk we chose.

First from our list is machine type. We can check it in GCP documentation. For tutorial purposes, we don’t need a big machine with a ton of vCPUs and RAM memory available, so we chose a small one, e.g., e2-standard-2.

The second is an image. The standard option is, of course, GNU/Linux distribution. I prefer Debian, but you can choose whatever distribution you want from this list

The template of image value looks like this: ‘image-project/image-family’.

Last, we need to think about the size of our attached disk. For tutorial purposes, we use a small number of course, but if you want to do it right in your case, you need to think about extra size for: 

  • future WordPress updates,
  • future WordPress plugins updates,
  • your custom changes in page,
  • custom themes for your page,
  • uploaded assets,
  • page cache.

We gathered all the needed information, so let’s code it.

resource "google_compute_instance" "this" {
 name                    = "example-wordpress"
 machine_type            = "e2-standard-2"
 zone                    = "europe-west1-b"
  metadata_startup_script = templatefile("${path.module}/init.sh", {
    DB_USERNAME = random_string.this.result
    DB_PASSWORD = random_password.this.result
    DB_HOST     = google_sql_database_instance.this.private_ip_address
  })

 boot_disk {
   initialize_params {
     image = “debian-cloud/debian-10     size  = 50
   }
 }

 network_interface {
   subnetwork = google_compute_subnetwork.this.id

   access_config {
     nat_ip = google_compute_address.this.address
   }
 }

 service_account {
   scopes = ["userinfo-email", "compute-ro", "storage-ro"]
 }
}

resource "google_compute_resource_policy" "this" {
 name   = "example-wordpress"
 region = “europe-west1”

 snapshot_schedule_policy {
  schedule {
    daily_schedule {
      days_in_cycle = 1
      start_time    = "02:00"
    }
  }

  retention_policy {
    max_retention_days    = 60
    on_source_disk_delete = "KEEP_AUTO_SNAPSHOTS"
  }

  snapshot_properties {
    storage_locations = ["eu"]
  }
 }
}

resource "google_compute_disk_resource_policy_attachment" "this" {
 name       = google_compute_resource_policy.this.name
 disk         = "example-wordpress"
 zone        = "europe-west1-b"
 depends_on = [google_compute_instance.this]
}

At this point, our code is ready. We can push it to our version control system of choice and proceed with automation configuration.

Spacelift integration

Step 1. Stack configuration

We do not want to deploy all the prepared code manually from our computer, we want it to be deployed automatically. For this, we will leverage Spacelift. At first, you need to create a stack:

add stack

Choose a version control system provider (GitHub in this scenario), repository and branch.

add stack

Next, we proceed to a backend configuration. Since we have been using Terraform, we will naturally select Terraform for our backend. For the purpose of this article, we will also let Spacelift manage our Terraform state.

add stack

Last but not least, a few minor details such as a worker pool and the name of a stack.

stack
stack

Step 2. Integration with GCP

Once our stack is created, we can proceed to integrate it with our Google Cloud Platform project. 

gcp

Edit your stack and go to “Integrations” tab. Chose Google Cloud Platform.

gcp

Generate a service account that will be used with this particular stack.

Now head to Google Cloud Platform console and into Identity and Access Management (IAM) tab.

gcp

Add appropriate permissions to the service account generated in the previous step. For the sake of the article, we will grant it Owner permissions (This permission is appropriate only for tutorial purposes. Never do that on real environments, because of issues and risks related to the possibility of hostile permission escalation.)

gcp

After this is done, you can simply click on a “Trigger” button in order to trigger a new deployment.

gcp

Conclusion

Looking at all the headings and code that we wrote today, it seems complicated and hard, but it’s not. If you have planned what has to be done, and you read descriptions added to properties that you want to use, it starts to arrange into a coherent whole.

Google Cloud Platform is big and has a lot of components, which might be frightening. To avoid losing your head, I recommend sticking to KISS principle – you can read about it a little more here

At the finish of this tutorial, you should have a working WordPress page and a bunch of scripts. As you have probably noticed, your site is not secured by SSL certificate. That is correct – this will be your homework, dear reader. Take what you have learned today and extend it!

Share this post

twitter logo

Comments