Skip to content
This repository was archived by the owner on Oct 10, 2025. It is now read-only.

Commit f13b08d

Browse files
committed
feat: [#14] implement persistent data volume for VM data persistence
This commit implements a dedicated 20GB persistent data volume that survives VM destruction, ensuring critical application data is preserved across infrastructure changes. ## Infrastructure Changes **Terraform Configuration:** - Add variable (default: 20GB) - Add resource - Attach persistent volume as second disk to VM **Cloud-init Configuration:** - Add disk setup for with GPT partition table - Add ext4 filesystem creation with 'torrust-data' label - Add automatic mount to with noatime option - Add directory structure creation and ownership setup ## Application Changes **Docker Compose Volume Mounts:** - Replace all `./storage/` paths with `/var/lib/torrust/` direct mounts - Remove symlink complexity for cleaner, more explicit configuration - Maintain same container functionality with persistent storage **Deployment Script Updates:** - Replace symlink creation with direct directory structure setup - Update all docker compose commands to use `--env-file /var/lib/torrust/compose/.env` - Add comprehensive persistent storage directory creation - Preserve existing .env files during deployment ## Data Persistence Benefits **What Survives VM Destruction:** - MySQL database (tracker data, user accounts, statistics) - Environment configuration (.env file with passwords/tokens) - Prometheus metrics (historical monitoring data) - Tracker logs and state - SSL certificates (when configured) - All application persistent data **What Gets Refreshed:** - Application code (deployed fresh from git) - System configuration (VM rebuilt from cloud-init) - Docker images (pulled fresh) ## Architecture Improvements **Twelve-Factor Compliance:** - Clean separation between infrastructure (Build) and application (Release/Run) - Configuration persists while code is deployed fresh - Maintains stateless application design with persistent data store **Filesystem Hierarchy Standard:** - Uses `/var/lib/torrust` for application persistent data - Follows Linux FHS conventions for service data storage - Clear separation between transient and persistent data ## Testing - All infrastructure tests pass - Docker Compose syntax validation passes - End-to-end deployment tests successful - Persistent data preservation verified across VM recreations This implementation provides production-ready data persistence while maintaining the clean twelve-factor architecture and deployment workflow.
1 parent 1f49a67 commit f13b08d

File tree

6 files changed

+118
-51
lines changed

6 files changed

+118
-51
lines changed

application/compose.yaml

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ services:
55
image: certbot/certbot
66
container_name: certbot
77
volumes:
8-
- ./storage/proxy/webroot:/var/www/html
9-
- ./storage/certbot/etc:/etc/letsencrypt
10-
- ./storage/certbot/lib:/var/lib/letsencrypt
8+
- /var/lib/torrust/proxy/webroot:/var/www/html
9+
- /var/lib/torrust/certbot/etc:/etc/letsencrypt
10+
- /var/lib/torrust/certbot/lib:/var/lib/letsencrypt
1111
logging:
1212
options:
1313
max-size: "10m"
@@ -25,11 +25,11 @@ services:
2525
- "80:80"
2626
- "443:443"
2727
volumes:
28-
- ./storage/proxy/webroot:/var/www/html
29-
- ./storage/proxy/etc/nginx-conf:/etc/nginx/conf.d
30-
- ./storage/certbot/etc:/etc/letsencrypt
31-
- ./storage/certbot/lib:/var/lib/letsencrypt
32-
- ./storage/dhparam:/etc/ssl/certs
28+
- /var/lib/torrust/proxy/webroot:/var/www/html
29+
- /var/lib/torrust/proxy/etc/nginx-conf:/etc/nginx/conf.d
30+
- /var/lib/torrust/certbot/etc:/etc/letsencrypt
31+
- /var/lib/torrust/certbot/lib:/var/lib/letsencrypt
32+
- /var/lib/torrust/dhparam:/etc/ssl/certs
3333
logging:
3434
options:
3535
max-size: "10m"
@@ -64,7 +64,7 @@ services:
6464
ports:
6565
- "9090:9090" # This port should not be exposed to the internet
6666
volumes:
67-
- ./storage/prometheus/etc:/etc/prometheus:Z
67+
- /var/lib/torrust/prometheus/etc:/etc/prometheus:Z
6868
logging:
6969
options:
7070
max-size: "10m"
@@ -87,12 +87,13 @@ services:
8787
- "3306:3306" # Only for debugging, remove in production
8888
volumes:
8989
- mysql_data:/var/lib/mysql
90-
- ./storage/mysql/init:/docker-entrypoint-initdb.d:ro
90+
- /var/lib/torrust/mysql/init:/docker-entrypoint-initdb.d:ro
9191
command: >
9292
--character-set-server=utf8mb4
9393
--collation-server=utf8mb4_unicode_ci
9494
healthcheck:
95-
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_PASSWORD}"]
95+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost",
96+
"-p${MYSQL_PASSWORD}"]
9697
interval: 10s
9798
timeout: 5s
9899
retries: 5
@@ -121,9 +122,9 @@ services:
121122
- 7070:7070
122123
- 1212:1212
123124
volumes:
124-
- ./storage/tracker/lib:/var/lib/torrust/tracker:Z
125-
- ./storage/tracker/log:/var/log/torrust/tracker:Z
126-
- ./storage/tracker/etc:/etc/torrust/tracker:Z
125+
- /var/lib/torrust/tracker/lib:/var/lib/torrust/tracker:Z
126+
- /var/lib/torrust/tracker/log:/var/log/torrust/tracker:Z
127+
- /var/lib/torrust/tracker/etc:/etc/torrust/tracker:Z
127128
logging:
128129
options:
129130
max-size: "10m"

infrastructure/cloud-init/user-data.yaml.tpl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@ users:
3333
# Disable SSH password authentication for security
3434
ssh_pwauth: false
3535

36+
# Disk and filesystem configuration
37+
disk_setup:
38+
/dev/vdb:
39+
table_type: gpt
40+
layout: true
41+
overwrite: false
42+
43+
fs_setup:
44+
- label: torrust-data
45+
filesystem: ext4
46+
device: /dev/vdb1
47+
overwrite: false
48+
49+
mounts:
50+
- ["/dev/vdb1", "/var/lib/torrust", "ext4", "defaults,noatime", "0", "2"]
51+
3652
# Package updates and installations
3753
package_update: true
3854
package_upgrade: true
@@ -109,6 +125,10 @@ write_files:
109125

110126
# Commands to run after package installation
111127
runcmd:
128+
# Set up persistent data volume and directory structure
129+
- mkdir -p /var/lib/torrust
130+
- chown -R torrust:torrust /var/lib/torrust
131+
112132
# Create torrust user directories
113133
- mkdir -p /home/torrust/github/torrust
114134
- chown -R torrust:torrust /home/torrust/github

infrastructure/docs/refactoring/multi-provider-abstraction/README.md

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ infrastructure/
8181

8282
The new architecture introduces a clear separation between:
8383

84-
1. **Provider-Agnostic Orchestration**: Main Terraform configuration that selects the appropriate provider module
84+
1. **Provider-Agnostic Orchestration**: Main Terraform configuration that selects the
85+
appropriate provider module
8586
2. **Provider-Specific Modules**: Self-contained modules for each cloud provider
8687
3. **Standardized Interfaces**: Common variables and outputs across all providers
8788
4. **Enhanced Configuration**: Environment-based provider selection and settings
@@ -173,8 +174,10 @@ module "hetzner_infrastructure" {
173174
# Standardized outputs (regardless of provider)
174175
output "vm_ip" {
175176
value = var.infrastructure_provider == "local" ?
176-
(length(module.local_infrastructure) > 0 ? module.local_infrastructure[0].vm_ip : "No IP assigned yet") :
177-
(length(module.hetzner_infrastructure) > 0 ? module.hetzner_infrastructure[0].vm_ip : "No IP assigned yet")
177+
(length(module.local_infrastructure) > 0 ?
178+
module.local_infrastructure[0].vm_ip : "No IP assigned yet") :
179+
(length(module.hetzner_infrastructure) > 0 ?
180+
module.hetzner_infrastructure[0].vm_ip : "No IP assigned yet")
178181
description = "IP address of the created VM"
179182
}
180183
@@ -187,8 +190,10 @@ output "vm_name" {
187190
188191
output "connection_info" {
189192
value = var.infrastructure_provider == "local" ?
190-
(length(module.local_infrastructure) > 0 ? module.local_infrastructure[0].connection_info : "VM not created") :
191-
(length(module.hetzner_infrastructure) > 0 ? module.hetzner_infrastructure[0].connection_info : "VM not created")
193+
(length(module.local_infrastructure) > 0 ?
194+
module.local_infrastructure[0].connection_info : "VM not created") :
195+
(length(module.hetzner_infrastructure) > 0 ?
196+
module.hetzner_infrastructure[0].connection_info : "VM not created")
192197
description = "SSH connection command"
193198
}
194199
```
@@ -271,7 +276,8 @@ USER_ID=1000
271276
# Extract infrastructure provider from environment
272277
get_infrastructure_provider() {
273278
if [[ -f "${CONFIG_DIR}/environments/${ENVIRONMENT}.env" ]]; then
274-
INFRASTRUCTURE_PROVIDER=$(grep "INFRASTRUCTURE_PROVIDER=" "${CONFIG_DIR}/environments/${ENVIRONMENT}.env" | cut -d'=' -f2)
279+
INFRASTRUCTURE_PROVIDER=$(grep "INFRASTRUCTURE_PROVIDER=" \
280+
"${CONFIG_DIR}/environments/${ENVIRONMENT}.env" | cut -d'=' -f2)
275281
fi
276282

277283
if [[ -z "${INFRASTRUCTURE_PROVIDER}" ]]; then
@@ -519,7 +525,9 @@ resource "hcloud_server" "vm" {
519525
520526
ssh_keys = [hcloud_ssh_key.torrust_key.id]
521527
522-
user_data = templatefile("${path.module}/../../cloud-init/${var.use_minimal_config ? "user-data-minimal.yaml.tpl" : "user-data.yaml.tpl"}", {
528+
user_data = templatefile(
529+
"${path.module}/../../cloud-init/${var.use_minimal_config ?
530+
"user-data-minimal.yaml.tpl" : "user-data.yaml.tpl"}", {
523531
ssh_public_key = var.ssh_public_key
524532
})
525533
@@ -668,29 +676,29 @@ output "hetzner_datacenter" {
668676
```makefile
669677
# Enhanced commands that work with any provider
670678
infra-apply: ## Provision infrastructure (works with any provider)
671-
@echo "Provisioning infrastructure for $(ENVIRONMENT)..."
672-
@echo "⚠️ This command may prompt for your password for provider-specific operations"
673-
$(SCRIPTS_DIR)/provision-infrastructure.sh $(ENVIRONMENT) apply
679+
@echo "Provisioning infrastructure for $(ENVIRONMENT)..."
680+
@echo "⚠️ This command may prompt for your password for provider-specific operations"
681+
$(SCRIPTS_DIR)/provision-infrastructure.sh $(ENVIRONMENT) apply
674682

675683
# Provider-specific configuration commands
676684
infra-config-hetzner: ## Generate Hetzner environment configuration
677-
@echo "Configuring Hetzner environment..."
678-
$(SCRIPTS_DIR)/configure-env.sh hetzner
685+
@echo "Configuring Hetzner environment..."
686+
$(SCRIPTS_DIR)/configure-env.sh hetzner
679687

680688
# Provider validation
681689
infra-test-prereq: ## Test system prerequisites for environment
682-
@echo "Testing prerequisites for $(ENVIRONMENT)..."
683-
$(INFRA_TESTS_DIR)/test-unit-infrastructure.sh prerequisites $(ENVIRONMENT)
690+
@echo "Testing prerequisites for $(ENVIRONMENT)..."
691+
$(INFRA_TESTS_DIR)/test-unit-infrastructure.sh prerequisites $(ENVIRONMENT)
684692

685693
# Provider-specific help
686694
infra-providers: ## Show supported infrastructure providers
687-
@echo "Supported Infrastructure Providers:"
688-
@echo " local - KVM/libvirt for local testing"
689-
@echo " hetzner - Hetzner Cloud for production"
690-
@echo ""
691-
@echo "Usage:"
692-
@echo " make infra-apply ENVIRONMENT=local # Local testing"
693-
@echo " make infra-apply ENVIRONMENT=hetzner # Hetzner production"
695+
@echo "Supported Infrastructure Providers:"
696+
@echo " local - KVM/libvirt for local testing"
697+
@echo " hetzner - Hetzner Cloud for production"
698+
@echo ""
699+
@echo "Usage:"
700+
@echo " make infra-apply ENVIRONMENT=local # Local testing"
701+
@echo " make infra-apply ENVIRONMENT=hetzner # Hetzner production"
694702
```
695703

696704
## 🚀 Benefits After Implementation

infrastructure/scripts/deploy-app.sh

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ release_stage() {
343343
fi
344344
" "Processing configuration for environment: ${ENVIRONMENT}"
345345

346-
# Ensure proper permissions
346+
# Set up persistent data volume and directory structure
347347
vm_exec "${vm_ip}" "
348348
cd /home/torrust/github/torrust/torrust-tracker-demo
349349
@@ -352,9 +352,22 @@ release_stage() {
352352
sudo ./infrastructure/scripts/fix-volume-permissions.sh
353353
fi
354354
355-
# Ensure storage directories exist
356-
mkdir -p application/storage/{tracker/lib/database,prometheus/data}
357-
" "Setting up application storage"
355+
# Ensure persistent storage directories exist
356+
sudo mkdir -p /var/lib/torrust/{tracker/{lib/database,log,etc},prometheus/{data,etc},proxy/{webroot,etc/nginx-conf},certbot/{etc,lib},dhparam,mysql/init,compose}
357+
358+
# Copy .env file to persistent storage if it doesn't exist
359+
if [ -f application/.env ] && [ ! -f /var/lib/torrust/compose/.env ]; then
360+
sudo cp application/.env /var/lib/torrust/compose/.env
361+
elif [ ! -f /var/lib/torrust/compose/.env ]; then
362+
# Create default .env from template if none exists
363+
if [ -f .env.production ]; then
364+
sudo cp .env.production /var/lib/torrust/compose/.env
365+
fi
366+
fi
367+
368+
# Ensure torrust user owns all persistent data
369+
sudo chown -R torrust:torrust /var/lib/torrust
370+
" "Setting up persistent data volume directory structure"
358371

359372
log_success "Release stage completed"
360373
}
@@ -371,7 +384,7 @@ wait_for_services() {
371384
log_info "Checking container status (attempt ${attempt}/${max_attempts})..."
372385

373386
# Get container status with service names only
374-
services=$(ssh -n -o StrictHostKeyChecking=no -o ConnectTimeout=10 "torrust@${vm_ip}" "cd /home/torrust/github/torrust/torrust-tracker-demo/application && docker compose ps --services" 2>/dev/null || echo "SSH_FAILED")
387+
services=$(ssh -n -o StrictHostKeyChecking=no -o ConnectTimeout=10 "torrust@${vm_ip}" "cd /home/torrust/github/torrust/torrust-tracker-demo/application && docker compose --env-file /var/lib/torrust/compose/.env ps --services" 2>/dev/null || echo "SSH_FAILED")
375388

376389
if [[ "${services}" == "SSH_FAILED" ]]; then
377390
log_warning "SSH connection failed while checking container status. Retrying in 10 seconds..."
@@ -397,7 +410,7 @@ wait_for_services() {
397410
container_count=$((container_count + 1))
398411

399412
# Get the container state and health for this service
400-
container_info=$(ssh -n -o StrictHostKeyChecking=no -o ConnectTimeout=10 "torrust@${vm_ip}" "cd /home/torrust/github/torrust/torrust-tracker-demo/application && docker compose ps ${service_name} --format '{{.State}}'" 2>/dev/null)
413+
container_info=$(ssh -n -o StrictHostKeyChecking=no -o ConnectTimeout=10 "torrust@${vm_ip}" "cd /home/torrust/github/torrust/torrust-tracker-demo/application && docker compose --env-file /var/lib/torrust/compose/.env ps ${service_name} --format '{{.State}}'" 2>/dev/null)
401414
health_status=$(ssh -n -o StrictHostKeyChecking=no -o ConnectTimeout=10 "torrust@${vm_ip}" "cd /home/torrust/github/torrust/torrust-tracker-demo/application && docker inspect ${service_name} --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' 2>/dev/null" || echo "no-healthcheck")
402415

403416
# Clean up output
@@ -447,7 +460,7 @@ wait_for_services() {
447460
done
448461

449462
log_error "Timeout waiting for services to become healthy after ${max_attempts} attempts."
450-
vm_exec "${vm_ip}" "cd /home/torrust/github/torrust/torrust-tracker-demo/application && docker compose ps && docker compose logs" "Dumping logs on failure"
463+
vm_exec "${vm_ip}" "cd /home/torrust/github/torrust/torrust-tracker-demo/application && docker compose --env-file /var/lib/torrust/compose/.env ps && docker compose --env-file /var/lib/torrust/compose/.env logs" "Dumping logs on failure"
451464
exit 1
452465
}
453466

@@ -463,7 +476,7 @@ run_stage() {
463476
cd /home/torrust/github/torrust/torrust-tracker-demo/application
464477
465478
if [ -f compose.yaml ]; then
466-
docker compose down --remove-orphans || true
479+
docker compose --env-file /var/lib/torrust/compose/.env down --remove-orphans || true
467480
fi
468481
" "Stopping existing services"
469482

@@ -474,7 +487,7 @@ run_stage() {
474487
475488
# Pull images with progress output
476489
echo 'Starting Docker image pull...'
477-
docker compose pull
490+
docker compose --env-file /var/lib/torrust/compose/.env pull
478491
echo 'Docker image pull completed'
479492
" 600 "Pulling Docker images with 10-minute timeout"
480493

@@ -483,7 +496,7 @@ run_stage() {
483496
cd /home/torrust/github/torrust/torrust-tracker-demo/application
484497
485498
# Start services
486-
docker compose up -d
499+
docker compose --env-file /var/lib/torrust/compose/.env up -d
487500
" "Starting application services"
488501

489502
# Wait for services to initialize
@@ -502,16 +515,16 @@ validate_deployment() {
502515
vm_exec "${vm_ip}" "
503516
cd /home/torrust/github/torrust/torrust-tracker-demo/application
504517
echo '=== Docker Compose Services (Detailed Status) ==='
505-
docker compose ps --format 'table {{.Service}}\t{{.State}}\t{{.Status}}\t{{.Ports}}'
518+
docker compose --env-file storage/compose/.env ps --format 'table {{.Service}}\t{{.State}}\t{{.Status}}\t{{.Ports}}'
506519
507520
echo ''
508521
echo '=== Docker Compose Services (Default Format) ==='
509-
docker compose ps
522+
docker compose --env-file storage/compose/.env ps
510523
511524
echo ''
512525
echo '=== Container Health Check Details ==='
513526
# Show health status for each container
514-
for container in \$(docker compose ps --format '{{.Name}}'); do
527+
for container in \$(docker compose --env-file storage/compose/.env ps --format '{{.Name}}'); do
515528
echo \"Container: \$container\"
516529
state=\$(docker inspect \$container --format '{{.State.Status}}')
517530
health=\$(docker inspect \$container --format '{{.State.Health.Status}}' 2>/dev/null || echo 'no-healthcheck')
@@ -527,7 +540,7 @@ validate_deployment() {
527540
done
528541
529542
echo '=== Service Logs (last 10 lines each) ==='
530-
docker compose logs --tail=10
543+
docker compose --env-file /var/lib/torrust/compose/.env logs --tail=10
531544
" "Checking detailed service status"
532545

533546
# Test application endpoints
@@ -592,8 +605,8 @@ show_connection_info() {
592605
echo
593606
echo "=== NEXT STEPS ==="
594607
echo "Health Check: make app-health-check ENVIRONMENT=${ENVIRONMENT}"
595-
echo "View Logs: ssh torrust@${vm_ip} 'cd torrust-tracker-demo/application && docker compose logs'"
596-
echo "Stop Services: ssh torrust@${vm_ip} 'cd torrust-tracker-demo/application && docker compose down'"
608+
echo "View Logs: ssh torrust@${vm_ip} 'cd torrust-tracker-demo/application && docker compose --env-file /var/lib/torrust/compose/.env logs'"
609+
echo "Stop Services: ssh torrust@${vm_ip} 'cd torrust-tracker-demo/application && docker compose --env-file /var/lib/torrust/compose/.env down'"
597610
echo
598611
}
599612

infrastructure/terraform/main.tf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ variable "vm_disk_size" {
5353
default = 20
5454
}
5555

56+
variable "persistent_data_size" {
57+
description = "Persistent data volume size in GB"
58+
type = number
59+
default = 20
60+
}
61+
5662
variable "base_image_url" {
5763
description = "URL for the base Ubuntu cloud image"
5864
type = string
@@ -85,6 +91,19 @@ resource "libvirt_volume" "vm_disk" {
8591
}
8692
}
8793

94+
# Create persistent data volume for application storage
95+
resource "libvirt_volume" "persistent_data" {
96+
name = "${var.vm_name}-data.qcow2"
97+
format = "qcow2"
98+
size = var.persistent_data_size * 1024 * 1024 * 1024 # Convert GB to bytes
99+
pool = "user-default"
100+
101+
# Fix permissions after creation
102+
provisioner "local-exec" {
103+
command = "${path.module}/../scripts/fix-volume-permissions.sh"
104+
}
105+
}
106+
88107
# Create cloud-init disk
89108
resource "libvirt_cloudinit_disk" "commoninit" {
90109
name = "${var.vm_name}-cloudinit.iso"
@@ -117,6 +136,11 @@ resource "libvirt_domain" "vm" {
117136
volume_id = libvirt_volume.vm_disk.id
118137
}
119138

139+
# Attach persistent data volume as second disk
140+
disk {
141+
volume_id = libvirt_volume.persistent_data.id
142+
}
143+
120144
network_interface {
121145
network_name = "default"
122146
wait_for_lease = false

project-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ netdev
5454
newgrp
5555
newtrackon
5656
nmap
57+
noatime
5758
NOPASSWD
5859
nosniff
5960
nullglob

0 commit comments

Comments
 (0)