• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Disaster Recovery 101: Architecting Auto-Failovers for DynamoDB and Python Deployments on Linode

Disaster Recovery 101: Architecting Auto-Failovers for DynamoDB and Python Deployments on Linode

Establishing a Multi-Region DynamoDB Strategy

For applications demanding high availability and resilience, a multi-region DynamoDB setup is non-negotiable. This involves replicating your DynamoDB tables across geographically distinct AWS regions. While DynamoDB Global Tables offer a managed solution, for a more granular control and to integrate with specific cloud providers like Linode, we’ll architect a custom replication mechanism. This approach allows for independent control over data residency and failover processes.

The core idea is to leverage DynamoDB Streams to capture item-level changes and then process these changes to replicate them to a secondary DynamoDB table in a different region. This can be achieved using AWS Lambda functions triggered by the stream. For this example, we’ll assume a primary region (e.g., `us-east-1`) and a secondary region (e.g., `eu-west-1`).

DynamoDB Streams and Lambda for Cross-Region Replication

First, ensure DynamoDB Streams are enabled on your primary table. Set the stream view type to NEW_AND_OLD_IMAGES to capture all necessary data for replication.

Lambda Function for Replication

This Python Lambda function will be triggered by the DynamoDB stream. It will process batch records and write them to the secondary table. Ensure the Lambda function has IAM permissions to read from the DynamoDB stream and write to the secondary DynamoDB table in the other region.

The following Python code demonstrates the core logic. Note that error handling, batching optimizations, and dead-letter queue configurations are crucial for production readiness but are omitted here for brevity.

import boto3
import json
import os

# Initialize DynamoDB clients for both regions
primary_region = os.environ.get('PRIMARY_REGION', 'us-east-1')
secondary_region = os.environ.get('SECONDARY_REGION', 'eu-west-1')
primary_table_name = os.environ.get('PRIMARY_TABLE_NAME')
secondary_table_name = os.environ.get('SECONDARY_TABLE_NAME')

# Ensure table names are set
if not all([primary_table_name, secondary_table_name]):
    raise ValueError("PRIMARY_TABLE_NAME and SECONDARY_TABLE_NAME environment variables must be set.")

dynamodb_primary = boto3.resource('dynamodb', region_name=primary_region)
dynamodb_secondary = boto3.resource('dynamodb', region_name=secondary_region)

table_primary = dynamodb_primary.Table(primary_table_name)
table_secondary = dynamodb_secondary.Table(secondary_table_name)

def lambda_handler(event, context):
    for record in event['Records']:
        if record['eventSource'] == 'aws:dynamodb':
            event_name = record['eventName']
            new_image = record.get('dynamodb', {}).get('NewImage')
            old_image = record.get('dynamodb', {}).get('OldImage')

            if event_name == 'INSERT' or event_name == 'MODIFY':
                # For INSERT and MODIFY, write the new image to the secondary table
                try:
                    table_secondary.put_item(Item=new_image)
                    print(f"Successfully replicated INSERT/MODIFY for item: {new_image.get('id')}") # Assuming 'id' is the partition key
                except Exception as e:
                    print(f"Error replicating INSERT/MODIFY for item {new_image.get('id')}: {e}")
                    # Implement retry logic or send to DLQ

            elif event_name == 'REMOVE':
                # For REMOVE, delete the item from the secondary table
                # We need the primary key to delete. Assuming 'id' is the partition key.
                # If there's a sort key, it needs to be included.
                primary_key = {}
                if 'Keys' in record['dynamodb']:
                    for key_name, key_value in record['dynamodb']['Keys'].items():
                        primary_key[key_name] = key_value

                if primary_key:
                    try:
                        table_secondary.delete_item(Key=primary_key)
                        print(f"Successfully replicated REMOVE for item: {primary_key}")
                    except Exception as e:
                        print(f"Error replicating REMOVE for item {primary_key}: {e}")
                        # Implement retry logic or send to DLQ
                else:
                    print(f"Could not determine primary key for REMOVE event: {record}")
    return {
        'statusCode': 200,
        'body': json.dumps('Replication process completed.')
    }

To deploy this, you’ll need to create a Lambda function in your primary region, configure its environment variables (PRIMARY_TABLE_NAME, SECONDARY_TABLE_NAME, PRIMARY_REGION, SECONDARY_REGION), and attach an IAM role with the necessary permissions. Then, create a DynamoDB trigger for this Lambda function on your primary table’s stream.

Architecting Python Deployments on Linode for High Availability

For Python applications deployed on Linode, achieving high availability involves several layers: load balancing, redundant application instances, and robust data storage. We’ll focus on setting up a basic auto-failover mechanism for your Python application instances across multiple Linode instances.

Load Balancing with HAProxy

HAProxy is an excellent choice for load balancing. We’ll configure it to distribute traffic across multiple Python application servers. For auto-failover, HAProxy can monitor the health of backend servers and automatically remove unhealthy ones from the rotation.

Consider a setup with at least two Linode instances for your application servers and one dedicated Linode instance for HAProxy. For true redundancy, you’d ideally have a highly available HAProxy setup (e.g., using Keepalived for VIP failover), but for simplicity, we’ll focus on HAProxy’s built-in health checks.

HAProxy Configuration Example

# /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
    acl is_api path_beg /api
    use_backend api_servers if is_api
    default_backend web_servers

backend web_servers
    balance roundrobin
    option httpchk GET /healthz HTTP/1.1\r\nHost:\ localhost
    server web1 192.168.1.10:8000 check
    server web2 192.168.1.11:8000 check
    server web3 192.168.1.12:8000 check

backend api_servers
    balance roundrobin
    option httpchk GET /api/health HTTP/1.1\r\nHost:\ localhost
    server api1 192.168.1.20:8001 check
    server api2 192.168.1.21:8001 check

In this configuration:

  • web_servers and api_servers are backend pools for different types of application endpoints.
  • balance roundrobin distributes requests evenly.
  • option httpchk defines a health check. HAProxy will send an HTTP GET request to the specified path and expect a 2xx or 3xx status code. If it fails, the server is marked as down.
  • check enables health checking for each server.

Ensure your Python application exposes a health check endpoint (e.g., /healthz for web servers, /api/health for API servers) that returns a 200 OK status code when the application is healthy.

Automated Deployment and Health Checks

To automate deployments and ensure new instances are healthy before being added to the load balancer, consider using a CI/CD pipeline. Tools like Jenkins, GitLab CI, or GitHub Actions can be integrated with Linode’s API to provision new instances, deploy your Python application, and then update HAProxy’s configuration (or use a dynamic configuration approach if HAProxy supports it). A common pattern is to deploy to a new instance, perform health checks, and if successful, add it to the HAProxy backend pool via its admin socket or by reloading the configuration.

Example Health Check Endpoint in Flask

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/healthz')
def health_check():
    # Add checks for database connectivity, external service availability, etc.
    # For this example, we'll assume the app is healthy if it can run.
    return jsonify({"status": "ok"}), 200

if __name__ == '__main__':
    # In production, use a WSGI server like Gunicorn or uWSGI
    # Example: gunicorn -w 4 -b 0.0.0.0:8000 app:app
    app.run(host='0.0.0.0', port=8000)

For API health checks, you might have a different endpoint:

from flask import Flask, jsonify

api_app = Flask(__name__)

@api_app.route('/api/health')
def api_health_check():
    # More specific checks for API services
    return jsonify({"api_status": "operational"}), 200

if __name__ == '__main__':
    # Example: gunicorn -w 4 -b 0.0.0.0:8001 api_app:api_app
    api_app.run(host='0.0.0.0', port=8001)

Implementing Application-Level Failover Logic

While HAProxy handles infrastructure-level failover, your Python application might need to be aware of potential data source failures, especially when interacting with DynamoDB. If your primary DynamoDB region becomes unavailable, your application should ideally attempt to use the secondary region’s DynamoDB table.

DynamoDB Client with Failover Logic

You can wrap your DynamoDB client interactions with try-except blocks that catch specific AWS exceptions (e.g., ClientError with a relevant error code indicating a regional issue) and switch to the secondary region’s client.

import boto3
from botocore.exceptions import ClientError
import os

class DynamoDBManager:
    def __init__(self, primary_region, secondary_region, primary_table_name, secondary_table_name):
        self.primary_region = primary_region
        self.secondary_region = secondary_region
        self.primary_table_name = primary_table_name
        self.secondary_table_name = secondary_table_name

        self.dynamodb_primary = boto3.resource('dynamodb', region_name=primary_region)
        self.dynamodb_secondary = boto3.resource('dynamodb', region_name=secondary_region)

        self.table_primary = self.dynamodb_primary.Table(primary_table_name)
        self.table_secondary = self.dynamodb_secondary.Table(secondary_table_name)

        self.current_region = primary_region # Start with primary

    def _get_table(self):
        if self.current_region == self.primary_region:
            return self.table_primary
        else:
            return self.table_secondary

    def _switch_to_secondary(self):
        print(f"Switching to secondary region: {self.secondary_region}")
        self.current_region = self.secondary_region

    def _switch_to_primary(self):
        print(f"Switching back to primary region: {self.primary_region}")
        self.current_region = self.primary_region

    def get_item(self, key):
        table = self._get_table()
        try:
            response = table.get_item(Key=key)
            return response.get('Item')
        except ClientError as e:
            # Example: Catching a specific error that might indicate regional issues
            # This is a simplification; real-world error code analysis is needed.
            if e.response['Error']['Code'] == 'ProvisionedThroughputExceededException': # Example, might need more specific error
                 print(f"Error accessing {self.current_region}: {e}")
                 if self.current_region == self.primary_region:
                     self._switch_to_secondary()
                     return self.get_item(key) # Retry in secondary region
            else:
                print(f"Unhandled ClientError in {self.current_region}: {e}")
            return None
        except Exception as e:
            print(f"An unexpected error occurred in {self.current_region}: {e}")
            return None

    def put_item(self, item):
        table = self._get_table()
        try:
            response = table.put_item(Item=item)
            # If successful in secondary, attempt to sync back to primary if we were in failover mode
            if self.current_region == self.secondary_region:
                try:
                    self.table_primary.put_item(Item=item)
                    self._switch_to_primary() # Switch back if sync to primary is successful
                except ClientError as primary_e:
                    print(f"Failed to sync item back to primary region after failover: {primary_e}")
                    # Decide on strategy: stay in secondary, retry sync later, etc.
            return response
        except ClientError as e:
            print(f"Error writing to {self.current_region}: {e}")
            if self.current_region == self.primary_region:
                self._switch_to_secondary()
                return self.put_item(item) # Retry in secondary region
            return None
        except Exception as e:
            print(f"An unexpected error occurred during put_item in {self.current_region}: {e}")
            return None

# Example Usage:
# dynamo_manager = DynamoDBManager(
#     primary_region='us-east-1',
#     secondary_region='eu-west-1',
#     primary_table_name='my-app-table-us-east-1',
#     secondary_table_name='my-app-table-eu-west-1'
# )
#
# item_to_get = {'id': 'user123'}
# retrieved_item = dynamo_manager.get_item(item_to_get)
#
# item_to_put = {'id': 'user456', 'data': 'some_value'}
# dynamo_manager.put_item(item_to_put)

This `DynamoDBManager` class attempts to abstract the failover logic. When an error occurs in the primary region, it switches to the secondary region and retries. Crucially, when writing data in the secondary region, it also attempts to write back to the primary region and switch back if successful. This is a simplified strategy; more complex scenarios might involve quorum-based writes or eventual consistency guarantees.

Orchestrating Failover and Recovery

A robust disaster recovery strategy requires automated detection and execution of failover. This can be achieved through a combination of monitoring tools and orchestration scripts.

Monitoring and Alerting

Implement comprehensive monitoring for:

  • Linode instance health (CPU, memory, network, disk I/O).
  • HAProxy status (backend server health, error rates, latency).
  • Application-level metrics (request rates, error rates, response times).
  • DynamoDB metrics (throttled requests, latency, errors) in both regions.

Tools like Prometheus with Alertmanager, Datadog, or New Relic can be used. Configure alerts for critical thresholds. For instance, if HAProxy reports all backend servers for a service as unhealthy for a sustained period, or if DynamoDB in the primary region shows a significant increase in throttled requests or high latency, an alert should be triggered.

Automated Failover Scripts

When an alert indicates a potential disaster, an automated script can initiate the failover. This script could:

  • For Application Instances: If HAProxy health checks are failing, the script could attempt to restart services on the Linode instances. If that fails, it could trigger a new instance deployment in a different Linode region (if using multi-region Linode infrastructure) and update HAProxy.
  • For DynamoDB: The application-level failover logic (as shown in the DynamoDBManager) is the primary mechanism. However, an external script could monitor DynamoDB health and, if a prolonged outage is detected, trigger a notification or even attempt to reconfigure application instances to exclusively use the secondary region if the application logic doesn’t handle it dynamically.

For Linode, you would use the Linode API (via `linode-cli` or direct API calls) to manage instances. For example, to provision a new instance in a different region:

# Example using linode-cli to create a new instance in a different region
linode-cli linode create --type g6-nanode --region us-east --image linode/ubuntu22.04 --label my-app-failover --root-pass "YOUR_SECURE_PASSWORD"

# After provisioning, deploy your application and update HAProxy configuration.
# This part is highly dependent on your deployment automation.

The key is to have a well-defined runbook for disaster scenarios, whether automated or manual, and to test it rigorously. For CTOs and VPs of Engineering, the goal is to minimize Mean Time To Recovery (MTTR) through proactive architecture and automation.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Step-by-Step: Diagnosing indexing lock conflicts and high CPU during bulk stock updates on DigitalOcean Servers
  • How to Debug and Fix memory leaks and socket exhaustion in daemon processes in Modern C++ Applications
  • Infrastructure as Code: Provisioning Secure PHP Clusters on DigitalOcean Using Terraform
  • Fixing Slow Largest Contentful Paint (LCP) caused by unoptimized database queries in Legacy Laravel Codebases Without Breaking API Contracts
  • An Auditor’s Checklist for Securing Laravel Backends on Google Cloud

Copyright © 2026 · Vinay Vengala