diff --git a/docker/swarm/stacks/api/.env.sample b/docker/swarm/stacks/api/.env.sample index 440875f47..9cd68b84e 100644 --- a/docker/swarm/stacks/api/.env.sample +++ b/docker/swarm/stacks/api/.env.sample @@ -1,10 +1,12 @@ # -- Docker Configuration DOCKER_REGISTRY= DEPLOYMENT_ENV= +DEPLOYMENT_VERSION= +DEPLOYMENT_TLD= # -- Runtime -NODE_ENV=production -FREECODECAMP_NODE_ENV=production +# NODE_ENV=production +# FREECODECAMP_NODE_ENV=production # PORT=3000 # HOST=0.0.0.0 @@ -13,6 +15,7 @@ MONGOHQ_URL= # -- Logging # FCC_API_LOG_LEVEL=info +LOKI_URL= SENTRY_DSN= SENTRY_ENVIRONMENT= @@ -28,11 +31,11 @@ AUTH0_DOMAIN= # -- Session, Cookie and JWT encryption strings JWT_SECRET= COOKIE_SECRET= -COOKIE_DOMAIN=.freecodecamp.org +# COOKIE_DOMAIN=.freecodecamp.org # -- Email -EMAIL_PROVIDER=ses -SES_REGION=us-east-1 +# EMAIL_PROVIDER=ses +# SES_REGION=us-east-1 SES_ID= SES_SECRET= diff --git a/docker/swarm/stacks/api/Makefile b/docker/swarm/stacks/api/Makefile new file mode 100644 index 000000000..1c63b9b15 --- /dev/null +++ b/docker/swarm/stacks/api/Makefile @@ -0,0 +1,68 @@ +# freeCodeCamp API Stack Deployment Makefile + +.PHONY: help validate decrypt config debug deploy clean + +# Default target +help: + @echo "freeCodeCamp API Stack Deployment" + @echo "" + @echo "Available targets:" + @echo " validate Validate all required environment variables" + @echo " decrypt Decrypt age-encrypted secrets (requires AGE_* vars)" + @echo " config Validate Docker stack configuration" + @echo " debug Save stack configuration to debug file" + @echo " deploy Deploy stack (auto-detects dev/prod from DEPLOYMENT_TLD)" + @echo " clean Remove temporary files" + @echo "" + @echo "Prerequisites:" + @echo " - Set DEPLOYMENT_VERSION and DEPLOYMENT_TLD environment variables" + @echo " - For encrypted secrets: run 'make decrypt' first" + @echo " - For manual deployment: source variables from .env or export manually" + +# Validate environment variables +validate: + @echo "Validating environment variables..." + @./scripts/validate-env.sh + +# Decrypt age-encrypted secrets +decrypt: + @echo "Decrypting secrets..." + @./scripts/decrypt-secrets.sh --save-env + +# Validate Docker stack configuration +config: + @echo "Validating Docker stack configuration..." + @docker stack config -c stack-api.yml > /dev/null + @echo "Stack configuration is valid" + +# Save debug configuration +debug: + @echo "Generating debug configuration..." + @if [ -z "$(DEPLOYMENT_VERSION)" ]; then \ + echo "ERROR: DEPLOYMENT_VERSION is required"; \ + exit 1; \ + fi + @docker stack config -c stack-api.yml > debug-docker-stack-config-$(DEPLOYMENT_VERSION).yml + @echo "Debug configuration saved to debug-docker-stack-config-$(DEPLOYMENT_VERSION).yml" + +# Deploy stack (auto-detects environment from DEPLOYMENT_TLD) +deploy: validate config + @echo "Deploying API stack..." + @if [ "$(DEPLOYMENT_TLD)" = "dev" ]; then \ + echo "Deploying to staging environment..."; \ + docker stack deploy -c stack-api.yml --prune --with-registry-auth --detach=false stg-api; \ + echo "Successfully deployed stg-api stack"; \ + elif [ "$(DEPLOYMENT_TLD)" = "org" ]; then \ + echo "Deploying to production environment..."; \ + docker stack deploy -c stack-api.yml --prune --with-registry-auth --detach=false prd-api; \ + echo "Successfully deployed prd-api stack"; \ + else \ + echo "ERROR: DEPLOYMENT_TLD must be 'dev' or 'org'"; \ + exit 1; \ + fi + +# Clean up temporary files +clean: + @echo "Cleaning up temporary files..." + @rm -f .env debug-docker-stack-config-*.yml + @echo "Cleanup complete" \ No newline at end of file diff --git a/docker/swarm/stacks/api/README.md b/docker/swarm/stacks/api/README.md index 92a71489d..e33fa68df 100644 --- a/docker/swarm/stacks/api/README.md +++ b/docker/swarm/stacks/api/README.md @@ -1,7 +1,97 @@ -## Usage +# API Stack -This stack defines all the services for the API. Name the stacks as per the environment ex: `prd-api`, etc. Set up the env values from the `.env.sample` file within the Portainer UI. +Docker Swarm stack configuration for freeCodeCamp Learn API. -**Caddyfile** +## Services -The Caddyfile is used to proxy the API to the correct port. It is located in the [`Caddyfile`](./Caddyfile) file. You will need to create a new Docker config for each new version of the file. Check the stack file for the correct name, and create the config within the `configs` section of Portainer. You can then set the `CADDY_CONFIG_NAME` environment variable to the name of the config you created. +| Service | Port | Health Check | +| ----------------- | --------- | -------------- | +| **svc-api-alpha** | 2345:3000 | `/status/ping` | +| **svc-api-bravo** | 2346:3000 | `/status/ping` | + +## Quick Start + +### 1. Prerequisites + +- Docker Swarm cluster initialized +- Nodes labeled with `api.enabled=true` and `api.variant=${DEPLOYMENT_TLD}` + +### 2. Deploy with Encrypted Secrets + +```bash +# Set required variables +export AGE_ENCRYPTED_ASC_SECRETS="" +export AGE_SECRET_KEY="" +export DEPLOYMENT_VERSION="" +export DEPLOYMENT_TLD="dev" # or "org" for production + +# Deploy in 2 steps +make decrypt # Decrypt secrets to .env file +source .env # Source environment variables +make deploy # Deploy stack (auto-detects staging/production) +``` + +### 3. Deploy with Manual Variables + +```bash +# Copy template and set values +cp .env.sample .env +# Edit .env with actual values... + +source .env # Source environment variables +make deploy # Deploy stack +``` + +## Available Commands + +- `make help` - Show all available commands +- `make decrypt` - Decrypt age-encrypted secrets to `.env` file +- `make validate` - Validate all required environment variables +- `make config` - Validate Docker stack configuration +- `make deploy` - Deploy stack (auto-detects dev/prod from DEPLOYMENT_TLD) +- `make debug` - Generate debug configuration file +- `make clean` - Remove temporary files + +## Environment Variables + +See `.env.sample` for the complete and current list. + +## Manual Deployment (Advanced) + +If you need to deploy without the Makefile: + +### 1. Label Nodes + +```bash +docker node update --label-add "api.enabled=true" +docker node update --label-add "api.variant=dev" # or "org" for production +``` + +### 2. Validate and Deploy + +```bash +# Validate environment +./scripts/validate-env.sh + +# Validate configuration +docker stack config -c stack-api.yml > /dev/null + +# Deploy stack manually +docker stack deploy -c stack-api.yml --prune --with-registry-auth --detach=false +``` + +**Stack naming convention:** + +- `DEPLOYMENT_TLD=dev` → `stg-api` +- `DEPLOYMENT_TLD=org` → `prd-api` + +## Notes + +- Use `make help` to see all available deployment commands +- Use `.env.sample` as template for required variables +- Scripts validate environment and handle age decryption automatically +- Docker Swarm doesn't support env files - source variables before deployment +- Services use host networking mode with placement constraints +- Health checks via `/status/ping?checker=swarm-manager` +- Loki logging with structured JSON pipeline +- Rolling updates with automatic rollback on 30% failure threshold diff --git a/docker/swarm/stacks/api/scripts/decrypt-secrets.sh b/docker/swarm/stacks/api/scripts/decrypt-secrets.sh new file mode 100755 index 000000000..4eabd532f --- /dev/null +++ b/docker/swarm/stacks/api/scripts/decrypt-secrets.sh @@ -0,0 +1,89 @@ +#!/bin/bash +set -e + +# API Stack Age Secret Decryption Script +# Decrypts age-encrypted secrets and sources environment variables + +if [[ -z "$AGE_ENCRYPTED_ASC_SECRETS" || -z "$AGE_SECRET_KEY" ]]; then + echo "ERROR: AGE_ENCRYPTED_ASC_SECRETS and AGE_SECRET_KEY environment variables are required" + exit 1 +fi + +echo "Decrypting secrets using age..." + +# Check if age is installed +if ! command -v age &> /dev/null; then + echo "ERROR: age is not installed. Install with: brew install age (macOS) or apt-get install age (Ubuntu)" + exit 1 +fi + +# Create temporary files +SECRETS_FILE=$(mktemp) +AGE_KEY_FILE=$(mktemp) +ENV_FILE=$(mktemp) +ENV_TMP_FILE=$(mktemp) + +# Cleanup function +cleanup() { + rm -f "$SECRETS_FILE" "$AGE_KEY_FILE" "$ENV_FILE" "$ENV_TMP_FILE" +} +trap cleanup EXIT + +echo "Creating temporary files..." + +# Write encrypted secrets and key to temporary files +echo "$AGE_ENCRYPTED_ASC_SECRETS" > "$SECRETS_FILE" +echo "$AGE_SECRET_KEY" > "$AGE_KEY_FILE" +chmod 600 "$AGE_KEY_FILE" + +echo "Decrypting secrets..." + +# Decrypt secrets +if ! age --identity "$AGE_KEY_FILE" --decrypt "$SECRETS_FILE" > "$ENV_FILE"; then + echo "ERROR: Failed to decrypt secrets" + exit 1 +fi + +echo "Cleaning up duplicate environment variables..." + +# Clean duplicates from .env (keep last occurrence of each variable) +touch "$ENV_TMP_FILE" +while IFS= read -r line; do + if [[ $line =~ ^[A-Za-z0-9_]+=.*$ ]]; then + # Extract the key (part before the first =) + key=${line%%=*} + # Remove any previous line with this key + sed -i.bak "/^${key}=/d" "$ENV_TMP_FILE" && rm -f "${ENV_TMP_FILE}.bak" + fi + # Append the current line + echo "$line" >> "$ENV_TMP_FILE" +done < "$ENV_FILE" + +echo "Adding deployment variables..." + +# Add deployment variables if they exist +{ + [[ -n "$DEPLOYMENT_VERSION" ]] && echo "DEPLOYMENT_VERSION=$DEPLOYMENT_VERSION" + [[ -n "$DEPLOYMENT_TLD" ]] && echo "DEPLOYMENT_TLD=$DEPLOYMENT_TLD" + [[ -n "$DEPLOYMENT_ENV" ]] && echo "DEPLOYMENT_ENV=$DEPLOYMENT_ENV" + [[ -n "$FCC_API_LOG_LEVEL" ]] && echo "FCC_API_LOG_LEVEL=$FCC_API_LOG_LEVEL" +} >> "$ENV_TMP_FILE" + +echo "Sourcing environment variables..." + +# Source all variables from the cleaned file +while IFS='=' read -r key value; do + if [[ -n "$key" && ! "$key" =~ ^# ]]; then + export "${key}=${value}" + echo " $key" + fi +done < "$ENV_TMP_FILE" + +VAR_COUNT=$(grep -c '^[A-Za-z0-9_]=' "$ENV_TMP_FILE" || echo "0") +echo "Successfully decrypted and sourced $VAR_COUNT environment variables" + +# Optional: Save to .env file for manual inspection +if [[ "$1" == "--save-env" ]]; then + cp "$ENV_TMP_FILE" .env + echo "Environment variables saved to .env file" +fi \ No newline at end of file diff --git a/docker/swarm/stacks/api/scripts/validate-env.sh b/docker/swarm/stacks/api/scripts/validate-env.sh new file mode 100755 index 000000000..6a04aa930 --- /dev/null +++ b/docker/swarm/stacks/api/scripts/validate-env.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -e + +# API Stack Environment Validation Script +# Validates all required environment variables are set + +REQUIRED_VARS=( + "DOCKER_REGISTRY" + "MONGOHQ_URL" + "SENTRY_DSN" + "SENTRY_ENVIRONMENT" + "AUTH0_CLIENT_ID" + "AUTH0_CLIENT_SECRET" + "AUTH0_DOMAIN" + "JWT_SECRET" + "COOKIE_SECRET" + "COOKIE_DOMAIN" + "SES_ID" + "SES_SECRET" + "GROWTHBOOK_FASTIFY_API_HOST" + "GROWTHBOOK_FASTIFY_CLIENT_KEY" + "HOME_LOCATION" + "API_LOCATION" + "STRIPE_SECRET_KEY" + "LOKI_URL" + "DEPLOYMENT_VERSION" + "DEPLOYMENT_TLD" + "DEPLOYMENT_ENV" + "FCC_API_LOG_LEVEL" +) + +echo "Validating environment variables for API stack deployment..." + +MISSING_VARS=() +for var in "${REQUIRED_VARS[@]}"; do + if [[ -z "${!var}" ]]; then + MISSING_VARS+=("$var") + fi +done + +if [[ ${#MISSING_VARS[@]} -gt 0 ]]; then + echo "ERROR: The following required environment variables are missing or empty:" + printf ' - %s\n' "${MISSING_VARS[@]}" + echo "" + echo "Use .env.sample as a reference for all required variables" + exit 1 +fi + +echo "All required environment variables are set (${#REQUIRED_VARS[@]} variables checked)" + +# Optional: Validate version format +if [[ ! "$DEPLOYMENT_VERSION" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "WARNING: DEPLOYMENT_VERSION format may be invalid: $DEPLOYMENT_VERSION" +fi + +# Optional: Validate TLD +if [[ "$DEPLOYMENT_TLD" != "dev" && "$DEPLOYMENT_TLD" != "org" ]]; then + echo "WARNING: DEPLOYMENT_TLD should be 'dev' or 'org', got: $DEPLOYMENT_TLD" +fi + +echo "Environment validation passed - ready for deployment" \ No newline at end of file