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