Repository: DevOps-Sad-people/itu-minitwit
Pull request #249: Terraform
Author: login: borchand
Created: 2025-05-29T11:21:03Z | Merged: 2025-05-29T14:53:07Z



Diff:
diff --git a/.gitignore b/.gitignore
index cf9bf72..3fbf2f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -174,4 +174,9 @@ cython_debug/
 # Vagrant
 .vagrant
 
-tmp/latest_processed_sim_action_id.txt
\ No newline at end of file
+tmp/latest_processed_sim_action_id.txt
+/terraform/ssh_key
+terraform/secrets
+terraform/.terraform.lock.hcl
+/terraform/.terraform
+/terraform/temp
diff --git a/docs/development.md b/docs/development.md
index 06c0b3b..370d258 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -44,3 +44,98 @@ playwright codegen http://localhost:4567/public
 
 Then you can use the recorder tool to create the test and copy it to the python file `test_<new_test>.py` and function you have created.
 
+
+# Terraform
+We use Terraform to manage our production infrastructure. The Terraform code is located in the `terraform` directory.
+
+## Setup
+### Install `jq`
+
+Some of the scripts use `jq` which is a cli tool for parsing JSON.
+
+On ubuntu install with:
+
+```bash
+sudo apt install -y jq
+```
+
+On macOS install with:
+
+```bash
+brew install jq
+```
+On other systems:
+
+Download the binaries here: https://stedolan.github.io/jq/ and put them in a folder included in your system's PATH.
+
+### Install Terraform
+Follow the instructions here: https://learn.hashicorp.com/tutorials/terraform/install-cli
+
+### Install AWS CLI
+We use the AWS CLI to fetch and upload files to DigitalOcean Spaces Object Storage, which is an S3-compatible object storage service. The AWS CLI is a command-line tool for managing AWS services, but it can also be used with other S3-compatible services like DigitalOcean Spaces Object Storage.
+
+You can install it with the following commands:
+
+On macOS:
+
+```bash
+brew install awscli
+```
+On Ubuntu:
+
+```bash
+sudo apt install -y awscli
+```
+
+### Add secrets
+You need to add a file called `secrets` in the `terraform` directory. You can use the `secrets_template` file as a template.
+You can copy it with the following command:
+
+```bash
+cp secrets_template secrets
+```
+
+This file should contain the following variables:
+
+```bash
+export TF_VAR_do_token=<TF_VAR_do_token>
+export SPACE_NAME=sadpeople
+export STATE_FILE=minitwit/terraform.tfstate
+export AWS_ACCESS_KEY_ID=<AWS_ACCESS_KEY_ID>
+export AWS_SECRET_ACCESS_KEY=<AWS_SECRET_ACCESS_KEY>
+export DIGITAL_OCEAN_KEY=<DIGITAL_OCEAN_KEY>
+```
+
+The space name and state file are already set to the values we use in production.
+
+
+The `TF_VAR_do_token` is used to allow terraform to create resources in your DigitalOcean account. You can create a new token in the DigitalOcean API control panel. Make sure to give it the `read` and `write` permissions.
+
+The `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are your DigitalOcean Spaces Object Storage credentials. You can create them in the DigitalOcean Spaces Object Storage control panel
+
+The `DIGITAL_OCEAN_KEY` is used to sign to the regestry. You can create one in the DigitalOcean API control panel.
+
+
+## Usage
+### Modifying the infrastructure
+To modify the infrastructure, you can edit the Terraform files in the `terraform` directory. The main file is `minitwit_swarm_cluster.tf`, which contains the resources and their configurations.
+
+### Creating and updating the infrastructure
+
+We create the `bootstrap.sh` script to bootstrap the infrastructure. This script will create the necessary resources in DigitalOcean and configure them for use with our application.
+
+Make sure to be in the `terraform` directory and run the following command to create the infrastructure:
+
+```bash
+./bootstrap.sh
+```
+
+### Destroying the infrastructure
+To destroy the infrastructure, run the following command in the `terraform` directory:
+
+```bash
+./destroy.sh
+```
+This will prompt you for confirmation before destroying the infrastructure. Type `y` to confirm.
+**Note** that this will destroy all resources created by the `bootstrap.sh` script, including the database and any other resources. Make sure to back up any important data before running this command.
+
diff --git a/remote_files/docker-compose.yml b/remote_files/docker-compose.yml
index 0d887eb..29c0486 100644
--- a/remote_files/docker-compose.yml
+++ b/remote_files/docker-compose.yml
@@ -1,11 +1,21 @@
 version: "3.8"
 
 services:
+  visualizer:
+    image: dockersamples/visualizer:stable
+    ports:
+      - "8888:8080"
+    volumes:
+      - "/var/run/docker.sock:/var/run/docker.sock"
+    deploy:
+      placement:
+        constraints: 
+          - node.role == manager
+          - node.labels.leader == true
   web:
     image: registry.digitalocean.com/sad-containers/minitwit:${TAG}
     deploy:
       replicas: 4
-      mode: global
       update_config:
         delay: 10s
         order: start-first
@@ -34,6 +44,7 @@ services:
       placement:
         constraints:
           - "node.role==manager"
+          - node.labels.leader != true
     logging:
       driver: "json-file"
       options:
@@ -60,6 +71,7 @@ services:
       placement:
         constraints:
           - "node.role==manager"
+          - node.labels.leader != true
 
   prometheus:
     image: registry.digitalocean.com/sad-containers/prometheus:${TAG}
@@ -71,6 +83,7 @@ services:
       placement:
         constraints:
           - "node.role==manager"
+          - node.labels.leader != true
     logging:
       driver: "json-file"
       options:
@@ -91,6 +104,7 @@ services:
       placement:
         constraints:
           - "node.role==manager"
+          - node.labels.leader != true
     logging:
       driver: "json-file"
       options:
@@ -136,6 +150,7 @@ services:
       placement:
         constraints:
           - "node.role==manager"
+          - node.labels.leader != true
     logging:
       driver: "json-file"
       options:
@@ -162,6 +177,7 @@ services:
       placement:
         constraints:
           - "node.role==manager"
+          - node.labels.leader != true
     logging:
       driver: "json-file"
       options:
@@ -184,6 +200,7 @@ services:
       placement:
         constraints:
           - "node.role==manager"
+          - node.labels.leader != true
     ports:
       - "5601:5601"
     logging:
diff --git a/terraform/backend.tf b/terraform/backend.tf
new file mode 100644
index 0000000..16669af
--- /dev/null
+++ b/terraform/backend.tf
@@ -0,0 +1,15 @@
+terraform {
+  backend "s3" {
+    region = "fra1"
+    skip_credentials_validation = true
+    skip_metadata_api_check = true
+    skip_region_validation = true
+    skip_requesting_account_id = true
+    skip_s3_checksum = true
+    use_path_style = true
+    acl = "private"
+    endpoints = {
+      s3 = "https://fra1.digitaloceanspaces.com"
+    }
+  }
+}
diff --git a/terraform/bootstrap.sh b/terraform/bootstrap.sh
new file mode 100755
index 0000000..336b256
--- /dev/null
+++ b/terraform/bootstrap.sh
@@ -0,0 +1,129 @@
+#!/bin/bash
+
+set -e
+
+echo -e "\n--> Bootstrapping Minitwit\n"
+
+result=${PWD##*/} 
+# check if current directory is terraform
+if [ "$result" != "terraform" ]; then
+    echo -e "\n--> Please run this script from the terraform directory\n"
+    exit 1
+fi
+
+# check if ssh-key already exists
+if [ -d "ssh_key" ]; then
+    echo -e "\n--> SSH key already exists, skipping generation\n"
+else
+    echo -e "\n--> Generating SSH key\n"
+    mkdir ssh_key && ssh-keygen -t rsa -b 4096 -q -N '' -f ./ssh_key/terraform
+fi
+
+# check if temp directory already exists
+if [ -d "temp" ]; then
+    echo -e "\n--> Temp directory already exists, skipping generation\n"
+else
+    echo -e "\n--> Generating temp directory\n"
+    mkdir temp
+fi
+
+echo -e "\n--> Loading environment variables from secrets file\n"
+source secrets
+
+echo -e "\n--> Checking that environment variables are set\n"
+# check that all variables are set
+[ -z "$TF_VAR_do_token" ] && echo "TF_VAR_do_token is not set" && exit
+[ -z "$SPACE_NAME" ] && echo "SPACE_NAME is not set" && exit
+[ -z "$STATE_FILE" ] && echo "STATE_FILE is not set" && exit
+[ -z "$AWS_ACCESS_KEY_ID" ] && echo "AWS_ACCESS_KEY_ID is not set" && exit
+[ -z "$AWS_SECRET_ACCESS_KEY" ] && echo "AWS_SECRET_ACCESS_KEY is not set" && exit
+[ -z "$DIGITAL_OCEAN_KEY" ] && echo "DIGITAL_OCEAN_KEY is not set" && exit
+
+echo -e "\n--> Fetching Environment File from Digital Ocean\n"
+
+aws --endpoint-url https://fra1.digitaloceanspaces.com \
+    s3 cp s3://$SPACE_NAME/minitwit/.env ./stack/.env
+
+
+echo -e "\n--> Initializing terraform\n"
+# initialize terraform
+terraform init \
+    -backend-config "bucket=$SPACE_NAME" \
+    -backend-config "key=$STATE_FILE" \
+    -backend-config "access_key=$AWS_ACCESS_KEY_ID" \
+    -backend-config "secret_key=$AWS_SECRET_ACCESS_KEY"
+
+# check that everything looks good
+echo -e "\n--> Validating terraform configuration\n"
+terraform validate
+
+# create infrastructure
+echo -e "\n--> Creating Infrastructure\n"
+terraform apply -auto-approve
+
+# add label to swarm leader
+echo -e "\n--> Adding label to swarm leader\n"
+ssh root@$(terraform output -raw minitwit-swarm-leader-ip-address) \
+    -i ssh_key/terraform \
+    "docker node update --label-add leader=true minitwit-swarm-leader"
+
+
+# Sign in to container registry
+echo -e "\n--> Signing in to container registry\n"
+
+echo \
+
+ssh root@$(terraform output -raw minitwit-swarm-leader-ip-address) \
+    -i ssh_key/terraform \
+    "export DIGITAL_OCEAN_KEY=${DIGITAL_OCEAN_KEY} && \
+    sudo snap install doctl && \
+    sudo mkdir -p /root/.config /root/.docker && \
+    sudo snap connect doctl:dot-docker && \
+    doctl auth init -t \$DIGITAL_OCEAN_KEY && \
+    doctl registry login --never-expire"
+
+# Pull images from registry
+echo -e "\n--> Pulling images from registry\n"
+ssh \
+    -o 'StrictHostKeyChecking no' \
+    root@$(terraform output -raw minitwit-swarm-leader-ip-address) \
+    -i ssh_key/terraform \
+    'docker compose -f minitwit_stack.yml pull'
+
+# deploy the stack to the cluster
+echo -e "\n--> Deploying the Minitwit stack to the cluster\n"
+ssh \
+    -o 'StrictHostKeyChecking no' \
+    root@$(terraform output -raw minitwit-swarm-leader-ip-address) \
+    -i ssh_key/terraform \
+    'docker stack deploy minitwit -c minitwit_stack.yml --with-registry-auth'
+
+
+echo -e "\n--> Done bootstrapping Minitwit"
+echo -e "--> The dbs will need a moment to initialize, this can take up to a couple of minutes..."
+echo -e "--> Site will be avilable @ http://$(terraform output -raw public_ip):4567"
+echo -e "--> You can check the status of swarm cluster @ http://$(terraform output -raw minitwit-swarm-leader-ip-address):8888"
+echo -e "--> ssh to swarm leader with 'ssh root@\$(terraform output -raw minitwit-swarm-leader-ip-address) -i ssh_key/terraform'"
+echo -e "--> To remove the infrastructure run: bash destroy.sh"
+
+# ask user if they want to restore the database from backup
+read -p "Do you want to restore the database from backup? (y/n): " restore_db
+if [[ "$restore_db" == "y" || "$restore_db" == "Y" ]]; then
+
+    echo -e "\n--> Restoring database from backup\n"
+    backup_node=$(ssh -o 'StrictHostKeyChecking no' root@$(terraform output -raw minitwit-swarm-leader-ip-address) -i ssh_key/terraform \
+    "docker service ps minitwit_backup --filter 'desired-state=running' --format '{{.Node}}' | head -n 1")
+    echo -e "--> Backup node is: $backup_node"
+
+    backup_ip=$(ssh -o 'StrictHostKeyChecking no' root@$(terraform output -raw minitwit-swarm-leader-ip-address) -i ssh_key/terraform \
+    "docker node inspect $backup_node --format '{{.Status.Addr}}'")
+    echo -e "--> Backup IP is: $backup_ip"
+
+    # Now SSH into the correct node and run the restore script
+    ssh -o 'StrictHostKeyChecking no' root@$backup_ip -i ssh_key/terraform \
+    'backup_container=$(docker ps --filter "name=minitwit_backup" --format "{{.Names}}" | head -n 1) && docker exec $backup_container sh restore.sh'
+
+    echo -e "--> Database restored successfully"
+else
+    echo -e "--> Skipping database restore"
+fi
\ No newline at end of file
diff --git a/terraform/destroy.sh b/terraform/destroy.sh
new file mode 100755
index 0000000..5b4c53d
--- /dev/null
+++ b/terraform/destroy.sh
@@ -0,0 +1,37 @@
+
+set -e
+
+result=${PWD##*/} 
+# check if current directory is terraform
+if [ "$result" != "terraform" ]; then
+    echo -e "\n--> Please run this script from the terraform directory\n"
+    exit 1
+fi
+
+# ask for confirmation so don't destroy by accident
+echo -e "\n--> Destroying Minitwit Infrastructure\n"
+echo -e "\n--> This will destroy all resources created by terraform\n"
+echo -e "\n--> This will also destroy the database and all data in it\n"
+echo -e "\n--> A dump file of the database will be created in the temp folder and saved to the bucket\n"
+echo -e "\n--> Are you sure you want to continue? (y/n)\n"
+read -r -p "Enter your choice: " choice
+if [[ "$choice" != "y" && "$choice" != "Y" ]]; then
+    echo -e "\n--> Aborting...\n"
+    exit 1
+fi
+
+
+echo -e "\n--> Loading environment variables from secrets file\n"
+source secrets
+
+echo -e "\n--> Checking that environment variables are set\n"
+# check that all variables are set
+[ -z "$TF_VAR_do_token" ] && echo "TF_VAR_do_token is not set" && exit
+[ -z "$SPACE_NAME" ] && echo "SPACE_NAME is not set" && exit
+[ -z "$STATE_FILE" ] && echo "STATE_FILE is not set" && exit
+[ -z "$AWS_ACCESS_KEY_ID" ] && echo "AWS_ACCESS_KEY_ID is not set" && exit
+[ -z "$AWS_SECRET_ACCESS_KEY" ] && echo "AWS_SECRET_ACCESS_KEY is not set" && exit
+
+
+echo -e "\n--> Destroying Infrastructure\n"
+terraform destroy -auto-approve
diff --git a/terraform/ip.tf b/terraform/ip.tf
new file mode 100644
index 0000000..87a2ee2
--- /dev/null
+++ b/terraform/ip.tf
@@ -0,0 +1,12 @@
+resource "digitalocean_floating_ip" "public-ip" {
+  region = var.region
+}
+
+resource "digitalocean_floating_ip_assignment" "public-ip" {
+  ip_address = digitalocean_floating_ip.public-ip.ip_address
+  droplet_id = digitalocean_droplet.minitwit-swarm-leader.id
+}
+
+output "public_ip" {
+  value = digitalocean_floating_ip.public-ip.ip_address
+}
diff --git a/terraform/minitwit.auto.tfvars b/terraform/minitwit.auto.tfvars
new file mode 100644
index 0000000..2220bb2
--- /dev/null
+++ b/terraform/minitwit.auto.tfvars
@@ -0,0 +1,6 @@
+# do region
+region = "fra1"
+
+# ssh key
+pub_key = "ssh_key/terraform.pub"
+pvt_key = "ssh_key/terraform"
diff --git a/terraform/minitwit_swarm_cluster.tf b/terraform/minitwit_swarm_cluster.tf
new file mode 100644
index 0000000..05d8586
--- /dev/null
+++ b/terraform/minitwit_swarm_cluster.tf
@@ -0,0 +1,193 @@
+
+#  _                _
+# | | ___  __ _  __| | ___ _ __
+# | |/ _ \/ _` |/ _` |/ _ \ '__|
+# | |  __/ (_| | (_| |  __/ |
+# |_|\___|\__,_|\__,_|\___|_|
+
+# create cloud vm
+resource "digitalocean_droplet" "minitwit-swarm-leader" {
+  image = "docker-20-04" // ubuntu-22-04-x64
+  name = "minitwit-swarm-leader"
+  region = var.region
+  size = "s-1vcpu-1gb"
+  # add public ssh key so we can access the machine
+  ssh_keys = [digitalocean_ssh_key.minitwit.fingerprint]
+
+  # specify a ssh connection
+  connection {
+    user = "root"
+    host = self.ipv4_address
+    type = "ssh"
+    private_key = file(var.pvt_key)
+    timeout = "2m"
+  }
+
+  provisioner "file" {
+    source = "stack/minitwit_stack.yml"
+    destination = "/root/minitwit_stack.yml"
+  }
+
+  provisioner "file" {
+    source = "stack/.env"
+    destination = "/root/.env"
+  }
+
+  provisioner "remote-exec" {
+    inline = [
+      # allow ports for docker swarm
+      "ufw allow 2377/tcp",
+      "ufw allow 7946",
+      "ufw allow 4789/udp",
+      # ports for apps
+      "ufw allow 80",
+      "ufw allow 8080",
+      "ufw allow 8888",
+      # SSH
+      "ufw allow 22",
+
+      # initialize docker swarm cluster
+      "docker swarm init --advertise-addr ${self.ipv4_address}"
+    ]
+  }
+}
+
+
+resource "null_resource" "swarm-manager-token" {
+  depends_on = [digitalocean_droplet.minitwit-swarm-leader]
+  # save the manager join token
+  provisioner "local-exec" {
+    command = "ssh -o 'ConnectionAttempts 3600' -o 'StrictHostKeyChecking no' root@${digitalocean_droplet.minitwit-swarm-leader.ipv4_address} -i ssh_key/terraform 'docker swarm join-token manager -q' > temp/manager_token"
+  }
+}
+
+
+#  _ __ ___   __ _ _ __   __ _  __ _  ___ _ __
+# | '_ ` _ \ / _` | '_ \ / _` |/ _` |/ _ \ '__|
+# | | | | | | (_| | | | | (_| | (_| |  __/ |
+# |_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_|
+#                              |___/
+
+# create cloud vm
+resource "digitalocean_droplet" "minitwit-swarm-manager" {
+  # create managers after the leader
+  depends_on = [null_resource.swarm-manager-token]
+
+  # number of vms to create
+  count = 1
+
+  image = "docker-20-04"
+  name = "minitwit-swarm-manager-${count.index}"
+  region = var.region
+  size = "s-2vcpu-4gb"
+  # add public ssh key so we can access the machine
+  ssh_keys = [digitalocean_ssh_key.minitwit.fingerprint]
+
+  # specify a ssh connection
+  connection {
+    user = "root"
+    host = self.ipv4_address
+    type = "ssh"
+    private_key = file(var.pvt_key)
+    timeout = "2m"
+  }
+
+  provisioner "file" {
+    source = "temp/manager_token"
+    destination = "/root/manager_token"
+  }
+
+  provisioner "remote-exec" {
+    inline = [
+      # allow ports for docker swarm
+      "ufw allow 2377/tcp",
+      "ufw allow 7946",
+      "ufw allow 4789/udp",
+      # ports for apps
+      "ufw allow 80",
+      "ufw allow 8080",
+      "ufw allow 8888",
+      # SSH
+      "ufw allow 22",
+
+      # join swarm cluster as managers
+      "docker swarm join --token $(cat manager_token) ${digitalocean_droplet.minitwit-swarm-leader.ipv4_address}"
+    ]
+  }
+}
+
+
+resource "null_resource" "swarm-worker-token" {
+  depends_on = [digitalocean_droplet.minitwit-swarm-leader, digitalocean_droplet.minitwit-swarm-manager]
+
+  # save the worker join token
+  provisioner "local-exec" {
+    command = "ssh -o 'ConnectionAttempts 3600' -o 'StrictHostKeyChecking no' root@${digitalocean_droplet.minitwit-swarm-leader.ipv4_address} -i ssh_key/terraform 'docker swarm join-token worker -q' > temp/worker_token"
+  }
+}
+
+#                     _
+# __      _____  _ __| | _____ _ __
+# \ \ /\ / / _ \| '__| |/ / _ \ '__|
+#  \ V  V / (_) | |  |   <  __/ |
+#   \_/\_/ \___/|_|  |_|\_\___|_|
+#
+# create cloud vm
+resource "digitalocean_droplet" "minitwit-swarm-worker" {
+  # create workers after the leader
+  depends_on = [null_resource.swarm-worker-token]
+
+  # number of vms to create
+  count = 2
+
+  image = "docker-20-04"
+  name = "minitwit-swarm-worker-${count.index}"
+  region = var.region
+  size = "s-1vcpu-1gb"
+  # add public ssh key so we can access the machine
+  ssh_keys = [digitalocean_ssh_key.minitwit.fingerprint]
+
+  # specify a ssh connection
+  connection {
+    user = "root"
+    host = self.ipv4_address
+    type = "ssh"
+    private_key = file(var.pvt_key)
+    timeout = "2m"
+  }
+
+  provisioner "file" {
+    source = "temp/worker_token"
+    destination = "/root/worker_token"
+  }
+
+  provisioner "remote-exec" {
+    inline = [
+      # allow ports for docker swarm
+      "ufw allow 2377/tcp",
+      "ufw allow 7946",
+      "ufw allow 4789/udp",
+      # ports for apps
+      "ufw allow 80",
+      "ufw allow 8080",
+      "ufw allow 8888",
+      # SSH
+      "ufw allow 22",
+
+      # join swarm cluster as workers
+      "docker swarm join --token $(cat worker_token) ${digitalocean_droplet.minitwit-swarm-leader.ipv4_address}"
+    ]
+  }
+}
+
+output "minitwit-swarm-leader-ip-address" {
+  value = digitalocean_droplet.minitwit-swarm-leader.ipv4_address
+}
+
+output "minitwit-swarm-manager-ip-address" {
+  value = digitalocean_droplet.minitwit-swarm-manager.*.ipv4_address
+}
+
+output "minitwit-swarm-worker-ip-address" {
+  value = digitalocean_droplet.minitwit-swarm-worker.*.ipv4_address
+}
diff --git a/terraform/provider.tf b/terraform/provider.tf
new file mode 100644
index 0000000..bb2dfd0
--- /dev/null
+++ b/terraform/provider.tf
@@ -0,0 +1,29 @@
+# api token
+# here it is exported in the environment like
+# export TF_VAR_do_token=xxx
+variable "do_token" {}
+
+# do region
+variable "region" {}
+
+# make sure to generate a pair of ssh keys
+variable "pub_key" {}
+variable "pvt_key" {}
+
+# setup the provider
+terraform {
+        required_providers {
+                digitalocean = {
+                        source = "digitalocean/digitalocean"
+                        version = "~> 2.37.1"
+                }
+                null = {
+                        source = "hashicorp/null"
+                        version = "3.1.0"
+                }
+        }
+}
+
+provider "digitalocean" {
+  token = var.do_token
+}
\ No newline at end of file
diff --git a/terraform/secrets_template b/terraform/secrets_template
new file mode 100644
index 0000000..e78d861
--- /dev/null
+++ b/terraform/secrets_template
@@ -0,0 +1,6 @@
+export TF_VAR_do_token=
+export SPACE_NAME=
+export STATE_FILE=
+export AWS_ACCESS_KEY_ID=
+export AWS_SECRET_ACCESS_KEY=
+export DIGITAL_OCEAN_KEY=
diff --git a/terraform/ssh_key.tf b/terraform/ssh_key.tf
new file mode 100644
index 0000000..d13adc6
--- /dev/null
+++ b/terraform/ssh_key.tf
@@ -0,0 +1,6 @@
+
+# add the ssh key
+resource "digitalocean_ssh_key" "minitwit" {
+  name = "minitwit"
+  public_key = file(var.pub_key)
+}
diff --git a/terraform/stack/minitwit_stack.yml b/terraform/stack/minitwit_stack.yml
new file mode 100644
index 0000000..09701fd
--- /dev/null
+++ b/terraform/stack/minitwit_stack.yml
@@ -0,0 +1,255 @@
+version: "3.8"
+
+services:
+  visualizer:
+    image: dockersamples/visualizer:stable
+    ports:
+      - "8888:8080"
+    volumes:
+      - "/var/run/docker.sock:/var/run/docker.sock"
+    deploy:
+      placement:
+        constraints: 
+          - node.role == manager
+          - node.labels.leader == true
+  web:
+    image: registry.digitalocean.com/sad-containers/minitwit:main
+    deploy:
+      replicas: 4
+      update_config:
+        delay: 10s
+        order: start-first
+      placement:
+        constraints:
+          - "node.role!=manager"
+    logging:
+      driver: "json-file"
+      options:
+        max-file: 4
+        max-size: 250m
+    env_file:
+      - .env
+    ports:
+      - "4567:4567"
+    networks:
+      - minitwit_network
+
+  db:
+    image: postgres:15
+    deploy:
+      replicas: 1
+      update_config:
+        delay: 10s
+        order: start-first
+      placement:
+        constraints:
+          - "node.role==manager"
+          - node.labels.leader != true
+    logging:
+      driver: "json-file"
+      options:
+        max-file: 4
+        max-size: 250m
+    env_file:
+      - .env
+    volumes:
+      - pg_data:/var/lib/postgresql/data
+    networks:
+      - minitwit_network
+
+  backup:
+    image: eeshugerman/postgres-backup-s3:15
+    env_file:
+      - .env
+    networks:
+      - minitwit_network
+    deploy:
+      replicas: 1
+      update_config:
+        delay: 10s
+        order: start-first
+      placement:
+        constraints:
+          - "node.role==manager"
+          - node.labels.leader != true
+
+  prometheus:
+    image: registry.digitalocean.com/sad-containers/prometheus:main
+    deploy:
+      replicas: 1
+      update_config:
+        delay: 10s
+        order: start-first
+      placement:
+        constraints:
+          - "node.role==manager"
+          - node.labels.leader != true
+    logging:
+      driver: "json-file"
+      options:
+        max-file: 4
+        max-size: 250m
+    volumes:
+      - prometheus-data:/prometheus
+    networks:
+      - minitwit_network
+
+  grafana:
+    image: registry.digitalocean.com/sad-containers/grafana:main
+    deploy:
+      replicas: 1
+      update_config:
+        delay: 10s
+        order: start-first
+      placement:
+        constraints:
+          - "node.role==manager"
+          - node.labels.leader != true
+    logging:
+      driver: "json-file"
+      options:
+        max-file: 4
+        max-size: 250m
+    env_file:
+      - .env
+    volumes:
+      - grafana-data:/var/lib/grafana
+    ports:
+      - "3000:3000"
+    networks:
+      - minitwit_network
+
+  elasticsearch-setup:
+    image: registry.digitalocean.com/sad-containers/elasticsearch-setup:main
+    deploy:
+      replicas: 1
+      placement:
+        constraints:
+          - "node.role==manager"
+      restart_policy:
+        condition: none
+    logging:
+      driver: "json-file"
+      options:
+        max-file: 4
+        max-size: 250m
+    env_file:
+      - .env
+    volumes:
+      - elasticsearch-init:/data
+    networks:
+      - minitwit_network
+
+  elasticsearch:
+    image: registry.digitalocean.com/sad-containers/elasticsearch:main
+    deploy:
+      replicas: 1
+      update_config:
+        delay: 10s
+        order: start-first
+      placement:
+        constraints:
+          - "node.role==manager"
+          - node.labels.leader != true
+    logging:
+      driver: "json-file"
+      options:
+        max-file: 4
+        max-size: 250m
+    environment:
+      node.name: elasticsearch
+      discovery.type: single-node
+      ES_JAVA_OPTS: -Xms512m -Xmx512m
+    env_file:
+      - .env
+    volumes:
+      - elasticsearch-data:/usr/share/elasticsearch/data:Z
+    networks:
+      - minitwit_network
+
+  logstash:
+    image: registry.digitalocean.com/sad-containers/logstash:main
+    deploy:
+      replicas: 1
+      update_config:
+        delay: 10s
+        order: start-first
+      placement:
+        constraints:
+          - "node.role==manager"
+          - node.labels.leader != true
+    logging:
+      driver: "json-file"
+      options:
+        max-file: 4
+        max-size: 250m
+    environment:
+      LS_JAVA_OPTS: -Xms256m -Xmx256m
+    env_file:
+      - .env
+    networks:
+      - minitwit_network
+
+  kibana:
+    image: registry.digitalocean.com/sad-containers/kibana:main
+    deploy:
+      replicas: 1
+      update_config:
+        delay: 10s
+        order: start-first
+      placement:
+        constraints:
+          - "node.role==manager"
+          - node.labels.leader != true
+    ports:
+      - "5601:5601"
+    logging:
+      driver: "json-file"
+      options:
+        max-file: 4
+        max-size: 250m
+    env_file:
+      - .env
+    networks:
+      - minitwit_network
+
+  filebeat:
+    image: registry.digitalocean.com/sad-containers/filebeat:main
+    deploy:
+      mode: global
+      update_config:
+        delay: 10s
+        order: start-first
+    user: root
+    command:
+      - -e
+      - --strict.perms=false
+    logging:
+      driver: "json-file"
+      options:
+        max-file: 4
+        max-size: 250m
+    env_file:
+      - .env
+    volumes:
+      - type: bind
+        source: /var/lib/docker/containers
+        target: /var/lib/docker/containers
+        read_only: true
+      - type: bind
+        source: /var/run/docker.sock
+        target: /var/run/docker.sock
+        read_only: true
+    networks:
+      - minitwit_network
+
+volumes:
+  pg_data:
+  prometheus-data:
+  grafana-data:
+  elasticsearch-data:
+  elasticsearch-init:
+
+networks:
+  minitwit_network:
+    driver: overlay
diff --git a/terraform/stack/ssh_key.tf b/terraform/stack/ssh_key.tf
new file mode 100644
index 0000000..d13adc6
--- /dev/null
+++ b/terraform/stack/ssh_key.tf
@@ -0,0 +1,6 @@
+
+# add the ssh key
+resource "digitalocean_ssh_key" "minitwit" {
+  name = "minitwit"
+  public_key = file(var.pub_key)
+}


Reviews:
user: login: xXPinkmagicXx
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T11:42:43Z
user: login: borchand
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T11:47:43Z
user: login: borchand
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T12:09:09Z

Review comments:
diff_hunk: @@ -0,0 +1,15 @@
+terraform {
+  backend "s3" {
+    region = "us-west-1"
path: terraform/backend.tf
user: login: xXPinkmagicXx
body: us-west? Does it matter?
created_at: 2025-05-29T11:38:55Z
author_association: CONTRIBUTOR
path: terraform/stack/minitwit_stack.yml
user: login: xXPinkmagicXx
body: This should replace the docker compose file in `remote_files` , right?

created_at: 2025-05-29T11:42:25Z
author_association: CONTRIBUTOR
path: terraform/stack/minitwit_stack.yml
user: login: borchand
body: Think we still need that. So we can deploy new changes
created_at: 2025-05-29T11:47:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,15 @@
+terraform {
+  backend "s3" {
+    region = "us-west-1"
path: terraform/backend.tf
user: login: borchand
body: Now it is fra1

created_at: 2025-05-29T12:09:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-29T12:08:25Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DevOps-Sad-people_itu-minitwit&pullRequest=249) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DevOps-Sad-people_itu-minitwit&pullRequest=249&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DevOps-Sad-people_itu-minitwit&pullRequest=249&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DevOps-Sad-people_itu-minitwit&pullRequest=249&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DevOps-Sad-people_itu-minitwit&pullRequest=249&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DevOps-Sad-people_itu-minitwit&pullRequest=249&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=DevOps-Sad-people_itu-minitwit&pullRequest=249)


author_association: NONE