A code-free IaaC link shortener using Kutt and GKE

Goal: Deploy a link shortener on your own domain without writing any (non-infrastructure) code.

Prerequisites: An operational Kubernetes cluster, knowledge of Kubernetes & Terraform, basic knowledge of Google Cloud Platform

At every company I’ve worked at in the past decade we’ve had some mechanism to create memorable links to commonly used documents.  Internally at Google, at least when I was there around 2010, they used the internally resolving name “go”, e.g. “go/payroll” or “go/chromedashboard” would point to internal payroll or internal project dashboards.  I suspect an ex-Googler liked the idea enough to make it a business, as GoLinks is a real thing you can pay for.  Below I’ll walk through how to setup Kutt (an open source link shortener) with Terraform in your own Kubernetes cluster in Google Cloud.

Kutt has several dependencies, so let’s make sure we’ve got those in orderg

  • You need a domain name and ability to set DNS records.  For example go.mycompany.com

  • You’ll need an SMTP Server for authenticating emails to the link shortener, we’ll use this just for the admin user.  Have your mail_host, mail_port, mail_user and mail_password at hand.

  • Optionally: A google analytics ID

  • A Redis Instance (we’ll deploy one with terraform)

  • A Postgresql database (we’ll deploy one with terraform)

For starters, let's setup our variables.tf file. There’s quite a few values here, and there are more configuration options that can be passed into Kutt via env vars down the road.

variables.tf

variable "k8s_host" {
  description = “IP of your K8S API Server”
}
 
variable "cluster_ca_certificate" {
  description = “K8S cluster certificate”
}
 
variable "region" {
  description = "Region of resources"
}
 
variable "project_id" {
  description = “google cloud project ID”
}
 
variable "google_service_account" {
  description = “JSON Service account to talk to GCP”
}
 
variable "namespace" {
  description = “kubernetes namespace to deploy to”
}
 
variable "vpc_id" {
  description = “VPC to put the database in”
}
 
variable "domain" {
  default = "go.mycompany.com"
}
 
variable “jwt_secret” {
  default = “CHANGE-ME-TO-SOMETHING-UNIQUE”
}
 
variable “smtp_host” {}
 
variable “smtp_port” {
  default = 587
}
 
variable “smtp_user” {}
 
variable “smtp_password” {}
 
variable “admin_emails” {}
 
variable “mail_from” {
  default = “lnkshortner@mycompany.com” 
}
 
variable “google_analytics_id” {}

Now let’s set up our database.  You can really do this anyway you like, but if we’re using Google Kubernetes Engine we likely also have access to Google Cloud SQL, so this is fairly straightforward.

database.tf

resource "google_sql_database_instance" "linkshortenerdb" {
  name             = replace("linkshortener-${var.namespace}", "_", "-")
  database_version = "POSTGRES_13"
  region           = "us-west2"
  project          = var.project_id
  lifecycle {
    prevent_destroy = true
  }
 
  settings {
    # $7 per month
    tier = "db-f1-micro"
    backup_configuration {
      enabled                        = true
      location                       = "us"
      point_in_time_recovery_enabled = false
      backup_retention_settings {
        retained_backups = 30
      }
    }
 
    ip_configuration {
      ipv4_enabled = false
      # In order for private networks to work the GCP Service Network API has to be enabled
      private_network = var.vpc_id
      require_ssl     = false
    }
  }
}
 
resource "google_sql_database" "linkshortener" {
  name     = "${var.namespace}-linkshortener"
  instance = google_sql_database_instance.linkshortenerdb.name
  project  = var.project_id
}
 
resource "random_password" "psql_password" {
  length  = 16
  special = true
}
 
resource "google_sql_user" "linkshorteneruser" {
  project  = var.project_id
  name     = "linkshortener"
  instance = google_sql_database_instance.linkshortenerdb.name
  password = random_password.psql_password.result
}

That's it (finally) for prerequisites, now the fun part, setting up Kutt itself (with a Redis sidecar)

kutt.tf

provider "google" {
 project     = var.project_id
 region      = var.region
 credentials = var.google_service_account
}
 
provider "google-beta" {
 project     = var.project_id
 region      = var.region
 credentials = var.google_service_account
}
 
data "google_client_config" "default" {}
 
provider "kubernetes" {
 host                   = "https://${var.k8s_host}"
 cluster_ca_certificate = var.cluster_ca_certificate
 token                  = data.google_client_config.default.access_token
}
 
resource "random_password" "redis_authstring" {
 length  = 16
 special = false
}
 
resource "kubernetes_deployment" "linkshortener" {
 metadata {
   name = "linkshortener"
   labels = {
     app = "linkshortener"
   }
   namespace = var.namespace
 }
 
 wait_for_rollout = false
 
 spec {
   replicas = 1
   selector {
     match_labels = {
       app = "linkshortener"
     }
   }
 
   template {
     metadata {
       labels = {
         app = "linkshortener"
       }
     }
 
     spec {
       container {
         image = "bitnami/redis:latest"
         name  = "redis"
 
         port {
           container_port = 3000
         }
 
         env {
           name  = "REDIS_PASSWORD"
           value = random_password.redis_authstring.result
         }
 
         env {
           name  = "REDIS_PORT_NUMBER"
           value = 3000
         }
       }
 
       container {
         image = "kutt/kutt"
         name  = "linkshortener"
         port {
           container_port = 80
         }
 
         env {
           name  = "PORT"
           value = "80"
         }
 
         env {
           name  = "DEFAULT_DOMAIN"
           value = var.domain
         }
 
         env {
           name  = "DB_HOST"
           value = google_sql_database_instance.linkshortenerdb.ip_address.0.ip_address
         }
 
         env {
           name  = "DB_PORT"
           value = "5432"
         }
 
         env {
           name  = "DB_USER"
           value = google_sql_user.linkshorteneruser.name
         }
 
         env {
           name  = "DB_PASSWORD"
           value = google_sql_user.linkshorteneruser.password
         }
 
         env {
           name  = "DB_NAME"
           value = google_sql_database.linkshortener.name
         }
 
         env {
           name  = "DB_SSL"
           value = "false"
         }
 
         env {
           name  = "REDIS_HOST"
           value = "localhost"
         }
 
         env {
           name  = "REDIS_PORT"
           value = "3000"
         }
 
         env {
           name  = "REDIS_PASSWORD"
           value = random_password.redis_authstring.result
         }
 
         env {
           name  = "JWT_SECRET"
           value = var.jwt_secret
         }
 
         env {
           name  = "ADMIN_EMAILS"
           value = var.admin_emails
         }
 
         env {
           name  = "SITE_NAME"
           value = "MyCompany Links"
         }
 
         env {
           name  = "MAIL_HOST"
           value = var.smtp_host
         }
 
         env {
           name  = "MAIL_PORT"
           value = var.smtp_port
         }
 
         env {
           name  = "MAIL_USER"
           value = var.smtp_user
         }
 
         env {
           name  = "MAIL_FROM"
           value = var.mail_from
         }
 
         env {
           name  = "DISALLOW_REGISTRATION"
           value = "true"
         }
 
         env {
           name  = "DISALLOW_ANONYMOUS_LINKS"
           value = "true"
         }
 
         env {
           name  = "GOOGLE_ANALYTICS"
           value = var.google_analytics_id
         }
 
         env {
           name  = "MAIL_PASSWORD"
           value = var.smtp_password
         }
         readiness_probe {
           http_get {
             path   = "/api/v2/health"
             port   = 80
             scheme = "HTTP"
           }
           timeout_seconds = 5
           period_seconds  = 10
         }
         resources {
           requests = {
             cpu    = "100m"
             memory = "200M"
           }
         }
       }
     }
   }
 }
}

With the above you should now have a postgres server, a redis instance and a kutt deployment deployed and talking to eachother. All that's left is to expose your deployment as a service and setup your DNS records.

Previous
Previous

Production SQL Server Checklist & Best Practices

Next
Next

Kaizen in infrastructure: Writing RCAs to improve system reliability and build customer trust