HA Terraform Template for Google Cloud
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:
- Subscribe to StorageLink BYOL on Google Cloud Marketplace, then set up private services access on the
defaultnetwork - Create
storagelink-ha-terraform.tfandterraform.tfvars(project, region, bucket name, admin credentials, your IP) - Run
terraform init && terraform apply(Cloud Shell recommended)
Key Variables:
ssh_admin_ip_address: your IP +/32for SSH accessgoogle_storage_bucket: an existing Cloud Storage bucket for StorageLink filesweb_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.
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
| Resource | Description |
|---|---|
| Regional managed instance group | Two StorageLink VMs, spread across zones in the region, created from an instance template |
| Load balancer | External 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 IP | Static IP address of the load balancer |
| Firewall | Allows 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 Account | Grants VM instances access to Cloud SQL and Cloud Storage |
| Google Storage Bucket | Your 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.tfterraform.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.
Accessing your StorageLink
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
| Variable | Default | Description |
|---|---|---|
stack_name | — | Lowercase/hyphen prefix for resource names |
project | — | GCP project ID to deploy into |
region | us-central1 | GCP region |
zone | us-central1-c | GCP 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_type | e2-medium | VM size |
image_path | StorageLink BYOL 1.3.0 image | Marketplace image self-link |
Terraform file contents
storagelink-ha-terraform.tf
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)
