Skip to content

Instantly share code, notes, and snippets.

@crescentrose
Created August 24, 2025 20:17
Show Gist options
  • Select an option

  • Save crescentrose/5d82d2a2732ef257aeb55393fb24e08f to your computer and use it in GitHub Desktop.

Select an option

Save crescentrose/5d82d2a2732ef257aeb55393fb24e08f to your computer and use it in GitHub Desktop.
k3s deployment with Terraform on DigitalOcean
#!/usr/bin/env sh
# Spits out a `kubeconfig` that lets you connect to the freshly build k3s cluster.
# Do sometihng like `./fetch_kubeconfig.sh > ~/.kube/config` if you have no other configs.
host=$(cd core && terraform output -raw cluster_ip)
keyfile=$(mktemp /tmp/authkey.XXXXXX)
# make sure we clean up if we fail
trap "rm $keyfile" 1 2 3 6 15
(cd core && terraform output -raw ssh_private_key > "$keyfile")
printf "\n" >> "$keyfile"
ssh "root@$host" -o StrictHostKeyChecking=accept-new -o IdentitiesOnly=yes -i "$keyfile" -t "cat /etc/rancher/k3s/k3s.yaml" | sed -e "s/127.0.0.1/$host/"
rm "$keyfile"
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}
# You can use a fancy secrets management solution like HashiCorp Vault
# If you're a solo dev, you can also just have a `secrets.yaml` that you encrypt
# with something like Agebox and just keep in the repo. If it's good enough for
# Rails, it's good enough for me.
data "local_sensitive_file" "secrets" {
filename = "../secrets.yaml"
}
locals {
secrets = yamldecode(data.local_sensitive_file.secrets.content)
# Change this to be closer to you
region = "ams3"
# You might want to use CoreOS or something less intensive.
# This is just for demonstration purposes. It's easy to swap out anyway.
image = "ubuntu-24-04-x64"
# Resize your droplet if you have the cash
# I do not recommend going lower than this, though.
size = "s-1vcpu-2gb"
# Rename your droplet if you feel creative
name = "k3s"
}
provider "digitalocean" {
token = local.secrets["digitalocean_token"]
}
# You'll want to generate a SSH key for your VM.
resource "digitalocean_ssh_key" "terraform" {
name = "terraform"
public_key = local.secrets["ssh_public_key"]
}
resource "digitalocean_reserved_ip" "reserved_ip"{
region = local.region
}
resource "digitalocean_droplet" "k3s" {
depends_on = [digitalocean_reserved_ip.reserved_ip]
image = local.image
name = local.name
region = local.region
size = local.size
ssh_keys = [
digitalocean_ssh_key.terraform.id
]
connection {
host = self.ipv4_address
user = "root"
type = "ssh"
timeout = "2m"
private_key = local.secrets["ssh_private_key"]
}
provisioner "remote-exec" {
inline = [
"curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC=\"--node-external-ip ${digitalocean_reserved_ip.reserved_ip.ip_address}\" sh -s -",
"mkdir -p /var/lib/rancher/k3s/agent/images"
]
}
}
resource "digitalocean_reserved_ip_assignment" "reserved_ip_assignment" {
ip_address = digitalocean_reserved_ip.reserved_ip.ip_address
droplet_id = digitalocean_droplet.k3s.id
}
# This is what you will want to point your DNS to
# Using a reserved IP allows you to rebuild the cluster with zero downtime, just by switching
# the machine the reserved IP points to.
# If you mess up, you can also tear down the old cluster and rebuild a new one without waiting
# for DNS to propagate.
output "reserved_ip" {
description = "The IPv4 address of the reserved IP attached to the currently active cluster."
value = digitalocean_reserved_ip.reserved_ip.ip_address
}
# This is used later on for convenience.
output "ssh_private_key" {
description = "The SSH private key used to access the k3s cluster."
sensitive = true
value = local.secrets["ssh_private_key"]
}
ssh_public_key: ssh-ed25519 ...
ssh_private_key: |-
put your private key here
digitalocean_token: dop_v1_...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment