Sitemap

Platform Engineering Starter Kit Series: Repository Scaffolding & Minimal Infra Bootstrap (2)

8 min readAug 20, 2025

In the first post, Introduction and Prerequisites (1), we prepared your workstation and created a baseline repo.

Now we’ll:

  • Scaffold Terraform modules and per-env folders (dev/staging/prod)
  • Add a remote backend for Terraform state (GCS)
  • Enable required GCP APIs in Terraform
  • Create a minimal VPC with secondary ranges for GKE
  • Stand up a GKE cluster with sensible defaults (autoscaling, release channel)
  • Add a Makefile with environment switching, validation, and kubeconfig helpers

0. Starting Point: Get the Post 1 Code

If you completed Post 1:

git checkout -b post-02-repo-scaffolding-infra

If you skipped Post 1:

git clone -b post-01-intro-prerequisites https://github.com/JiminByun0101/platform-starter-kit.git
cd platform-starter-kit
git checkout -b post-02-repo-scaffolding-infra

1. Makefile (with env switching + quality gates)

We’ll make it easy to run Terraform and gcloud commands without long copy-paste strings.

Create Makefile at repo root:

Note: Medium’s code blocks may alter indentation. For accurate formatting, copy the file directly from the GitHub repository instead of this post.

# Default environment (override: make ENV=staging plan)
ENV ?= dev

# Load .env.<ENV> if it exists
ifneq (,$(wildcard .env.$(ENV)))
include .env.$(ENV)
export
endif

TF_DIR=terraform/gcp/envs/$(ENV)

.PHONY: help init fmt validate plan apply destroy kubeconfig apis whoami tfstate

help: ## Show targets
@echo "ENV=$(ENV)"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS=":.*?## "}; {printf "\033[36m%-18s\033[0m %s\n", $$1, $$2}'

whoami: ## Show gcloud identity and project
gcloud config list
gcloud auth list

apis: ## (One-time) Enable required GCP APIs (idempotent)
gcloud services enable container.googleapis.com compute.googleapis.com artifactregistry.googleapis.com \
cloudresourcemanager.googleapis.com iamcredentials.googleapis.com serviceusage.googleapis.com \
cloudbuild.googleapis.com secretmanager.googleapis.com

fmt: ## Terraform fmt
cd $(TF_DIR) && terraform fmt -recursive

fmt-all: ## Terraform fmt all envs and modules
terraform -chdir=terraform/gcp fmt -recursive

validate: ## Terraform validate
cd $(TF_DIR) && terraform init -backend=false && terraform validate

init: ## Terraform init (uses backend)
cd $(TF_DIR) && terraform init

reinit: ## Reinitialize backend after changing bucket/prefix
cd $(TF_DIR) && terraform init -reconfigure

plan: validate ## Terraform plan with tfvars
cd $(TF_DIR) && terraform plan -var-file=$(ENV).tfvars

apply: ## Terraform apply with tfvars
cd $(TF_DIR) && terraform apply -auto-approve -var-file=$(ENV).tfvars

destroy: ## Terraform destroy (careful!)
cd $(TF_DIR) && terraform destroy -auto-approve -var-file=$(ENV).tfvars

kubeconfig: ## Fetch kubeconfig for the current env cluster
gcloud container clusters get-credentials $(CLUSTER_NAME) --region $(REGION) --project $(PROJECT_ID)

Tip: run make help to see targets. Switch envs with ENV=staging make plan.

Learn More about Terraform CLI: https://developer.hashicorp.com/terraform/cli/commands

2. Remote State Backend (GCS)

Terraform needs to remember what it has built so it can update or destroy it later.
It stores this information in a state file (terraform.tfstate).
By default, that file lives on your computer — but that’s risky because:

  • Only you can see it (teammates can’t work from the same state)
  • If your laptop dies or the file is deleted, the state is lost
  • If two people run Terraform at the same time, the file can get corrupted

Solution: use a remote backend in Google Cloud Storage (GCS) with locking and versioning.

2.1 Create the bucket (one-time):

Bucket names are globally unique. Add a small personal tag so readers don’t collide with your example.

# vars from your .env.dev (or any env)
PROJECT_ID=platform-starter-kit
REGION=northamerica-northeast1

# Name must be globally unique
# Check if bucket already exists before creating
if ! gsutil ls gs://${TF_STATE_BUCKET} > /dev/null 2>&1; then
echo "Creating bucket gs://${TF_STATE_BUCKET}..."
gcloud storage buckets create gs://${TF_STATE_BUCKET} \
--project ${PROJECT_ID} \
--location ${REGION} \
--uniform-bucket-level-access
echo "Bucket created."
else
echo "Bucket gs://${TF_STATE_BUCKET} already exists."
fi

# Optional: make it versioned
gcloud storage buckets update gs://${TF_STATE_BUCKET} --versioning

2.2 Use the bucket in Terraform

We won’t hardcode the state file path — instead, we’ll tell Terraform to use this bucket as its backend in each environment’s main.tf:

(You don’t need to do this now)

Example:

terraform {
backend "gcs" {
bucket = "platform-starter-kit-jimin-tf-state" # your bucket name
prefix = "envs/dev" # subfolder for this env’s state
}
}
  • bucket → The Cloud Storage bucket where the state will be kept
  • prefix → A folder path inside the bucket for this environment’s state files (dev, staging, prod)

3. Enable Required APIs (one-time per project)

We can enable required GCP APIs manually or in Terraform.
For now, let’s enable them with make apis:

3.1 Verify you’re on the right account & project (console output shown)

# Show the active account and project
gcloud auth list
gcloud config list --format='value(core.account,core.project)'

Expected output example:

* jimin@real-smile.com
jimin@real-smile.com platform-starter-kit

Install GNU Make if you don’t have it:

# Ubuntu / WSL2
sudo apt-get update && sudo apt-get install -y make

# macOS (Homebrew)
brew install make

Then run:

make apis

This runs:

  • Kubernetes Engine API
  • Compute Engine API
  • Artifact Registry API
  • Cloud Resource Manager API
  • IAM Service Account Credentials API
  • Service Usage API
  • Cloud Build API
  • KMS API
  • Secret Manager API

Important: If you skip this, Terraform will fail when creating GKE or other resources.

4. Terraform Code Structure: Common Modules + Environment Configs

Terraform code is split into two layers:

  • Modules (modules/vpc, modules/gke) — reusable building blocks
  • Environment configs (envs/dev, envs/staging, envs/prod) — specific settings per env

4.1 Repository Layout

platform-starter-kit/
├── Makefile
├── .env.sample
├── terraform/
│ └── gcp/
│ ├── backend/ # one-time remote state (GCS bucket)
│ │ └── main.tf
│ ├── modules/
│ │ ├── vpc/
│ │ │ ├── main.tf
│ │ │ ├── variables.tf
│ │ │ └── outputs.tf
│ │ └── gke/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── envs/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── dev.tfvars
│ ├── staging/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── staging.tfvars
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ └── prod.tfvars
└── docs/

4.2 VPC Module

Creates a custom VPC, subnet, and secondary ranges for GKE pods & services.

terraform/gcp/modules/vpc/variables.tf

variable "project_id"  { type = string }
variable "region" { type = string }
variable "network_name" { default = "psk-vpc" }
variable "subnet_name" { default = "psk-subnet" }
variable "subnet_cidr" { default = "10.10.0.0/24" }
variable "pods_cidr" { default = "10.20.0.0/16" }
variable "services_cidr"{ default = "10.30.0.0/20" }

terraform/gcp/modules/vpc/main.tf

resource "google_compute_network" "vpc" {
project = var.project_id
name = var.network_name
auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "subnet" {
project = var.project_id
name = var.subnet_name
ip_cidr_range = var.subnet_cidr
region = var.region
network = google_compute_network.vpc.id
private_ip_google_access = true

secondary_ip_range {
range_name = "pods"
ip_cidr_range = var.pods_cidr
}
secondary_ip_range {
range_name = "services"
ip_cidr_range = var.services_cidr
}
}

terraform/gcp/modules/vpc/outputs.tf

output "network" {
description = "The name of the VPC network"
value = google_compute_network.vpc.name
}

output "subnet" {
description = "The name of the subnet"
value = google_compute_subnetwork.subnet.name
}

4.3 GKE Module

Creates a GKE cluster & default node pool with autoscaling.

terraform/gcp/modules/gke/variables.tf

variable "project_id"   { type = string }
variable "region" { type = string }
variable "cluster_name" { type = string }
variable "network" { type = string }
variable "subnet" { type = string }
variable "min_nodes" { default = 1 }
variable "max_nodes" { default = 3 }
variable "machine_type" { default = "e2-standard-2" }
variable "spot_nodes" { default = true }

terraform/gcp/modules/gke/main.tf

resource "google_container_cluster" "this" {
name = var.cluster_name
location = var.region
project = var.project_id
network = var.network
subnetwork = var.subnet
remove_default_node_pool = true
initial_node_count = 1

deletion_protection = false

ip_allocation_policy {
cluster_secondary_range_name = "pods"
services_secondary_range_name = "services"
}

release_channel { channel = "REGULAR" }
enable_shielded_nodes = true
}

resource "google_container_node_pool" "default" {
name = "${var.cluster_name}-pool"
cluster = google_container_cluster.this.name
location = var.region
project = var.project_id

node_config {
machine_type = var.machine_type
spot = var.spot_nodes
oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
metadata = { disable-legacy-endpoints = "true" }
}

autoscaling {
min_node_count = var.min_nodes
max_node_count = var.max_nodes
}

management {
auto_upgrade = true
auto_repair = true
}
}

terraform/gcp/modules/gke/outputs.tf

output "name" {
description = "The name of the GKE cluster"
value = google_container_cluster.this.name
}

output "endpoint" {
description = "The endpoint of the GKE cluster"
value = google_container_cluster.this.endpoint
}

output "node_pool_name" {
description = "The name of the default node pool"
value = google_container_node_pool.default.name
}

4.4 Dev Environment Config

Links the modules with project/env-specific values.

terraform/gcp/envs/dev/variables.tf

variable "project_id"   { type = string }
variable "region" { type = string }
variable "cluster_name" { type = string }

terraform/gcp/envs/dev/dev.tfvars

project_id   = "platform-starter-kit"
region = "northamerica-northeast1"
cluster_name = "psk-dev-gke"

terraform/gcp/envs/dev/main.tf

terraform {
backend "gcs" {
bucket = "platform-starter-kit-jimin-tf-state" # your bucket name
prefix = "envs/dev"
}
}

provider "google" {
project = var.project_id
region = var.region
}

module "vpc" {
source = "../../modules/vpc"
project_id = var.project_id
region = var.region
}

module "gke" {
source = "../../modules/gke"
project_id = var.project_id
region = var.region
cluster_name = var.cluster_name
network = module.vpc.network
subnet = module.vpc.subnet
}

5. Bootstrap the Infra

# Enable required APIs
make apis

# Init Terraform for dev env
make init

# Preview changes
make plan

# Apply changes
make apply

# Get kubeconfig
make kubeconfig

# Verify cluster access
kubectl get nodes

5.1 Verification in GCP Console

Go to:

  • VPC Network → VPC networks → you should see psk-vpc
Press enter or click to view image in full size
  • Kubernetes Engine → Clusters → you should see your GKE cluster psk-dev-gke
Press enter or click to view image in full size

5.2 Fetch kubeconfig

make kubeconfig
kubectl get nodes

If you see the gke-gcloud-auth-plugin warning, install the plugin based on how you installed the Cloud SDK:

# Install gke-gcloud-auth-plugin if needed
# For Ubuntu/Debian (apt repo)
sudo apt-get update && sudo apt-get install -y google-cloud-sdk-gke-gcloud-auth-plugin

# For macOS (Homebrew)
brew upgrade google-cloud-sdk

# Enable plugin in shell
export USE_GKE_GCLOUD_AUTH_PLUGIN=True

Re-run:

make kubeconfig
kubectl cluster-info
kubectl get nodes

5.3 Cost Warning

GKE clusters incur costs even if idle.
If you’re not going straight into Post 3 within the hour, you should delete the resources to avoid surprise bills:

make destroy

This will remove the GKE cluster, VPC, and any associated resources created in this post. You can always recreate them later by re-running make apply.

6. Commit & Push

git add .
git commit -m "Post 02: Repo scaffolding + minimal VPC & GKE bootstrap"
git push -u origin post-02-repo-scaffolding-infra

Post 2 branch on GitHub

Next in the Series

In Post 3 we’ll:

  • Install Argo CD in GKE
  • Set up GitOps for a sample FastAPI + React app
  • Prepare Helm charts for platform components

--

--

Jimin
Jimin

Written by Jimin

DevOps engineer and tech enthusiast. Sharing tech insights to simplify the complex. Let's connect on LinkedIn! https://www.linkedin.com/in/byun-jimin/

No responses yet