This post covers how to build Google Cloud Platform infrastructure using Terraform.

The files you need include:

  • main.tf
  • variables.tf
  • backend.tf (Optional)

The configurations are referenced from inside modules in this implementation, but you could have everything defined in one main.tf.

Note: Google Cloud Platform requires APIs to be enabled, and you will need account credentials with sufficient permission to build the following resources. The project will need to be linked to a billing account.

GCP Setup via CLI

Here's what you'll need to do to get set up via the CLI:

$ export PROJECT_ID=my_gcp_project
$ export ACCOUNT_ID=$(gcloud beta billing accounts list | grep True | cut -d ' ' -f1)
$ gcloud auth login
$ gcloud projects create $PROJECT_ID
$ gcloud config set compute/region us-east1
$ gcloud config set project $PROJECT_ID
$ gcloud beta billing projects link $PROJECT_ID --billing-account=$ACCOUNT_ID

# enable apis
$ gcloud services enable \
    cloudapis.googleapis.com \
    cloudresourcemanager.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    iam.googleapis.com \
    redis.googleapis.com \
    servicenetworking.googleapis.com \
    sqladmin.googleapis.com

# If you want to use gcs for remote storage
$ gsutil mb -c standard -l us-east1 gs://$PROJECT_ID

# Create a service account for terraform
$ gcloud iam service-accounts create terraform \
    --description="Terraform Service Account" \
    --display-name="Terraform"
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member serviceAccount:terraform@$PROJECT_ID.iam.gserviceaccount.com \
  --role roles/owner
$ gcloud iam service-accounts keys create CREDENTIALS_FILE.json --iam-account=terraform@$PROJECT_ID.iam.gserviceaccount.com --project $PROJECT_ID
$ mv CREDENTIALS_FILE.json terraform/
gcloud CLI setup.

Setting up Terraform

Once all the variables are set, run:

$ terraform init
Initialize modules

This will download the necessary files to ./terraform/.

Next we'll create a dry-run execution plan to see what infrastructure will be built, and decide if any changes are necessary:

$ terraform plan
Create an execution plan

The following will create the infrastructure as it's been defined:

$ terraform apply
Apply changes

Configurations for GCP Infrastructure

Let's get into what our actual Terraform configurations look like. We start with some general configuration in main.tf:

locals {
  database_version = "" # "POSTGRES_11"
  network          = "" # Network name
  region           = "" # us-east1
  project_id       = "" # GCP Project ID
  subnetwork       = "" # Subnetwork name
}

// Configure the Google Cloud provider
provider "google" {
 credentials = file("CREDENTIALS_FILE.json")
 project     = local.project_id
 region      = local.region
}

provider "google-beta" {
  credentials = file("CREDENTIALS_FILE.json")
  project     = local.project_id
  region      = local.region
}

module "cloudsql" {
  source           = "./modules/cloudsql"
  network          = local.network
  private_ip_name  = "" # Private IP Name
  project          = local.project_id
  region           = local.region
}

module "gke" {
  source           = "./modules/gke"
  cluster          = "" # Cluster Name
  network          = local.network
  project          = local.project_id
  region           = local.region
  subnetwork       = local.subnetwork
  zones            = "" # ["us-east1-b", "us-east1-c", "us-east1-d"]
}

module "memorystore" {
  source         = "./modules/memorystore"
  display_name   = "" # Display Name
  ip_range       = "" #
  location       = "" # Zone
  name           = "" # Instance name
  network        = local.network
  project        = local.project_id
  redis_version  = "" # 5.0
  region         = local.region
  size           = "" # 1
  tier           = "" # STANDARD
}

module "vpc" {
  source           = "./modules/vpc"
  project          = local.project_id
  network          = local.network
  region           = local.region
  subnetwork       = local.subnetwork
}
main.tf

Next we need to make Terraform aware of our credentials file in backend.tf:

data "terraform_remote_state" "backend" {
  backend = "gcs"
  config = {
    bucket  = ""
    prefix  = "terraform"
    credentials = file("CREDENTIALS_FILE.json")
  }
}
backend.tf
# GCP variables
variables.tf

CloudSQL Configuration

locals {
  network          = join("/", ["projects", var.project, "global", "networks", var.network])
}

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

  name          = var.private_ip_name
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = 16
  network       = local.network
  depends_on    = [local.network]
}

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

  network                 = local.network
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.private_ip_address.name]
  depends_on              = [local.network]
}

resource "random_id" "db_name_suffix" {
  byte_length = 4
}

resource "google_sql_database_instance" "instance" {
  provider = google-beta

  name   = "private-instance-${random_id.db_name_suffix.hex}"
  database_version = var.database_version
  region = var.region

  depends_on = [google_service_networking_connection.private_vpc_connection]

  settings {
    tier = "db-custom-1-3840"
    ip_configuration {
      ipv4_enabled    = false
      private_network = local.network
    }
  }
}

resource "google_sql_user" "users" {
  name     = var.user_name
  instance = google_sql_database_instance.instance.name
  password = var.user_password
}
modules/cloudsql/main.tf
variable "database_version" {
  description = "The database version"
}

variable "network" {
  description = "The name of the network being created"
}

variable "private_ip_name" {
  description = "The name of the private ip address being created"
}

variable "project" {
  description = "Project ID"
}

variable "region" {
  description = "Region"
}

variable "user_name" {
  default     = "DB_USER"
}

variable "user_password" {
  default     = "DB_PASSWORD"
}
modules/cloudsql/variables.tf

Kubernetes Configuration

module "gke" {
  source                     = "terraform-google-modules/kubernetes-engine/google"
  project_id                 = var.project
  name                       = var.cluster
  region                     = var.region
  zones                      = var.zones
  network                    = var.network
  subnetwork                 = var.subnetwork
  ip_range_pods              = join("-",[var.subnetwork,"pods"])
  ip_range_services          = join("-",[var.subnetwork,"services"])
  http_load_balancing        = "true"
  horizontal_pod_autoscaling = "true"
  network_policy             = "true"
  maintenance_start_time     = "05:00"
  remove_default_node_pool   = "true"

  node_pools = [
    {
      name               = "pool-1"
      machine_type       = "n1-standard-2"
      min_count          = 1
      max_count          = 10
      local_ssd_count    = 0
      disk_size_gb       = 100
      disk_type          = "pd-standard"
      image_type         = "COS"
      auto_repair        = "true"
      auto_upgrade       = "true"
      preemptible        = "true"
      initial_node_count = 1
    }
  ]

  node_pools_oauth_scopes = {
    all = [
      "https://www.googleapis.com/auth/cloud-platform",
    ]
  }

  node_pools_labels = {
    all = {}

  }

  node_pools_metadata = {
    all = {}

  }

  node_pools_tags = {
    all = []

  }
}
modules/gke/main.tf
variable "cluster" {
  description = "Cluster name"
}

variable "kubernetes_version" {
  description = "The Kubernetes version of the masters. If set to 'latest' it will pull latest available version in the selected region."
  type        = string
  default     = "latest"
}

variable "network" {
  description = "The name of the network being created"
}

variable "project" {
  description = "Project"
}

variable "region" {
  description = "Region of resources"
}

variable "subnetwork" {
  description = "The name of the subnetwork being created"
}

variable "zones" {
  description = "Zones"
}
modules/gke/variables.tf

Redis Configuration

resource "google_redis_instance" "cache" {
  authorized_network      = var.network
  display_name            = var.display_name
  name                    = var.name
  memory_size_gb          = var.size
  location_id             = var.location
  project                 = var.project
  redis_version           = var.redis_version
  region                  = var.region
  reserved_ip_range       = var.ip_range
  tier                    = var.tier
}
modules/memorystore/main.tf
variable "display_name" {
  description = "Instance Name"
}

variable "ip_range" {
  description = "IP Range"
}

variable "location" {
  description = "Zone"
}

variable "name" {
  description = "Instance Name"
}

variable "network" {
  description = "Authorized Network"
}

variable "project" {
  description = "Project ID"
}

variable "redis_version" {
  description = "Redis Version"
}

variable "region" {
  description = "Region"
}

variable "size" {
  description = "Memory Size in GB"
}

variable "tier" {
  description = "Service Tier"
}
modules/memorystore/variables.tf

VPC Configuration

module "vpc" {
    source  = "terraform-google-modules/network/google"
    version = "~> 2.3"

    project_id   = var.project
    network_name = var.network
    routing_mode = "GLOBAL"

    subnets = [
        {
            subnet_name              = var.subnetwork
            subnet_ip                = "10.183.0.0/20"
            subnet_region            = var.region
            subnet_private_access    = "true"
            subnet_flow_logs         = "true"
            description              = var.subnet_description
        }
    ]

    secondary_ranges = {
        (var.subnetwork) = [
            {
                range_name    = join("-", [var.subnetwork, "pods"])
                ip_cidr_range = "10.184.0.0/14"
            },
            {
                range_name    = join("-", [var.subnetwork, "services"])
                ip_cidr_range = "10.188.0.0/20"
            },
        ]
    }

}
modules/vpc/main.tf
variable "auto_create_subnetworks" {
  type        = bool
  description = "When set to true, the network is created in 'auto subnet mode' and it will create a subnet for each region automatically across the 10.128.0.0/9 address range. When set to false, the network is created in 'custom subnet mode' so the user can explicitly connect subnetwork resources."
  default     = false
}

variable "description" {
  type        = string
  description = "An optional description of this resource. The resource must be recreated to modify this field."
  default     = "Toptal VPC network"
}

variable "network" {
  description = "The name of the network being created"
}

variable "project" {
  description = "Toptal Project"
}

variable "region" {
  description = "Region of resources"
}

variable "routing_mode" {
  type        = string
  default     = "GLOBAL"
  description = "The network routing mode (default 'GLOBAL')"
}

variable "subnet_description" {
  type        = string
  description = "An optional description of this resource. The resource must be recreated to modify this field."
  default     = "Toptal VPC subnetwork"
}

variable "shared_vpc_host" {
  type        = bool
  description = "Makes this project a Shared VPC host if 'true' (default 'false')"
  default     = false
}

variable "subnetwork" {
  description = "The name of the subnetwork being created"
}
modules/vpc/variables.tf