From 4b571c469d0b7662f40b0d1b82e23ee9a11b73ab Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Thu, 8 Aug 2024 01:37:18 +0400 Subject: [PATCH] chore: add terraform configs --- .gitignore | 5 +- terraform/.env.example | 2 + terraform/environments/production/main.tf | 16 ++ terraform/environments/production/provider.tf | 18 +++ .../environments/production/variables.tf | 7 + terraform/environments/staging/main.tf | 16 ++ terraform/environments/staging/provider.tf | 18 +++ terraform/environments/staging/variables.tf | 7 + terraform/modules/digitalocean/droplets.tf | 153 ++++++++++++++++++ .../modules/digitalocean/get-join-token.sh | 14 ++ terraform/modules/digitalocean/networking.tf | 75 +++++++++ terraform/modules/digitalocean/outputs.tf | 4 + terraform/modules/digitalocean/provider.tf | 8 + terraform/modules/digitalocean/variables.tf | 44 +++++ 14 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 terraform/.env.example create mode 100644 terraform/environments/production/main.tf create mode 100644 terraform/environments/production/provider.tf create mode 100644 terraform/environments/production/variables.tf create mode 100644 terraform/environments/staging/main.tf create mode 100644 terraform/environments/staging/provider.tf create mode 100644 terraform/environments/staging/variables.tf create mode 100644 terraform/modules/digitalocean/droplets.tf create mode 100755 terraform/modules/digitalocean/get-join-token.sh create mode 100644 terraform/modules/digitalocean/networking.tf create mode 100644 terraform/modules/digitalocean/outputs.tf create mode 100644 terraform/modules/digitalocean/provider.tf create mode 100644 terraform/modules/digitalocean/variables.tf diff --git a/.gitignore b/.gitignore index e660a3611..487d27359 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,7 @@ package-lock.json .vscode # Sentry Config File .sentryclirc -.history \ No newline at end of file +.history +terraform/**/.t* +terraform/**/.env +terraform/**/**/*.tfstate* \ No newline at end of file diff --git a/terraform/.env.example b/terraform/.env.example new file mode 100644 index 000000000..140fa11fb --- /dev/null +++ b/terraform/.env.example @@ -0,0 +1,2 @@ +export TF_VAR_DO_TOKEN= +export TF_VAR_PRIVATE_KEY= \ No newline at end of file diff --git a/terraform/environments/production/main.tf b/terraform/environments/production/main.tf new file mode 100644 index 000000000..b5827b3dc --- /dev/null +++ b/terraform/environments/production/main.tf @@ -0,0 +1,16 @@ +module "droplets" { + source = "../../modules/digitalocean" + + private_key = "${var.PRIVATE_KEY}" + project_name = "hmp" + region = "fra1" + environment = "prd" + base_image = "docker-20-04" + worker_size = "s-2vcpu-2gb-amd" + worker_count = 6 + subnet_range = "10.117.0.0/20" + manager_size = "s-2vcpu-2gb-amd" + manager_count = 2 + + digitalocean_project_name = "Production - Homepage" +} \ No newline at end of file diff --git a/terraform/environments/production/provider.tf b/terraform/environments/production/provider.tf new file mode 100644 index 000000000..289827652 --- /dev/null +++ b/terraform/environments/production/provider.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = "~> 2.0" + } + } + cloud { + organization = "appwrite" + workspaces { + name = "production-homepage" + } + } +} + +provider "digitalocean" { + token = var.DO_TOKEN +} \ No newline at end of file diff --git a/terraform/environments/production/variables.tf b/terraform/environments/production/variables.tf new file mode 100644 index 000000000..e3c5d5f06 --- /dev/null +++ b/terraform/environments/production/variables.tf @@ -0,0 +1,7 @@ +variable "DO_TOKEN" { + description = "DigitalOcean API token" +} +variable "PRIVATE_KEY" { + description = "Contents of your local SSH private key file" + default = "$(cat ~/.ssh/id_rsa)" +} diff --git a/terraform/environments/staging/main.tf b/terraform/environments/staging/main.tf new file mode 100644 index 000000000..42237140e --- /dev/null +++ b/terraform/environments/staging/main.tf @@ -0,0 +1,16 @@ +module "droplets" { + source = "../../modules/digitalocean" + + private_key = "${var.PRIVATE_KEY}" + project_name = "hmp" + region = "fra1" + environment = "stg" + base_image = "docker-20-04" + subnet_range = "10.116.0.0/20" + worker_size = "s-1vcpu-2gb" + worker_count = 4 + manager_size = "s-1vcpu-2gb" + manager_count = 2 + + digitalocean_project_name = "Staging - Homepage" +} \ No newline at end of file diff --git a/terraform/environments/staging/provider.tf b/terraform/environments/staging/provider.tf new file mode 100644 index 000000000..b717ab4d4 --- /dev/null +++ b/terraform/environments/staging/provider.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = "~> 2.0" + } + } + cloud { + organization = "appwrite" + workspaces { + name = "staging-homepage" + } + } +} + +provider "digitalocean" { + token = var.DO_TOKEN +} \ No newline at end of file diff --git a/terraform/environments/staging/variables.tf b/terraform/environments/staging/variables.tf new file mode 100644 index 000000000..e3c5d5f06 --- /dev/null +++ b/terraform/environments/staging/variables.tf @@ -0,0 +1,7 @@ +variable "DO_TOKEN" { + description = "DigitalOcean API token" +} +variable "PRIVATE_KEY" { + description = "Contents of your local SSH private key file" + default = "$(cat ~/.ssh/id_rsa)" +} diff --git a/terraform/modules/digitalocean/droplets.tf b/terraform/modules/digitalocean/droplets.tf new file mode 100644 index 000000000..447df7575 --- /dev/null +++ b/terraform/modules/digitalocean/droplets.tf @@ -0,0 +1,153 @@ +locals { + mount_nfs = "/letsencrypt" + setup_firewall = [ + "ufw allow 2377/tcp", + "ufw allow 7946/tcp", + "ufw allow 7946/udp", + "ufw allow 4789/udp", + "ufw reload", + "systemctl restart docker" + ] + setup_nfs = [ + "ufw allow 2049", + "ufw reload", + "apt install -y nfs-common", + "mkdir -p ${local.mount_nfs}", + "echo '${digitalocean_droplet.nfs.ipv4_address_private}:${local.mount_nfs} ${local.mount_nfs} nfs proto=tcp,port=2049,nfsvers=4,sync,noexec,rw 0 0' >> /etc/fstab", + "mount -a", + ] +} + +resource "digitalocean_project" "homepage" { + name = var.digitalocean_project_name + description = "Appwrite Homepage" + purpose = "Web Application" + environment = "Development" + resources = flatten([ + digitalocean_droplet.leader.urn, + digitalocean_droplet.manager[*].urn, + digitalocean_droplet.worker[*].urn, + digitalocean_droplet.nfs.urn + ]) +} + +# Tags +resource "digitalocean_tag" "worker" { + name = "${var.environment}-worker" +} + +resource "digitalocean_tag" "manager" { + name = "${var.environment}-manager" +} + +resource "digitalocean_droplet" "leader" { + image = var.base_image + name = "${var.project_name}-${var.region}-${var.environment}-leader-0" + region = var.region + size = var.manager_size + tags = [digitalocean_tag.manager.id] + ssh_keys = [ + data.digitalocean_ssh_key.Christy.id + ] + vpc_uuid = digitalocean_vpc.subnet.id + + connection { + host = self.ipv4_address + user = "root" + type = "ssh" + private_key = var.private_key + timeout = "2m" + } + + provisioner "remote-exec" { + inline = concat(local.setup_firewall, local.setup_nfs, [ + "docker swarm init --advertise-addr ${self.ipv4_address_private}" + ]) + } +} + +resource "digitalocean_droplet" "manager" { + count = var.manager_count + image = var.base_image + name = "${var.project_name}-${var.region}-${var.environment}-manager-${count.index}" + region = var.region + size = var.manager_size + tags = [digitalocean_tag.manager.id] + vpc_uuid = digitalocean_vpc.subnet.id + ssh_keys = [ + data.digitalocean_ssh_key.Christy.id + ] + + connection { + host = self.ipv4_address + user = "root" + type = "ssh" + private_key = var.private_key + timeout = "2m" + } + + provisioner "remote-exec" { + inline = concat(local.setup_firewall, local.setup_nfs, [ + "docker swarm join --token ${data.external.swarm_join_token.result.manager} ${digitalocean_droplet.leader.ipv4_address_private}:2377" + ]) + } +} + +resource "digitalocean_droplet" "worker" { + count = var.worker_count + image = var.base_image + name = "${var.project_name}-${var.region}-${var.environment}-worker-${count.index}" + region = var.region + size = var.worker_size + tags = [digitalocean_tag.worker.id] + vpc_uuid = digitalocean_vpc.subnet.id + ssh_keys = [ + data.digitalocean_ssh_key.Christy.id + ] + + connection { + host = self.ipv4_address + user = "root" + type = "ssh" + private_key = var.private_key + timeout = "2m" + } + + provisioner "remote-exec" { + inline = concat(local.setup_firewall, [ + "docker swarm join --token ${data.external.swarm_join_token.result.worker} ${digitalocean_droplet.leader.ipv4_address_private}:2377" + ]) + } +} + +resource "digitalocean_droplet" "nfs" { + image = var.base_image + name = "${var.project_name}-${var.region}-${var.environment}-nfs-0" + region = var.region + size = var.worker_size + vpc_uuid = digitalocean_vpc.subnet.id + ssh_keys = [ + data.digitalocean_ssh_key.Christy.id + ] + + connection { + host = self.ipv4_address + user = "root" + type = "ssh" + private_key = var.private_key + timeout = "2m" + } + + provisioner "remote-exec" { + inline = [ + "ufw allow 2049", + "ufw reload", + "sudo apt update", + "sudo apt install -y nfs-kernel-server", + "mkdir -p ${local.mount_nfs}", + "echo '${local.mount_nfs} ${var.subnet_range}(rw,sync,no_root_squash,no_subtree_check)' >> /etc/exports", + "exportfs -arvf", + "systemctl restart nfs-kernel-server", + ] + } +} \ No newline at end of file diff --git a/terraform/modules/digitalocean/get-join-token.sh b/terraform/modules/digitalocean/get-join-token.sh new file mode 100755 index 000000000..0544289d1 --- /dev/null +++ b/terraform/modules/digitalocean/get-join-token.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# Exit if any of the intermediate steps fail +set -e + +# Extract input variables +eval "$(jq -r '@sh "HOST=\(.host)"')" + +# Get worker join token +WORKER=$(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$HOST docker swarm join-token worker -q) +MANAGER=$(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$HOST docker swarm join-token manager -q) + +# Pass back a JSON object +jq -n --arg worker $WORKER --arg manager $MANAGER '{"worker":$worker,"manager":$manager}' \ No newline at end of file diff --git a/terraform/modules/digitalocean/networking.tf b/terraform/modules/digitalocean/networking.tf new file mode 100644 index 000000000..f1f9f658c --- /dev/null +++ b/terraform/modules/digitalocean/networking.tf @@ -0,0 +1,75 @@ +# VPC +resource "digitalocean_vpc" "subnet" { + name = "${var.environment}-subnet" + region = var.region + ip_range = var.subnet_range +} + +# Firewall Rules +resource "digitalocean_firewall" "web" { + name = "${var.environment}-web" + tags = [digitalocean_tag.worker.id, digitalocean_tag.manager.id] + + # HTTP/HTTPS + inbound_rule { + protocol = "tcp" + port_range = "80" + source_addresses = ["0.0.0.0/0", "::/0"] + } + + inbound_rule { + protocol = "tcp" + port_range = "443" + source_addresses = ["0.0.0.0/0", "::/0"] + } + + # Outbound communication + outbound_rule { + protocol = "tcp" + port_range = "all" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + + outbound_rule { + protocol = "udp" + port_range = "all" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + + outbound_rule { + protocol = "icmp" + destination_addresses = ["0.0.0.0/0", "::/0"] + } +} + +resource "digitalocean_firewall" "vpc_communication" { + name = "${var.environment}-vpc-communication" + droplet_ids = [ digitalocean_droplet.nfs.id ] + tags = [digitalocean_tag.worker.id, digitalocean_tag.manager.id] + + # Internal communication + inbound_rule { + protocol = "tcp" + port_range = "all" + source_addresses = [var.subnet_range] + } + + inbound_rule { + protocol = "udp" + port_range = "all" + source_addresses = [var.subnet_range] + } +} + +resource "digitalocean_firewall" "ssh" { + name = "${var.environment}-ssh" + droplet_ids = [ digitalocean_droplet.nfs.id ] + tags = [digitalocean_tag.worker.id, digitalocean_tag.manager.id] + + # SSH + inbound_rule { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + } +} \ No newline at end of file diff --git a/terraform/modules/digitalocean/outputs.tf b/terraform/modules/digitalocean/outputs.tf new file mode 100644 index 000000000..80b8e96fe --- /dev/null +++ b/terraform/modules/digitalocean/outputs.tf @@ -0,0 +1,4 @@ +output "leader_public_ip" { + value = digitalocean_droplet.leader.ipv4_address + description = "The public IP address of the leader node" +} \ No newline at end of file diff --git a/terraform/modules/digitalocean/provider.tf b/terraform/modules/digitalocean/provider.tf new file mode 100644 index 000000000..4c7f8298e --- /dev/null +++ b/terraform/modules/digitalocean/provider.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = "~> 2.0" + } + } +} \ No newline at end of file diff --git a/terraform/modules/digitalocean/variables.tf b/terraform/modules/digitalocean/variables.tf new file mode 100644 index 000000000..0626d7020 --- /dev/null +++ b/terraform/modules/digitalocean/variables.tf @@ -0,0 +1,44 @@ +variable "private_key" { + description = "The path to the private key used to SSH into the droplets" +} +variable "project_name" { + description = "Name for the current infrastructure project" +} +variable "region" { + description = "The region to deploy the infrastructure to. See https://docs.digitalocean.com/products/platform/availability-matrix/#available-datacenters" +} +variable "environment" { + description = "Name of the current environment" +} +variable "base_image" { + description = "Base Image to use for all droplets" +} +variable "subnet_range" { + description = "Subnet range for the VPC" +} +variable "worker_size" { + description = "Size of the NFS node. See https://slugs.do-api.dev/" +} +variable "worker_count" { + description = "Count of worker nodes required" +} +variable "manager_size" { + description = "Size of the manager node. See https://slugs.do-api.dev/" +} +variable "manager_count" { + description = "Count of API nodes required" +} +variable "digitalocean_project_name" { + description = "Name of the DigitalOcean Project" +} + +data "digitalocean_ssh_key" "Christy" { + name = "Christy" +} + +data "external" "swarm_join_token" { + program = ["${path.module}/get-join-token.sh"] + query = { + host = "${digitalocean_droplet.leader.ipv4_address}" + } +} \ No newline at end of file