From 33375c0969539c2b0664ff03d7b3bc62cf26f944 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 28 Aug 2026 09:56:33 -0500 Subject: [PATCH] Retain a single production backup --- .env.example | 2 +- Makefile | 4 ++-- backend/scripts/backup_postgres_to_s3.py | 2 +- backend/tests/scripts/test_backup_postgres_to_s3.py | 2 ++ backend/tests/test_kamal_deploy_config.py | 1 + config/crontab | 2 +- config/deploy.prd.yml | 2 +- config/deploy.yml | 2 +- 8 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 87397fac..b06e2b1c 100644 --- a/.env.example +++ b/.env.example @@ -111,7 +111,7 @@ ELASTICSEARCH_INDEX=btaa_geospatial_api # Shared guard. Keep false locally/dev; prd deploy config defaults this true. BACKUP_ENABLED=false BACKUP_REQUIRED_DEST=prd -BACKUP_RETENTION_COUNT=3 +BACKUP_RETENTION_COUNT=1 BACKUP_S3_BUCKET= BACKUP_S3_PREFIX=btaa-geospatial-api BACKUP_S3_REGION=us-east-2 diff --git a/Makefile b/Makefile index 90c2c7c6..18efe9be 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ KAMAL_REINDEX_REMOVE_LEGACY_INDEX ?= true # If unset, the target falls back to APPLICATION_URL from Kamal env. KAMAL_API_URL ?= KAMAL_CACHE_TYPE ?= search -KAMAL_BACKUP_RETAIN_COUNT ?= 3 +KAMAL_BACKUP_RETAIN_COUNT ?= 1 KAMAL_NETWORK_SELF_URL ?= KAMAL_NETWORK_EXTERNAL_URLS ?= https://api.github.com https://raw.githubusercontent.com https://gin.btaa.org http://example.com KAMAL_NETWORK_CONNECT_TIMEOUT ?= 5 @@ -1644,7 +1644,7 @@ kamal-backup-postgres: ## Run production-gated Postgres backup on Kamal @kamal app exec -d $(KAMAL_DEST) --roles cron --reuse "bash -lc '/opt/venv/bin/python3 /app/scripts/backup_postgres_to_s3.py'" # Manually run the production-gated Elasticsearch snapshot from the cron container. -# Usage: make kamal-backup-elasticsearch KAMAL_DEST=prd [KAMAL_BACKUP_RETAIN_COUNT=3] +# Usage: make kamal-backup-elasticsearch KAMAL_DEST=prd [KAMAL_BACKUP_RETAIN_COUNT=1] kamal-backup-elasticsearch: ## Run production-gated Elasticsearch S3 snapshot on Kamal @echo "Running Elasticsearch snapshot on Kamal cron container (KAMAL_DEST=$(KAMAL_DEST))..." @if [ -z "$$KAMAL_SSH_USER" ] || [ -z "$$KAMAL_HOST" ]; then \ diff --git a/backend/scripts/backup_postgres_to_s3.py b/backend/scripts/backup_postgres_to_s3.py index cd07ee40..d0acbe24 100644 --- a/backend/scripts/backup_postgres_to_s3.py +++ b/backend/scripts/backup_postgres_to_s3.py @@ -25,7 +25,7 @@ DEFAULT_DATABASE_NAME = "btaa_geospatial_api" DEFAULT_PREFIX = "btaa-geospatial-api" DEFAULT_REQUIRED_DEST = "prd" -DEFAULT_RETENTION_COUNT = 3 +DEFAULT_RETENTION_COUNT = 1 LOCK_FILENAME = "postgres-backup.lock" diff --git a/backend/tests/scripts/test_backup_postgres_to_s3.py b/backend/tests/scripts/test_backup_postgres_to_s3.py index 2ebe8fca..3013767b 100644 --- a/backend/tests/scripts/test_backup_postgres_to_s3.py +++ b/backend/tests/scripts/test_backup_postgres_to_s3.py @@ -103,6 +103,7 @@ def test_build_config_supports_local_target(monkeypatch, tmp_path: Path): monkeypatch.setenv("BACKUP_POSTGRES_TARGET", "local") monkeypatch.setenv("BACKUP_LOCAL_DIR", str(tmp_path / "backups")) monkeypatch.delenv("BACKUP_S3_BUCKET", raising=False) + monkeypatch.delenv("BACKUP_RETENTION_COUNT", raising=False) config = backup._build_config() @@ -110,6 +111,7 @@ def test_build_config_supports_local_target(monkeypatch, tmp_path: Path): assert config.bucket is None assert config.local_dir == tmp_path / "backups" assert config.work_dir == tmp_path / "backups" / ".tmp" + assert config.retention_count == 1 def test_prune_old_local_backups_keeps_newest_count(tmp_path: Path): diff --git a/backend/tests/test_kamal_deploy_config.py b/backend/tests/test_kamal_deploy_config.py index e2596844..2685f553 100644 --- a/backend/tests/test_kamal_deploy_config.py +++ b/backend/tests/test_kamal_deploy_config.py @@ -89,6 +89,7 @@ def test_prd_postgres_backup_uses_local_mounted_storage(): ) assert "ENV.fetch('BACKUP_POSTGRES_TARGET', 'local')" in config_text assert "ENV.fetch('BACKUP_LOCAL_DIR', '/var/backups/btaa-geospatial-api')" in config_text + assert "ENV.fetch('BACKUP_RETENTION_COUNT', '1')" in config_text def test_prd_memory_profile_protects_service_headroom(): diff --git a/config/crontab b/config/crontab index 36af6718..2a7ea7c1 100644 --- a/config/crontab +++ b/config/crontab @@ -35,7 +35,7 @@ TZ=America/Chicago 30 5 * * * /opt/venv/bin/python3 /app/scripts/backup_postgres_to_s3.py >> /proc/1/fd/1 2>> /proc/1/fd/2 # Daily 5:45 AM: production-gated Elasticsearch snapshot to S3. -45 5 * * * /opt/venv/bin/python3 /app/scripts/backup_elasticsearch.py --scheduled --create --wait --retain-count ${BACKUP_RETENTION_COUNT:-3} >> /proc/1/fd/1 2>> /proc/1/fd/2 +45 5 * * * /opt/venv/bin/python3 /app/scripts/backup_elasticsearch.py --scheduled --create --wait --retain-count ${BACKUP_RETENTION_COUNT:-1} >> /proc/1/fd/1 2>> /proc/1/fd/2 # Hourly: prune expired durable API response cache rows so the Postgres L2 cache # stays bounded even if Redis is cold or search traffic is high. diff --git a/config/deploy.prd.yml b/config/deploy.prd.yml index eb9bc3cc..b823379c 100644 --- a/config/deploy.prd.yml +++ b/config/deploy.prd.yml @@ -118,7 +118,7 @@ env: BACKUP_REQUIRED_DEST: prd BACKUP_POSTGRES_TARGET: "<%= ENV.fetch('BACKUP_POSTGRES_TARGET', 'local') %>" BACKUP_LOCAL_DIR: "<%= ENV.fetch('BACKUP_LOCAL_DIR', '/var/backups/btaa-geospatial-api') %>" - BACKUP_RETENTION_COUNT: "<%= ENV.fetch('BACKUP_RETENTION_COUNT', '3') %>" + BACKUP_RETENTION_COUNT: "<%= ENV.fetch('BACKUP_RETENTION_COUNT', '1') %>" BACKUP_S3_BUCKET: "<%= ENV.fetch('BACKUP_S3_BUCKET', '') %>" BACKUP_S3_PREFIX: "<%= ENV.fetch('BACKUP_S3_PREFIX', 'btaa-geospatial-api') %>" BACKUP_S3_REGION: "<%= ENV.fetch('BACKUP_S3_REGION', ENV.fetch('AWS_REGION', 'us-east-2')) %>" diff --git a/config/deploy.yml b/config/deploy.yml index 99cb61b6..996b4f15 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -157,7 +157,7 @@ env: BACKUP_REQUIRED_DEST: "prd" BACKUP_POSTGRES_TARGET: "s3" BACKUP_LOCAL_DIR: "<%= ENV.fetch('BACKUP_LOCAL_DIR', '/var/backups/btaa-geospatial-api') %>" - BACKUP_RETENTION_COUNT: "3" + BACKUP_RETENTION_COUNT: "1" BACKUP_S3_BUCKET: "<%= ENV.fetch('BACKUP_S3_BUCKET', '') %>" BACKUP_S3_PREFIX: "<%= ENV.fetch('BACKUP_S3_PREFIX', 'btaa-geospatial-api') %>" BACKUP_S3_REGION: "<%= ENV.fetch('BACKUP_S3_REGION', ENV.fetch('AWS_REGION', 'us-east-2')) %>"