Overview #
VPCs #
- In GCP, the VPCs are global and do not have a CIDR range.
Subnets #
-
A single public subnet can serve all zones within that region.
-
It is not necessary to create separate public subnets for each zone as it would be in AWS.
-
A single subnet can host VM instances in multiple zones within the same region.
-
This makes it easier to create high availability architectures without needing to create separate subnets for each zone.
This Setup & Flowchart #
This is a configuration example for two public subnets in one region. Technically it is not necessary to deploy two public subnets in the same region, since a single public subnet can serve all zones within that region.
For the a configuration that deploys two public subnets in two different regions, take a look at the next blog post in this series.
Prerequisites #
Google Cloud SDK #
Installation #
The Google Cloud SDK (Software Development Kit) includes the gcloud command-line interface (CLI).
# Install dependencies
sudo apt install curl apt-transport-https ca-certificates gnupg -y
# Add package source
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
# Import the Google Cloud public key
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
# Update package index
sudo apt update
# Install the Google Cloud SDK
sudo apt install google-cloud-sdk -y
# Verify installation / list version
gcloud version
# Shell output:
Google Cloud SDK 506.0.0
alpha 2025.01.10
beta 2025.01.10
bq 2.1.11
bundled-python3-unix 3.11.9
core 2025.01.10
gcloud-crc32c 1.0.0
gsutil 5.33
Login to GCP / Verify Login #
Open the link from the shell in a browser, login to GCP and paste the verification code in the shell.
# Alternative use the following command to login without a browser
gcloud auth login --no-launch-browser
Verify Login:
# List Google Cloud accounts that are currently authenticated
gcloud auth list
Set Default Project #
# Set the default project: Syntax
gcloud config set project <PROJECT_ID>
# Set the default project: Example
gcloud config set project my-example-project-448310
# Shell output:
Updated property [core/project].
Enable Project APIs #
# Enable the necessary APIs, that are used in the project
gcloud services enable compute.googleapis.com
# Shell output:
Operation "operations/acf.p2-249181329052-8b52d534-2fc8-4221-88b8-7f17b4df9d95" finished successfully.
Service Account for Terraform #
# Create a Service Account named "terraform"
gcloud iam service-accounts create terraform \
--description="Terraform service account" \
--display-name="Terraform"
# Shell output:
Created service account [terraform].
# Assign IAM roles: Define project ID
gcloud projects add-iam-policy-binding my-example-project-448310 \
--member="serviceAccount:terraform@my-example-project-448310.iam.gserviceaccount.com" \
--role="roles/editor"
# Shell output:
Updated IAM policy for project [my-example-project-448310].
bindings:
- members:
- serviceAccount:terraform@my-example-project-448310.iam.gserviceaccount.com
role: roles/editor
- members:
- user:juergen@jklug.work
role: roles/owner
etag: BwYsDPrXlRw=
version: 1
# Generate a JSON Key (used by Terraform for authentication): Define project ID
gcloud iam service-accounts keys create ~/terraform-key.json \
--iam-account terraform@my-example-project-448310.iam.gserviceaccount.com
# Shell output:
created key [76d8418c1229bec494f7c0b1a5c1d9e600b4b65e] of type [json] as [/home/ubuntu/terraform-key.json] for [terraform@my-example-project-448310.iam.gserviceaccount.com]
Terraform Installation #
# Install the HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
# Verify the GPG key fingerprint
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
# Add the official HashiCorp repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Install Terraform
sudo apt update && sudo apt-get install terraform
# Verify installation / check version
terraform version
Terraform Network Stack #
File and Folder Structure #
The file and folder structure of the Terraform project looks like this:
gcp-vpc-stack-one-region
├── compute.tf
├── outputs.tf
├── terraform.tf
├── variables.tf
└── vpc.tf
Create SSH Key Pair #
# Create a new SSH key pair for the VM access
ssh-keygen -t rsa -b 4096 -f ~/.ssh/terraform
Terraform Configuration Files #
Project Folder & Terraform Provider #
# Create project folder
TF_PROJECT_NAME=gcp-vpc-stack
mkdir $TF_PROJECT_NAME && cd $TF_PROJECT_NAME
- terraform.tf
# Terraform Provider
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
}
}
# Provider GCP (Fallback)
provider "google" {
credentials = file(var.gcp_credentials_file)
project = var.gcp_project_id
region = var.gcp_region
}
Variables #
- variables.tf
## GCP Configuration
# GCP Credentials File
variable "gcp_credentials_file" {
description = "Path to the GCP credentials JSON file"
type = string
default = "~/terraform-key.json" # Define JSON Key created with SDK
}
# GCP Project ID
variable "gcp_project_id" {
description = "GCP project ID"
type = string
default = "my-example-project-448310"
}
# GCP Region
variable "gcp_region" {
description = "GCP region"
type = string
default = "us-central1"
}
## VPC & Subnets
# VPC Name
variable "vpc_name" {
description = "The name of the VPC"
type = string
default = "example-vpc"
}
# Subnets CIDR Blocks
variable "subnets_cidr_blocks" {
description = "List of CIDR blocks for subnets"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
# Subnets Names
variable "subnets_names" {
description = "List of names for the subnets"
type = list(string)
default = ["subnet-public1", "subnet-public2"]
}
## Compuite Engine
# Zones for Subnets: Define different AZs
variable "gcp_zones" {
description = "List of GCP zones for the subnets"
type = list(string)
default = ["us-central1-a", "us-central1-b"]
}
# Image ID
variable "image_id" {
description = "GCP image to use for the instances"
type = string
default = "projects/debian-cloud/global/images/family/debian-12"
}
# Machine Type
variable "machine_type" {
description = "VM Type"
type = string
default = "f1-micro"
}
## SSH Key
# Define local public SSH Key
variable "ssh_public_key_file" {
description = "Path to the public SSH key file"
type = string
default = "~/.ssh/terraform.pub"
}
VPC & Subnets #
- vpc.tf
# VPC
resource "google_compute_network" "vpc_network" {
name = var.vpc_name
auto_create_subnetworks = false
}
# Subnets Public
resource "google_compute_subnetwork" "subnets_public" {
count = length(var.subnets_cidr_blocks)
name = var.subnets_names[count.index]
ip_cidr_range = var.subnets_cidr_blocks[count.index]
region = var.gcp_region
network = google_compute_network.vpc_network.id
}
Computing & Firewall #
- compute.tf
## Compute
# Static IPs for VMs
resource "google_compute_address" "static_ip" {
count = length(var.gcp_zones)
name = "debian-vm-${count.index + 1}"
}
# Compute Engine Instances
resource "google_compute_instance" "debian_vm" {
count = length(var.gcp_zones)
name = "debian-${count.index + 1}"
machine_type = var.machine_type
zone = element(var.gcp_zones, count.index)
tags = ["ingress","egress"] # Add both ingress and egress tags
# Metadata for SSH Access
metadata = {
ssh-keys = "debian:${file(var.ssh_public_key_file)}"
}
boot_disk {
initialize_params {
image = var.image_id
}
}
network_interface {
subnetwork = google_compute_subnetwork.subnets_public[count.index].id
access_config {
nat_ip = google_compute_address.static_ip[count.index].address
}
}
}
## Firewall
# Firewall: Ingress Rules
resource "google_compute_firewall" "ingress" {
name = "ingress"
network = google_compute_network.vpc_network.name
target_tags = ["ingress"]
allow {
protocol = "tcp"
ports = ["22"] # Allow SSH
}
allow {
protocol = "icmp" # Allow Ping
}
source_ranges = ["0.0.0.0/0"] # Allow traffic from anywhere
}
# Firewall: Egress Rules
resource "google_compute_firewall" "egress" {
name = "egress"
network = google_compute_network.vpc_network.name
target_tags = ["egress"]
allow {
protocol = "all"
}
direction = "EGRESS"
destination_ranges = ["0.0.0.0/0"]
}
Outputs #
- outputs.tf
# Public IPs of the VMs
output "vm_public_ips" {
description = "Public IP addresses of the VMs"
value = [for ip in google_compute_address.static_ip : ip.address]
}
# Private IPs of the VMs
output "vm_private_ips" {
description = "Private IP addresses of the VMs"
value = [for vm in google_compute_instance.debian_vm : vm.network_interface[0].network_ip]
}
Apply Terraform Configuration #
Initialize Terraform Project #
This will download and install the AWS Terraform provider defined in the terraform.tf file with “hashicorp/aws”, as well as setting up the configuration files in the project directory.
# Initialize the Terraform project
terraform init
Validate Configuration Files #
# Validates the syntax and structure of Terraform configuration files
terraform validate
# Shell output:
Success! The configuration is valid.
Plan the Deployment #
# Dry run / preview changes before applying them
terraform plan
Apply the Configuration #
# Create network stack
terraform apply -auto-approve
# Shell output:
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
Outputs:
vm_private_ips = [
"10.0.1.2",
"10.0.2.2",
]
vm_public_ips = [
"34.31.72.75",
"34.68.253.237",
]
Verify Deployment State #
# Lists all resources tracked in the Terraform state file
terraform state list
# Shell output:
google_compute_address.static_ip[0]
google_compute_address.static_ip[1]
google_compute_firewall.egress
google_compute_firewall.ingress
google_compute_instance.debian_vm[0]
google_compute_instance.debian_vm[1]
google_compute_network.vpc_network
google_compute_subnetwork.subnets_public[0]
google_compute_subnetwork.subnets_public[1]
Verify Network Connectivity #
SSH Into Compute Instances #
# SSH into EC2 instance 1
ssh -i ~/.ssh/terraform debian@34.31.72.75
# SSH into EC2 instance 2
ssh -i ~/.ssh/terraform debian@34.68.253.237
Note: If GCP assigns the same public IP to the VMs (in case you destroy and reapply them), it’s neccessary to remove the host keys.
# Remove host keys
ssh-keygen -R 34.31.72.75
ssh-keygen -R 34.68.253.237
Ping VM in other Subnet #
# Ping Instance 2 from instance 1
debian@debian-1:~$ ping debian-2
# Shell output:
PING debian-2.c.my-example-project-448310.internal (10.0.2.2) 56(84) bytes of data.
64 bytes from debian-2.us-central1-b.c.my-example-project-448310.internal (10.0.2.2): icmp_seq=1 ttl=64 time=1.85 ms
64 bytes from debian-2.us-central1-b.c.my-example-project-448310.internal (10.0.2.2): icmp_seq=2 ttl=64 time=0.373 ms
# Ping Instance 2 from instance 1: Use private IP
debian@debian-1:~$ ping 10.0.2.2
# Shell output:
PING 10.0.2.2 (10.0.2.2) 56(84) bytes of data.
64 bytes from 10.0.2.2: icmp_seq=1 ttl=64 time=2.24 ms
64 bytes from 10.0.2.2: icmp_seq=2 ttl=64 time=0.402 m
# Ping Instance 1 from instance 2
debian@debian-2:~$ ping debian-1
# Shell output:
PING debian-1.c.my-example-project-448310.internal (10.0.1.2) 56(84) bytes of data.
64 bytes from debian-1.us-central1-a.c.my-example-project-448310.internal (10.0.1.2): icmp_seq=1 ttl=64 time=2.19 ms
64 bytes from debian-1.us-central1-a.c.my-example-project-448310.internal (10.0.1.2): icmp_seq=2 ttl=64 time=0.399 ms
# Ping Instance 1 from instance 2: Use private IP
debian@debian-2:~$ ping 10.0.1.2
# Shell output:
PING 10.0.1.2 (10.0.1.2) 56(84) bytes of data.
64 bytes from 10.0.1.2: icmp_seq=1 ttl=64 time=0.024 ms
64 bytes from 10.0.1.2: icmp_seq=2 ttl=64 time=0.037 ms
Verify GCP Resources #
List VPCs #
# List VPC networks
gcloud compute networks list
# Shell output:
NAME SUBNET_MODE BGP_ROUTING_MODE IPV4_RANGE GATEWAY_IPV4
default AUTO REGIONAL
example-vpc CUSTOM REGIONAL
List Subnets in VPC #
# List subnets in VPC
gcloud compute networks subnets list --filter="network:example-vpc"
# Shell output:
NAME REGION NETWORK RANGE STACK_TYPE IPV6_ACCESS_TYPE INTERNAL_IPV6_PREFIX EXTERNAL_IPV6_PREFIX
subnet-public1 us-central1 example-vpc 10.0.1.0/24 IPV4_ONLY
subnet-public2 us-central1 example-vpc 10.0.2.0/24 IPV4_ONLY
List Compute Instances #
# List virtual machines
gcloud compute instances list
# Shell output:
NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS
debian-1 us-central1-a f1-micro 10.0.1.2 34.31.72.75 RUNNING
debian-2 us-central1-b f1-micro 10.0.2.2 34.68.253.237 RUNNING
Links #
# GitHub Repository
https://github.com/jueklu/terraform-gcp-vpc-subnet