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
andmail_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.