Thorn Tech Marketing Ad
Skip to main content
Version: Next

HA Terraform Template for Google Cloud

TLDR

Purpose: Deploy StorageLink BYOL in a highly available configuration on Google Cloud using Terraform

Architecture: Regional managed instance group (2 VMs) behind an external passthrough network load balancer + single-zone Cloud SQL for PostgreSQL 16 on a private IP + firewall rules

Deployment:

  1. Subscribe to StorageLink BYOL on Google Cloud Marketplace, then set up private services access on the default network
  2. Create storagelink-ha-terraform.tf and terraform.tfvars (project, region, bucket name, admin credentials, your IP)
  3. Run terraform init && terraform apply (Cloud Shell recommended)

Key Variables:

  • ssh_admin_ip_address: your IP + /32 for SSH access
  • google_storage_bucket: an existing Cloud Storage bucket for StorageLink files
  • web_admin_username / web_admin_password: web admin credentials (password of 12 or more characters)

After Deployment: Sign in at https://<ip_address> and start the free 30-day trial from the admin UI — a BYOL deployment starts unlicensed

Product: StorageLink by Thorn Technologies — cloud storage gateway for secure file sharing

Overview​

You can deploy StorageLink in an HA configuration using Terraform.

This article covers deploying an HA deployment of StorageLink BYOL version 1.3.0 on Google Cloud. The Terraform template is provided as an example, so feel free to further customize it for your business case.

Note: Make sure you are subscribed to StorageLink BYOL on the Google Cloud Marketplace (click Get Started and accept the terms in the project you deploy to) before deploying the Terraform template, or else you will run into errors. There is no software charge on the BYOL listing — you pay only for the Google Cloud resources.

Using the pay-as-you-go Standard listing instead

To deploy the Standard listing, subscribe to StorageLink on the Google Cloud Marketplace and set image_path to https://www.googleapis.com/compute/v1/projects/thorn-technologies-public/global/images/storagelink-1-3-0-1787784680. A Standard instance licenses itself automatically but is limited to 10 StorageLink users (administrators are not counted), and its license check needs the attached service account the template already creates. See StorageLink Standard vs BYOL.

Prerequisites​

The database uses a private IP address on the default network, which requires private services access on that network. Projects that have never used Cloud SQL private IP, Memorystore, or similar services do not have it, and terraform apply then fails while creating the Cloud SQL instance. From Cloud Shell, in the project you deploy to:

gcloud services enable compute.googleapis.com sqladmin.googleapis.com servicenetworking.googleapis.com iam.googleapis.com

# Check whether private services access is already set up on the default network:
gcloud services vpc-peerings list --network=default

# If the command above lists nothing, set it up:
gcloud compute addresses create google-managed-services-default \
--global --purpose=VPC_PEERING --prefix-length=16 --network=default
gcloud services vpc-peerings connect --service=servicenetworking.googleapis.com \
--ranges=google-managed-services-default --network=default

The template also expects an existing Cloud Storage bucket for StorageLink files. To create one:

gcloud storage buckets create gs://<your-bucket-name> --location=<your-region>

Resources Created​

ResourceDescription
Regional managed instance groupTwo StorageLink VMs, spread across zones in the region, created from an instance template
Load balancerExternal passthrough network load balancer: forwarding rules for TCP 22, 80, and 443, backend services, and an HTTPS health check on /backend/actuator/health, so a VM whose StorageLink application stops is taken out of rotation
Public IPStatic IP address of the load balancer
FirewallAllows TCP 80/443 from anywhere and from Google's health-check ranges; restricts SSH (22) to ssh_admin_ip_address
Cloud SQL (PostgreSQL)Managed database for StorageLink users and configuration (single-zone by default; change availability_type to REGIONAL for HA)
Service AccountGrants VM instances access to Cloud SQL and Cloud Storage
Google Storage BucketYour existing bucket, used as StorageLink's default storage. It is not created or deleted by the template

Running the Template​

We recommend that you use the Cloud Shell within the Google Cloud Platform console. The Cloud Shell supports the Terraform CLI. And it also inherits your Google Cloud permissions from the web console.

This article contains two files:

  • storagelink-ha-terraform.tf
  • terraform.tfvars

Create these two files, using the file contents at the bottom of this page. Make adjustments to the terraform.tfvars file. Then, run the following commands:

terraform init
terraform plan

When you are ready to deploy the template, run:

terraform apply

terraform apply typically takes about 5 minutes, with the Cloud SQL instance taking the longest. The web interface is usually ready a minute or two after it finishes, once both VMs complete their first boot.

When deployment completes, Terraform outputs the static IP address of the Load Balancer (ip_address). Use it to access the StorageLink web admin interface:

https://<ip_address>

Sign in with the web_admin_username / web_admin_password you set in terraform.tfvars. Run terraform output to see all deployment values.

Start your free trial​

A deployment from the StorageLink BYOL listing starts unlicensed: administrators can sign in and configure everything, but StorageLink users can't be created until a trial or license is active. After you sign in, use the Start a 30-day free trial card at the top of the admin UI — enter your work email, accept the End User License Agreement, and enter the 6-digit code from the email. See First Launch Experience.

Both VMs share the license through the Cloud SQL database. The VM that handles the activation is licensed at once, and the other VM picks the license up within about a minute. A VM that the instance group recreates later starts licensed.

When you are ready to license the deployment, book a 15-minute call with our Sales team. See Purchasing a License for the activation steps.

Deleting the Stack​

To delete your Terraform stack, run the command:

terraform destroy

Terraform deletes the compute and network resources, then stops with an error on the Cloud SQL user (role "…-iam@….iam" cannot be dropped because some objects depend on it): StorageLink's own database belongs to that user. Terraform also keeps deletion protection on the Cloud SQL instance. Delete the Cloud SQL instance manually, from the Google Cloud console or with the command below, then run terraform destroy again to finish:

gcloud sql instances delete <stack_name>-db-instance
terraform destroy

Your Cloud Storage bucket is not part of the stack, so Terraform leaves it and its files in place. Delete it separately if you no longer need it.

Variable Reference​

VariableDefaultDescription
stack_name—Lowercase/hyphen prefix for resource names
project—GCP project ID to deploy into
regionus-central1GCP region
zoneus-central1-cGCP zone within the region
web_admin_username—Web admin username
web_admin_password—Web admin password (minimum 12 characters)
ssh_admin_ip_address—Workstation public IP with /32 suffix for SSH access
google_storage_bucket—Name of an existing GCS bucket for StorageLink files
credentials—Path to a JSON service-account key (optional)
machine_typee2-mediumVM size
image_pathStorageLink BYOL 1.3.0 imageMarketplace image self-link

Terraform file contents​

terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "5.2.0"
}
}
}

provider "google" {
credentials = var.credentials == null ? null : file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}

variable "credentials" {
type = string
description = "Name of your Service Key"
default = null
}

variable "project" {
type = string
description = "Name of your Google Cloud project"
}

variable "stack_name" {
type = string
description = "Name of this Terraform stack, which acts as a prefix for resources"
}

variable "region" {
type = string
description = "Name of your Google Cloud region"
default = "us-central1"
}

variable "zone" {
type = string
description = "Name of your region zone"
default = "us-central1-c"
}

variable "google_storage_bucket" {
type = string
description = "Name of your existing Google Storage bucket"
}

variable "image_path" {
type = string
description = "Path to the VM image. Defaults to the StorageLink BYOL 1.3.0 image."
default = "https://www.googleapis.com/compute/v1/projects/thorn-technologies-public/global/images/storagelink-byol-1-3-0-1787784674"
}

variable "web_admin_username" {
type = string
description = "This is the web admin username"
}

variable "web_admin_password" {
type = string
description = "This is the web admin password"
sensitive = true
}

variable "ssh_admin_ip_address" {
type = string
description = "IP address range of your workstation. Provides sysadmin access for SSH only."
}

variable "machine_type" {
type = string
description = "Machine type of your VM"
default = "e2-medium"
}

locals {
truncated_email = trimsuffix(google_service_account.service_account_solution.email, ".gserviceaccount.com")
bucket_name = var.google_storage_bucket
}

resource "random_id" "new" {
byte_length = "8"
}

resource "random_password" "db_password" {
length = 20
special = true
override_special = "*+?&^!@"
}

# Bucket

# Note: This template uses an existing Google Storage bucket
# The bucket must be created separately before running this template

# IAM

resource "google_service_account" "service_account_solution" {
account_id = "${var.stack_name}-iam"
display_name = "StorageLink Service Account Terraform"
project = var.project
}

resource "google_project_iam_member" "role_service_account_sql_instance" {
role = "roles/cloudsql.instanceUser"
member = "serviceAccount:${google_service_account.service_account_solution.email}"
project = var.project
}

resource "google_project_iam_member" "role_service_account_sql_client" {
role = "roles/cloudsql.client"
member = "serviceAccount:${google_service_account.service_account_solution.email}"
project = var.project
}

resource "google_project_iam_member" "role_service_account_logging" {
role = "roles/logging.serviceAgent"
member = "serviceAccount:${google_service_account.service_account_solution.email}"
project = var.project
}

resource "google_project_iam_member" "role_service_account_storage" {
role = "roles/storage.admin"
member = "serviceAccount:${google_service_account.service_account_solution.email}"
project = var.project
}

resource "google_project_iam_member" "role_service_account_metric_write" {
role = "roles/monitoring.metricWriter"
member = "serviceAccount:${google_service_account.service_account_solution.email}"
project = var.project
}

resource "google_project_iam_member" "role_service_account_log_write" {
role = "roles/logging.logWriter"
member = "serviceAccount:${google_service_account.service_account_solution.email}"
project = var.project
}

resource "google_project_iam_member" "role_service_account_log_view_accessor" {
role = "roles/logging.viewAccessor"
member = "serviceAccount:${google_service_account.service_account_solution.email}"
project = var.project
}

# DB

resource "google_sql_database" "database" {
name = "${var.stack_name}-storagelink"
instance = google_sql_database_instance.instance.name
charset = "utf8"
depends_on = [
google_sql_user.db-service-user
]
}

resource "google_sql_database_instance" "instance" {
name = "${var.stack_name}-db-instance"
database_version = "POSTGRES_16"
root_password = "${random_password.db_password.result}"
settings {
tier = "db-custom-1-3840" // smallest dedicated-core tier; raise it for production load
edition = "ENTERPRISE"
disk_size = 20
disk_type = "PD_SSD"
availability_type = "ZONAL"
disk_autoresize = true
activation_policy = "ALWAYS"
maintenance_window {
hour = 0
day = 7
}
backup_configuration {
enabled = true
start_time = "00:00"
}
ip_configuration {
ipv4_enabled = false
private_network = "https://www.googleapis.com/compute/v1/projects/${var.project}/global/networks/default"
}
database_flags {
name = "cloudsql.iam_authentication"
value = "on"
}
}
}

resource "google_sql_user" "db-service-user" {
name = "${local.truncated_email}"
instance = google_sql_database_instance.instance.name
type = "CLOUD_IAM_SERVICE_ACCOUNT"
}

# Compute

resource "google_compute_instance_template" "it" {
name = "${var.stack_name}-instance-template"
machine_type = var.machine_type
network_interface {
network = "default"
access_config {}
}
service_account {
email = "${google_service_account.service_account_solution.email}"
scopes = [
"https://www.googleapis.com/auth/cloud.useraccounts.readonly",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring.write",
"https://www.googleapis.com/auth/cloud-platform"
]
}
disk {
auto_delete = true
type = "PERSISTENT"
device_name = "boot"
boot = true
source_image = "${var.image_path}"
disk_size_gb = 32
disk_type = "pd-ssd"
}
can_ip_forward = false
tags = [
"${var.stack_name}-firewall-health-check",
"${var.stack_name}-public-network",
"${var.stack_name}-admin-network"
]
metadata = {
google-logging-enable = 1
google-monitoring-enable = 1
user-data = <<-EOT
#cloud-config
write_files:
- content : |
#!/bin/bash
export CLOUD_PROVIDER=gcp
export ARCHITECTURE=HA
export SECRET_ID="${random_password.db_password.result}"
export DB_HOST=${google_sql_database_instance.instance.connection_name}
path: /opt/swiftgw/launch_config.env
- path: /opt/swiftgw/application.properties
append: true
content: |
features.first-connection.cloud-provider=gcp
features.first-connection.base-prefix=${local.bucket_name}
features.first-connection.use-instance-credentials=true
runcmd:
- 'for i in $(seq 1 60); do curl -s http://localhost:8080/actuator/health | grep -q UP && break; sleep 10; done'
- 'curl -X "POST" "http://localhost:8080/1.0.0/admin/config" -H "accept: */*" -H "Content-Type: application/json" -d "{\"password\": \"${var.web_admin_password}\",\"username\": \"${var.web_admin_username}\"}"'

EOT
}
}

resource "google_compute_region_instance_group_manager" "igm" {
name = "${var.stack_name}-igm"
region = var.region
base_instance_name = "${var.stack_name}"
version {
instance_template = google_compute_instance_template.it.id
}
target_size = 2
named_port {
name = "http"
port = 80
}
named_port {
name = "https"
port = 443
}
named_port {
name = "ssh"
port = 22
}
depends_on = [
google_compute_instance_template.it
]
}

resource "google_compute_region_health_check" "rhc" {
name = "${var.stack_name}-rhc"
// Checks the StorageLink application through nginx, so a VM whose application is down leaves the load balancer
https_health_check {
port = 443
request_path = "/backend/actuator/health"
}
}

resource "google_compute_address" "ip" {
name = "${var.stack_name}-ip"
}

resource "google_compute_forwarding_rule" "fr_80" {
name = "${var.stack_name}-fr-80"
port_range = 80
backend_service = google_compute_region_backend_service.backend_http.id
ip_address = google_compute_address.ip.address
depends_on = [
google_compute_address.ip,
google_compute_region_backend_service.backend_http
]
}

resource "google_compute_forwarding_rule" "fr_443" {
name = "${var.stack_name}-fr-443"
port_range = 443
backend_service = google_compute_region_backend_service.backend_https.id
ip_address = google_compute_address.ip.address
}

resource "google_compute_forwarding_rule" "fr_22" {
name = "${var.stack_name}-fr-22"
port_range = 22
backend_service = google_compute_region_backend_service.backend_ssh.id
ip_address = google_compute_address.ip.address
}

resource "google_compute_region_backend_service" "backend_http" {
name = "${var.stack_name}-backend-http"
health_checks = [
google_compute_region_health_check.rhc.id
]
backend {
group = google_compute_region_instance_group_manager.igm.instance_group
}
port_name = "http"
protocol = "TCP"
load_balancing_scheme = "EXTERNAL"
}

resource "google_compute_region_backend_service" "backend_https" {
name = "${var.stack_name}-backend-https"
health_checks = [
google_compute_region_health_check.rhc.id
]
backend {
group = google_compute_region_instance_group_manager.igm.instance_group
}
port_name = "https"
protocol = "TCP"
load_balancing_scheme = "EXTERNAL"
}

resource "google_compute_region_backend_service" "backend_ssh" {
name = "${var.stack_name}-backend-ssh"
health_checks = [
google_compute_region_health_check.rhc.id
]
backend {
group = google_compute_region_instance_group_manager.igm.instance_group
}
port_name = "ssh"
protocol = "TCP"
load_balancing_scheme = "EXTERNAL"
}

resource "google_compute_firewall" "firewall_health_check" {
name = "${var.stack_name}-firewall-health-check"
network = "default"
target_tags = ["${var.stack_name}-firewall-health-check"]
source_ranges = [
"130.211.0.0/22",
"35.191.0.0/16",
"209.85.152.0/22",
"209.85.204.0/22",
"169.254.169.254/32"
]
allow {
protocol = "tcp"
ports = ["80", "443"]
}
}

resource "google_compute_firewall" "public-network" {
name = "${var.stack_name}-public-network"
network = "default"
target_tags = ["${var.stack_name}-public-network"]
source_ranges = [
"0.0.0.0/0"
]
allow {
protocol = "tcp"
ports = ["80", "443"]
}
}

resource "google_compute_firewall" "admin-network" {
name = "${var.stack_name}-admin-network"
network = "default"
target_tags = ["${var.stack_name}-admin-network"]
source_ranges = [
"${var.ssh_admin_ip_address}",
"35.235.240.0/20" // Google Cloud Portal SSH tool
]
allow {
protocol = "tcp"
ports = ["22"]
}
}

output "ip_address" {
value = "${google_compute_address.ip.address}"
}

Example terraform.tfvars​

Make sure you replace these values with your own.

stack_name = "your-terraform-stack"
web_admin_username = "admin"
web_admin_password = "replace this with a strong password" // 12 or more characters
google_storage_bucket = "your-bucket-terraform"
ssh_admin_ip_address = "1.2.3.4/32" // replace this with your IP address, followed by /32
project = "your-google-cloud-project"
region = "us-central1"
zone = "us-central1-c"
machine_type = "e2-medium"

image_path = "https://www.googleapis.com/compute/v1/projects/thorn-technologies-public/global/images/storagelink-byol-1-3-0-1787784674" // StorageLink BYOL 1.3.0 (the default; optional)