- Gavin In The Cloud
- Posts
- Creating a Secure Bastion Host in Google Cloud with Terraform and GitHub Actions
Creating a Secure Bastion Host in Google Cloud with Terraform and GitHub Actions
Automating Access Control and Infrastructure Deployment for Private Instances
Creating a Secure Bastion Host in Google Cloud with Terraform and GitHub Actions
Introduction: In cloud environments, ensuring secure access to private instances while maintaining control over network traffic is crucial. One commonly used approach is to set up a bastion host, also known as a jump host or jump server. In this blog post, we will explore how to create a bastion host instance in Google Cloud Platform (GCP) using Terraform and establish secure access to private instances. Additionally, we will leverage GitHub Actions for automating the infrastructure deployment process.

Prerequisites:
Before getting started, make sure you have the following prerequisites in place:
A GCP account with the necessary permissions to create instances and networking resources.
A GitHub account and a repository set up to manage your Terraform code.
Repository Structure:
To keep our project organized, we will follow a similar structure within our GitHub repository: GitHub Repo

You can simply clone my public repository: GitHub Repo
Now, let's dive into the details of each component of our Terraform Configuration.
main.tf: The
main.tf
file contains the Terraform configuration for creating a VPC network, subnets, firewall rules, and VM instances for the bastion host and private server. It defines the necessary resources using the Google Cloud Platform provider.
Let's break down the code to understand each component:
VPC Network: The google_compute_network
resource creates a VPC network named "bastion-vpc" with auto-created subnetworks disabled.
Subnets: Two subnets, "subnet-a" and "subnet-b," are created using the google_compute_subnetwork
resource. Each subnet has an IP CIDR range and is associated with the previously defined VPC network.
Firewall Rules: Two firewall rules, allow_bastion_host
and allow_bastion_host_to_private_server
, are defined using the google_compute_firewall
resource. These rules allow incoming TCP traffic on port 22 (SSH) from any source IP to the bastion host and from the bastion host to the private server, respectively. The rules are associated with the VPC network using the network
attribute and are controlled by tags.
VM Instances: Two VM instances, web_server
and bastion_host
, are created using the google_compute_instance
resource. The web_server
instance represents the private server and is tagged with "private-server". The bastion_host
instance represents the bastion host and is tagged with "bastion-host". Both instances have a machine type, zone, and boot disk configuration. The web_server
instance is associated with subnet-a
, while the bastion_host
instance is associated with subnet-b
and has an ephemeral public IP assigned.
# Create the VPC network
resource "google_compute_network" "bastion_vpc" {
name = "bastion-vpc"
auto_create_subnetworks = false
}
# Create the subnets
resource "google_compute_subnetwork" "subnet_a" {
name = "subnet-a"
ip_cidr_range = "10.0.1.0/24"
network = google_compute_network.bastion_vpc.self_link
region = var.region
}
resource "google_compute_subnetwork" "subnet_b" {
name = "subnet-b"
ip_cidr_range = "10.0.2.0/24"
network = google_compute_network.bastion_vpc.self_link
region = var.region.
}
# Create firewall rules
resource "google_compute_firewall" "allow_bastion_host" {
name = "allow-bastion-host"
network = google_compute_network.bastion_vpc.self_link
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["bastion-host"]
}
resource "google_compute_firewall" "allow_bastion_host_to_private_server" {
name = "allow-bastionhost-to-privateserver"
network = google_compute_network.bastion_vpc.self_link
allow {
protocol = "tcp"
ports = ["22"]
}
source_tags = ["bastion-host"]
target_tags = ["private-server"]
}
# Create the VM instances
resource "google_compute_instance" "web_server" {
name = "web-server"
machine_type = var.machine_type
zone = var.zone
tags = ["private-server"]
boot_disk {
initialize_params {
image = var.image
}
}
network_interface {
network = google_compute_network.bastion_vpc.self_link
subnetwork = google_compute_subnetwork.subnet_a.self_link
# No nat_ip specified to prevent the creation of an external IP
}
}
resource "google_compute_instance" "bastion_host" {
name = "bastion-host"
machine_type = var.machine_type
zone = var.zone
tags = ["bastion-host"]
boot_disk {
initialize_params {
image = var.image
}
}
network_interface {
network = google_compute_network.bastion_vpc.self_link
subnetwork = google_compute_subnetwork.subnet_b.self_link
access_config {
// Ephemeral public IP
}
}
}
variables.tf:
The variables.tf
file defines the variables used within the Terraform configuration, such as project ID, region, instance types, and networking details.
variable "region" {
description = "The region where the resources will be created."
default = "us-central1"
}
variable "zone" {
description = "The zone where the resources will be created."
default = "us-central1-c"
}
variable "machine_type" {
description = "The machine type for the instances."
default = "n1-standard-1"
}
variable "image" {
description = "The image for the instances."
default = "ubuntu-os-cloud/ubuntu-2204-lts"
}
Terraform.tfvars: The
terraform.tfvars
file contains the actual values for these variables.
project_id = "your project id" // Replace with your project_id
region = "us-central1"
zone = "us-central1-c"
machine_type = "n1-standard-1"
image = "ubuntu-os-cloud/ubuntu-2204-lts"
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "4.58.0"
}
}
}
backend "gcs" {
bucket = "your-backend-bucket"
prefix = "terraform-bastion-host"
}
provider "google" {
project = "your-project-id" // Replace with your project_id
}
module "bastion" {
source = "./src"
}
GitLab CI/CD Configuration: The
terraform.yml
file sets up a GitHub Actions workflow for automating the Terraform deployment process. It defines stages, jobs, and associated steps to perform tasks such as initialization, validation, planning, applying, and destroying the infrastructure.
name: "Deploy to Bastion Host"
on:
push:
branches:
- main
jobs:
terraform:
name: "Terraform"
runs-on: ubuntu-latest
env:
GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}
defaults:
run:
working-directory: ./
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.0.1
terraform_wrapper: false
- name: Terraform Init
id: init
run: terraform init
- name: Terraform Format
id: fmt
run: terraform fmt
- name: Terraform Plan
id: plan
run: terraform plan
- name: Terraform Apply
id: apply
run: terraform apply -auto-approve
#- name: Terraform Destroy
# id: destroy
# run: terraform destroy -auto-approve
Implementation Steps:
Now, let's walk through the implementation steps to create the bastion host and establish secure access to private instances using Terraform and GitHub Actions.
1. Set up GitHub Repository:
Create a new repository on GitHub or use an existing one to host your Terraform code. Alternatively, you can clone my GitHub Repo: GitHub Repo
2. Configure GCP Provider:
In the module.tf
file, configure the GCP provider by specifying your GCS backend bucket ID and region.
3. Define terraform.tfvars: In the terraform.tfvars
file, replace the variables with your values, including project_id, region, zone, machine_type, and image.
4. Set Secrets in GitHub: In your GitHub repository, navigate to Settings > Secrets. Add a new secret named "GOOGLE_CREDENTIALS" and paste the contents of your Google Cloud service account key file into the value field. This securely provides the necessary credentials for Terraform to authenticate with GCP.

Note: Remove any white spaces in your token content and then paste it.
5. Run the GitHub Actions Pipeline:
Commit and push your Terraform code to the GitHub repository's main branch. This will trigger the GitHub Actions pipeline. Monitor the pipeline execution in the Actions tab of your repository to ensure it completes successfully.
6. Check Resource Creation in GCP:
After the pipeline is finished, verify the creation of resources in the Google Cloud Platform (GCP) Console. Make sure that the bastion host, private instances, and associated networking resources are provisioned accurately.
When you try to access web-server via SSH you will receive following error:

That is beacause you can access the “web-server” only via SSH from “bastion-host”.
You should be able to access the bastion host instance via SSH and then SSH into your web server (private server) using the command:
ssh [username]@[web_server_internal_ip]

Conclusion: In this blog post, we explored how to create a bastion host instance in GCP using Terraform and establish secure access to private instances. We also leveraged GitHub Actions to automate the infrastructure deployment process. By following the steps outlined above, you can ensure secure access to your private instances while maintaining control over network traffic.
References: GitHub Repo