Build it with Terraform

Build it with Terraform

Provision Google Cloud infrastructure using Hashicorp Terraform. Spin up instances of CloudSQL, Redis, Kubernetes and more.

David Aquino
David Aquino

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

HashicorpDevOpsAutomationGoogle CloudTerraform

David Aquino

Spent years in the military to become a killing machine using only 2 CDJs. Automated all of life's inconveniences, including investments in the financial markets.