Zero-Downtime Blue-Green Deployment Pipelines for Perl Applications on DigitalOcean
Architectural Overview: Blue-Green for Perl on DigitalOcean
Implementing zero-downtime deployments for Perl applications, especially those with stateful components or complex dependencies, requires a robust strategy. Blue-Green deployment offers a proven path by maintaining two identical production environments: “Blue” (current live) and “Green” (new version). Traffic is switched from Blue to Green only after Green has been thoroughly validated. This post details a practical implementation on DigitalOcean using HAProxy for traffic management and a simple scripting approach for environment provisioning and deployment.
Prerequisites and Setup
Before diving into the deployment pipeline, ensure the following are in place:
- A DigitalOcean account with sufficient Droplets and resources.
- SSH access to your DigitalOcean account and a basic understanding of Droplet management.
- Perl application code managed in a Git repository.
- A mechanism for managing application dependencies (e.g., Carton, cpanm).
- HAProxy installed and configured on a dedicated load balancer Droplet.
- Basic familiarity with shell scripting.
Infrastructure Provisioning with `doctl`
We’ll leverage DigitalOcean’s command-line interface (`doctl`) to provision and manage Droplets. This allows for repeatable infrastructure setup. First, ensure `doctl` is installed and authenticated. You can find installation instructions on the DigitalOcean documentation. We’ll define our infrastructure in a shell script.
Droplet Configuration Script (`provision_infra.sh`)
This script will create the necessary Droplets for our Blue and Green environments, along with a load balancer. For simplicity, we’ll assume a single application server per environment. In a production scenario, you’d likely have multiple servers per environment behind HAProxy.
#!/bin/bash
# --- Configuration ---
REGION="nyc3"
SIZE="s-2vcpu-4gb" # Adjust as needed
IMAGE="ubuntu-22-04-x64"
SSH_KEY_FINGERPRINT="YOUR_SSH_KEY_FINGERPRINT" # Get from 'doctl compute ssh-key list'
APP_NAME="my-perl-app"
TAG_BLUE="env:blue"
TAG_GREEN="env:green"
TAG_LB="role:loadbalancer"
LB_IP="" # Will be populated after LB creation
# --- Create Load Balancer ---
echo "Creating HAProxy Load Balancer..."
LB_ID=$(doctl compute load-balancers create \
--region $REGION \
--size $SIZE \
--name "${APP_NAME}-lb" \
--tag $TAG_LB \
--health-check '{"protocol":"tcp", "port":80, "path":"/"}' \
--algorithm round_robin \
--droplet-ids "" \
--node-ports '[80,443]' \
--tag-filter $TAG_BLUE \
--tag-filter $TAG_GREEN \
--format ID --no-header)
if [ -z "$LB_ID" ]; then
echo "Error creating load balancer."
exit 1
fi
echo "Load Balancer created with ID: $LB_ID"
# Wait for LB to be ready and get its IP
echo "Waiting for Load Balancer IP..."
sleep 60 # Give it some time to provision
LB_IP=$(doctl compute load-balancers get $LB_ID --format PublicIPv4 --no-header)
if [ -z "$LB_IP" ]; then
echo "Error retrieving Load Balancer IP. Please check DigitalOcean console."
exit 1
fi
echo "Load Balancer IP: $LB_IP"
# --- Create Blue Environment Droplets ---
echo "Creating Blue environment Droplets..."
doctl compute droplet create "${APP_NAME}-blue-1" \
--region $REGION \
--size $SIZE \
--image $IMAGE \
--ssh-keys $SSH_KEY_FINGERPRINT \
--tag $TAG_BLUE \
--wait
echo "Blue Droplet created."
# --- Create Green Environment Droplets ---
echo "Creating Green environment Droplets..."
doctl compute droplet create "${APP_NAME}-green-1" \
--region $REGION \
--size $SIZE \
--image $IMAGE \
--ssh-keys $SSH_KEY_FINGERPRINT \
--tag $TAG_GREEN \
--wait
echo "Green Droplet created."
echo "Infrastructure provisioning complete."
echo "Load Balancer IP: $LB_IP"
echo "Remember to configure HAProxy on the load balancer Droplet."
Note: Replace YOUR_SSH_KEY_FINGERPRINT with your actual SSH key fingerprint. You can obtain this by running doctl compute ssh-key list. The --tag-filter on the load balancer is crucial for dynamically associating Droplets. We’ll add Droplets to these tags during deployment.
HAProxy Configuration for Blue-Green
The HAProxy configuration is central to directing traffic. We’ll use two distinct backend server groups, one for “Blue” and one for “Green”. A frontend rule will initially direct all traffic to the “Blue” backend. A manual intervention (or an automated script) will be required to switch traffic.
HAProxy Configuration File (`/etc/haproxy/haproxy.cfg`)
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend http_frontend
bind *:80
bind *:443 ssl crt /etc/ssl/private/your_domain.pem # Assuming you have SSL configured
mode http
default_backend blue_backend
backend blue_backend
mode http
balance roundrobin
# Dynamically add/remove servers based on tags
# This requires a script to update HAProxy config and reload
backend green_backend
mode http
balance roundrobin
# Dynamically add/remove servers based on tags
# This requires a script to update HAProxy config and reload
# Example of static configuration (will be managed by script)
# server blue1 192.168.1.10:80 check
# server green1 192.168.1.20:80 check
listen stats
bind *:8404
mode http
stats enable
stats uri /stats
stats realm Haproxy\ Statistics
stats auth admin:YourSecurePassword # CHANGE THIS
stats admin if TRUE
The key here is that the backend sections are initially empty or commented out. A separate script will be responsible for fetching Droplet IPs tagged with env:blue and env:green and dynamically updating the HAProxy configuration. After updating, HAProxy must be reloaded:
sudo systemctl reload haproxy
Deployment Pipeline Script
This script orchestrates the deployment process. It clones the application, installs dependencies, deploys to the “Green” environment, tests it, and then switches traffic.
Deployment Script (`deploy.sh`)
#!/bin/bash # --- Configuration --- APP_REPO="[email protected]:your_org/your_perl_app.git" APP_DIR="/opt/your_perl_app" DEPLOY_USER="deploy" # User on application Droplets SSH_USER="root" # SSH user for Droplets DIGITALOCEAN_TAG_BLUE="env:blue" DIGITALOCEAN_TAG_GREEN="env:green" APP_PORT="8080" # Port your Perl app listens on HEALTH_CHECK_URL="/health" # URL for health checks # --- Functions --- # Get Droplet IPs by tag get_droplet_ips() { local tag=$1 doctl compute droplet list --tag $tag --format PublicIPv4 --no-header | tr '\n' ' ' } # Update HAProxy configuration with current active servers update_haproxy() { local blue_ips=$(get_droplet_ips $DIGITALOCEAN_TAG_BLUE) local green_ips=$(get_droplet_ips $DIGITALOCEAN_TAG_GREEN) # Construct backend server lines local blue_servers="" for ip in $blue_ips; do blue_servers+="server blue_app_${ip//./_} $ip:$APP_PORT check\n" done local green_servers="" for ip in $green_ips; do green_servers+="server green_app_${ip//./_} $ip:$APP_PORT check\n" done # Create a temporary HAProxy config # This is a simplified example. In production, you'd likely use a template # and manage the full config file more robustly. cat << EOF > /tmp/haproxy.cfg global log /dev/log local0 log /dev/log local1 notice chroot /var/lib/haproxy stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners stats timeout 30s user haproxy group haproxy daemon defaults log global mode http option httplog option dontlognull timeout connect 5000 timeout client 50000 timeout server 50000 errorfile 400 /etc/haproxy/errors/400.http errorfile 403 /etc/haproxy/errors/403.http errorfile 408 /etc/haproxy/errors/408.http errorfile 500 /etc/haproxy/errors/500.http errorfile 502 /etc/haproxy/errors/502.http errorfile 503 /etc/haproxy/errors/503.http errorfile 504 /etc/haproxy/errors/504.http frontend http_frontend bind *:80 bind *:443 ssl crt /etc/ssl/private/your_domain.pem mode http acl is_green hdr(Host) -i green.your_domain.com # Example for testing Green directly use_backend green_backend if is_green default_backend blue_backend backend blue_backend mode http balance roundrobin $blue_servers backend green_backend mode http balance roundrobin $green_servers listen stats bind *:8404 mode http stats enable stats uri /stats stats realm Haproxy\ Statistics stats auth admin:YourSecurePassword stats admin if TRUE EOF # Validate and replace HAProxy config if haproxy -c -f /tmp/haproxy.cfg; then sudo mv /tmp/haproxy.cfg /etc/haproxy/haproxy.cfg sudo systemctl reload haproxy echo "HAProxy configuration updated and reloaded." else echo "HAProxy configuration validation failed. Aborting." exit 1 fi } # Deploy application to a specific environment (blue or green) deploy_to_environment() { local env_tag=$1 local env_name=$2 # e.g., "Blue" or "Green" echo "Deploying to $env_name environment ($env_tag)..." # Get IPs for the target environment local target_ips=$(get_droplet_ips $env_tag) if [ -z "$target_ips" ]; then echo "No Droplets found for tag $env_tag. Skipping deployment to $env_name." return 1 fi # Add Droplets to the load balancer's target pools # This step is crucial for HAProxy to see the servers. # The exact command depends on how your LB is configured. # For DigitalOcean Load Balancers, this is managed via tags. # We assume the LB is already configured to watch these tags. # Deploy code and dependencies for ip in $target_ips; do echo "Deploying to $ip..." ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $SSH_USER@$ip << EOF set -e # Exit immediately if a command exits with a non-zero status. # Ensure deploy user exists and has sudo privileges if ! id "$DEPLOY_USER" &>/dev/null; then sudo useradd -m -s /bin/bash $DEPLOY_USER echo "$DEPLOY_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/$DEPLOY_USER sudo chmod 0440 /etc/sudoers.d/$DEPLOY_USER fi # Clone or pull latest code if [ -d "$APP_DIR" ]; then echo "Pulling latest code..." sudo -u $DEPLOY_USER git -C $APP_DIR pull origin main # Assuming 'main' is your deployment branch else echo "Cloning repository..." sudo -u $DEPLOY_USER git clone $APP_REPO $APP_DIR sudo -u $DEPLOY_USER git -C $APP_DIR checkout main # Or your deployment branch fi # Install dependencies (using Carton as an example) echo "Installing dependencies..." cd $APP_DIR sudo -u $DEPLOY_USER carton install --deployment # Restart application server (e.g., Starman, Plackup) # This command will vary based on your application's setup. # Example for Starman: echo "Restarting application server..." sudo pkill -f 'starman --workers' # Graceful shutdown if possible sleep 2 sudo -u $DEPLOY_USER starman --workers 4 --listen "*:$APP_PORT" --pid /var/run/your_app.pid app.psgi & sleep 5 # Give it time to start echo "Application restarted on $ip:$APP_PORT" EOF if [ $? -ne 0 ]; then echo "Deployment to $ip failed." return 1 fi done return 0 } # Health check function health_check() { local target_ip=$1 local url=$2 local retries=5 local delay=10 echo "Performing health check on $target_ip$url..." for i in $(seq 1 $retries); do if curl -s --head "$target_ip$url" | grep "200 OK" > /dev/null; then echo "Health check passed for $target_ip." return 0 fi echo "Health check attempt $i/$retries failed for $target_ip. Retrying in $delay seconds..." sleep $delay done echo "Health check failed for $target_ip after $retries attempts." return 1 } # --- Main Deployment Logic --- echo "Starting Blue-Green Deployment..." # 1. Provision/Update Infrastructure (if needed) # In a real CI/CD, this might be a separate step or triggered by changes. # For this script, we assume infra exists or is managed separately. # We will update HAProxy config to reflect current Droplets. # 2. Update HAProxy to point to Blue (should already be there) echo "Ensuring HAProxy points to Blue..." update_haproxy # This will re-register existing blue servers # 3. Deploy the new version to the Green environment if ! deploy_to_environment $DIGITALOCEAN_TAG_GREEN "Green"; then echo "Deployment to Green environment failed. Aborting." exit 1 fi # 4. Health Check the Green environment echo "Performing health checks on Green environment..." GREEN_IPS=$(get_droplet_ips $DIGITALOCEAN_TAG_GREEN) ALL_GREEN_HEALTHY=true for ip in $GREEN_IPS; do if ! health_check "http://$ip:$APP_PORT" $HEALTH_CHECK_URL; then ALL_GREEN_HEALTHY=false echo "One or more Green servers failed health checks. Manual intervention required." # In a real scenario, you might rollback or alert here. # For now, we'll proceed but flag the issue. fi done if [ "$ALL_GREEN_HEALTHY" = false ]; then echo "WARNING: Some Green servers failed health checks. Proceeding with caution." # Consider adding a manual confirmation step here. fi # 5. Switch traffic to Green echo "Switching traffic to Green environment..." # This is the critical step. We update HAProxy to route to the Green backend. # We'll temporarily disable the blue backend and enable the green one. # A more sophisticated approach might involve a specific "maintenance" page # or a gradual rollout. # Temporarily remove blue servers from HAProxy config echo "Temporarily disabling Blue backend..." BLUE_SERVERS_TO_REMOVE=$(get_droplet_ips $DIGITALOCEAN_TAG_BLUE) if [ -n "$BLUE_SERVERS_TO_REMOVE" ]; then # This is a simplified approach. A robust solution would parse the existing config. # For now, we'll regenerate the config with *only* green servers. # This assumes the LB is configured to dynamically add/remove based on tags. # If not, we need to explicitly remove blue servers. # For DigitalOcean LB, this is handled by removing the 'env:blue' tag from the LB. # However, we are managing HAProxy directly on a DO Droplet here. # So, we need to modify the HAProxy config. # Let's re-generate the config, but this time, we'll comment out the blue backend # and make the green backend the default. # This requires modifying the update_haproxy function or creating a new one. # --- Simplified Traffic Switch --- # For this script, we'll assume HAProxy is configured to route to 'green_backend' # when 'blue_backend' is empty or unhealthy. A more direct switch involves # modifying the frontend rules or backend server lists. # A more direct switch: Modify frontend to point to green, or remove blue servers. # Let's simulate removing blue servers from the config. echo "Removing Blue servers from HAProxy config..." # This requires a more advanced config manipulation. # For simplicity, let's assume we can reload HAProxy with a config that # prioritizes Green or has Blue servers commented out. # A common pattern is to have a 'maintenance' backend or a specific frontend rule. # For this example, we'll simulate by reloading HAProxy with a config that # effectively switches. The `update_haproxy` function needs to be smarter. # Let's create a specific function for switching switch_to_green() { echo "Switching traffic to Green backend..." local green_ips=$(get_droplet_ips $DIGITALOCEAN_TAG_GREEN) local green_servers="" for ip in $green_ips; do green_servers+="server green_app_${ip//./_} $ip:$APP_PORT check\n" done # Create a temporary HAProxy config with ONLY green servers active cat << EOF > /tmp/haproxy_green.cfg global log /dev/log local0 log /dev/log local1 notice chroot /var/lib/haproxy stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners stats timeout 30s user haproxy group haproxy daemon defaults log global mode http option httplog option dontlognull timeout connect 5000 timeout client 50000 timeout server 50000 errorfile 400 /etc/haproxy/errors/400.http errorfile 403 /etc/haproxy/errors/403.http errorfile 408 /etc/haproxy/errors/408.http errorfile 500 /etc/haproxy/errors/500.http errorfile 502 /etc/haproxy/errors/502.http errorfile 503 /etc/haproxy/errors/503.http errorfile 504 /etc/haproxy/errors/504.http frontend http_frontend bind *:80 bind *:443 ssl crt /etc/ssl/private/your_domain.pem mode http default_backend green_backend # Traffic now goes to Green backend blue_backend # This backend is now empty mode http balance roundrobin backend green_backend mode http balance roundrobin $green_servers listen stats bind *:8404 mode http stats enable stats uri /stats stats realm Haproxy\ Statistics stats auth admin:YourSecurePassword stats admin if TRUE EOF if haproxy -c -f /tmp/haproxy_green.cfg; then sudo mv /tmp/haproxy_green.cfg /etc/haproxy/haproxy.cfg sudo systemctl reload haproxy echo "HAProxy switched to Green backend." else echo "HAProxy configuration validation failed during switch. Aborting." exit 1 fi } switch_to_green fi # 6. Post-deployment validation (optional but recommended) echo "Performing final validation on Green environment..." GREEN_IPS=$(get_droplet_ips $DIGITALOCEAN_TAG_GREEN) ALL_GREEN_FINAL_HEALTHY=true for ip in $GREEN_IPS; do if ! health_check "http://$ip:$APP_PORT" $HEALTH_CHECK_URL; then ALL_GREEN_FINAL_HEALTHY=false echo "Final health check failed for $ip." fi done if [ "$ALL_GREEN_FINAL_HEALTHY" = false ]; then echo "CRITICAL: Final health checks failed after traffic switch. Manual rollback required!" # Implement rollback logic here: switch traffic back to Blue. exit 1 fi echo "Deployment to Green environment successful and traffic switched!" # 7. Cleanup Blue environment (optional, can be done later) echo "Blue environment is now idle. Consider decommissioning or preparing for next deployment." # You might want to keep it running for a rollback window. echo "Deployment pipeline finished."
Explanation of the Deployment Script:
- `get_droplet_ips`: Fetches the public IPs of Droplets tagged with a specific environment (e.g.,
env:blue). - `update_haproxy`: This function is crucial. It dynamically generates an HAProxy configuration file based on the current IPs of Blue and Green Droplets. It then validates and reloads HAProxy. In a real-world scenario, you’d likely use a templating engine (like Jinja2) and manage the HAProxy configuration more formally.
- `deploy_to_environment`: Iterates through the Droplets of the target environment (initially Green). For each Droplet, it SSHes in, pulls the latest code, installs dependencies (using Carton as an example), and restarts the application server.
- `health_check`: A simple utility to poll a health check endpoint on a given server.
- Main Logic:
- Ensures HAProxy is configured for Blue (initial state).
- Deploys the new version to the Green environment.
- Performs health checks on the Green servers.
- Switches Traffic: This is the core of the zero-downtime. The script simulates switching by reloading HAProxy with a configuration that directs all traffic to the Green backend. The `switch_to_green` function demonstrates this by creating a new config with only Green servers active.
- Performs final validation on the Green environment.
- Marks the Blue environment as idle.
Automating the Pipeline
This deployment script can be integrated into a CI/CD platform like GitLab CI, GitHub Actions, Jenkins, or a custom solution. The trigger could be a Git push to the main branch, a tag creation, or a manual trigger.
Example: GitHub Actions Workflow (`.github/workflows/deploy.yml`)
name: Deploy Perl App
on:
push:
branches:
- main # Or your deployment branch
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up SSH
uses: webfactory/[email protected]
with:
ssh-private-key: ${{ secrets.DIGITALOCEAN_SSH_PRIVATE_KEY }}
- name: Configure doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
- name: Run Deployment Script
env:
APP_REPO: [email protected]:${{ github.repository }}.git # Adjust if repo is different
APP_DIR: /opt/your_perl_app
DEPLOY_USER: deploy
SSH_USER: root
DIGITALOCEAN_TAG_BLUE: env:blue
DIGITALOCEAN_TAG_GREEN: env:green
APP_PORT: 8080
HEALTH_CHECK_URL: /health
# Ensure your DigitalOcean Droplets have the correct SSH key associated
# and that the SSH_USER has passwordless sudo access for the deploy user.
run: |
# Download the deploy.sh script or include it directly
wget https://raw.githubusercontent.com/your_user/your_repo/main/deploy.sh -O deploy.sh
chmod +x deploy.sh
./deploy.sh
Secrets Required for GitHub Actions:
DIGITALOCEAN_TOKEN: Your DigitalOcean API token with read/write permissions.DIGITALOCEAN_SSH_PRIVATE_KEY: The private SSH key corresponding to the public key added to your DigitalOcean account and Droplets.
Rollback Strategy
In case of critical failures after the traffic switch, a rollback is essential. The simplest rollback is to switch traffic back to the Blue environment. This can be achieved by re-running a modified version of the `switch_to_green` function, but configured to point back to the Blue backend.
Rollback Script Snippet (`rollback.sh`)
#!/bin/bash
# ... (similar setup as deploy.sh) ...
# --- Rollback Function ---
switch_to_blue() {
echo "Switching traffic back to Blue backend..."
local blue_ips=$(get_droplet_ips $DIGITALOCEAN_TAG_BLUE)
local blue_servers=""
for ip in $blue_ips; do
blue_servers+="server blue_app_${ip//./_} $ip:$APP_PORT check\n"
done
# Create a temporary HAProxy config with ONLY blue servers active
cat << EOF > /tmp/haproxy_blue.cfg
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend http_frontend
bind *:80
bind *:443 ssl crt /etc/ssl/private/your_domain.pem
mode http
default_backend blue_backend # Traffic now goes to Blue
backend blue_backend
mode http
balance roundrobin
$blue_servers
backend green_backend # This backend is now empty
mode http
balance roundrobin
listen stats
bind *:8404
mode http
stats enable
stats uri /stats
stats realm Haproxy\ Statistics
stats auth admin:YourSecurePassword
stats admin if TRUE
EOF
if haproxy -c -f /tmp/haproxy_blue.cfg; then
sudo mv /tmp/haproxy_blue.cfg /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
echo "HAProxy switched back to Blue backend."
else
echo "HAProxy configuration validation failed during rollback. Manual intervention required!"
exit 1
fi
}
# Execute rollback
switch_to_blue
echo "Rollback complete. Traffic is now directed to the Blue environment."
This rollback script can be triggered manually or automatically by monitoring tools that detect failures in the Green environment post-deployment.
Considerations for Stateful Applications
If your Perl application is stateful (e.g., relies on in-memory session data, local caches, or database connections that require specific handling), Blue-Green deployments become more complex. Strategies include:
- Shared State Storage: Use external services like Redis, Memcached, or a robust database for session management and caching, accessible by both Blue and Green environments.
- Database Migrations: Ensure database schema changes are backward-compatible. Deploy the new application code that can read both old and new schema versions, then perform schema migrations, and finally deploy the application version that requires the new schema. This is often referred to as “Expand/Contract” pattern for database changes.
- Graceful Shutdown: Ensure your application server (e.g., Starman, Plackup) handles `SIGTERM` gracefully, finishing in-flight requests before exiting. This minimizes disruption during the traffic switch.
Conclusion
Implementing zero-downtime Blue-Green deployments for Perl applications on DigitalOcean is achievable with careful planning and automation. By leveraging tools like doctl for infrastructure, HAProxy for traffic management, and robust scripting for the deployment process, you can significantly reduce deployment risks and improve application availability. Remember to tailor the scripts to your specific application’s needs, dependency management, and operational requirements.