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.