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

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Automating Multi-Region Redundancy for Perl Architectures on DigitalOcean

Automating Multi-Region Redundancy for Perl Architectures on DigitalOcean

Establishing Multi-Region Redundancy for Perl Applications

Achieving robust disaster recovery for Perl-based architectures on DigitalOcean necessitates a strategic approach to multi-region redundancy. This involves not just replicating application code and data, but also ensuring that critical infrastructure components and their configurations are consistently deployed and managed across geographically dispersed data centers. This document outlines a practical, code-driven methodology for automating this process, focusing on infrastructure as code (IaC) principles and leveraging DigitalOcean’s API for seamless orchestration.

Infrastructure as Code with Terraform

Terraform is our chosen tool for defining and provisioning infrastructure. It allows us to declare our desired state in configuration files, which Terraform then translates into actionable API calls to DigitalOcean. This ensures consistency and repeatability across regions.

We’ll define our infrastructure in a modular fashion. A core module will handle common resources like VPCs, firewall rules, and load balancers, while region-specific modules will manage Droplet deployments and their associated configurations.

Core Infrastructure Module (`modules/core/main.tf`)

# modules/core/main.tf

variable "region" {
  description = "The DigitalOcean region for this deployment."
  type        = string
}

variable "project_id" {
  description = "The DigitalOcean Project ID."
  type        = string
}

resource "digitalocean_vpc" "app_vpc" {
  name     = "app-vpc-${var.region}"
  region   = var.region
  ip_range = "10.10.0.0/16"
}

resource "digitalocean_firewall" "app_firewall" {
  name = "app-firewall-${var.region}"
  # Associate with the VPC
  vpc_ids = [digitalocean_vpc.app_vpc.id]

  # Allow SSH from anywhere (consider restricting this in production)
  inbound_rule {
    protocol    = "tcp"
    ports       = "22"
    sources {
      addresses = ["0.0.0.0/0"]
    }
  }

  # Allow HTTP/HTTPS traffic
  inbound_rule {
    protocol    = "tcp"
    ports       = "80"
    sources {
      addresses = ["0.0.0.0/0"]
    }
  }
  inbound_rule {
    protocol    = "tcp"
    ports       = "443"
    sources {
      addresses = ["0.0.0.0/0"]
    }
  }

  # Allow internal traffic within the VPC
  inbound_rule {
    protocol    = "tcp"
    ports       = "all"
    sources {
      vpc_ids = [digitalocean_vpc.app_vpc.id]
    }
  }
  inbound_rule {
    protocol    = "udp"
    ports       = "all"
    sources {
      vpc_ids = [digitalocean_vpc.app_vpc.id]
    }
  }
  inbound_rule {
    protocol    = "icmp"
    ports       = "all"
    sources {
      vpc_ids = [digitalocean_vpc.app_vpc.id]
    }
  }

  # Allow all outbound traffic
  outbound_rule {
    protocol    = "tcp"
    ports       = "all"
    destinations {
      addresses = ["0.0.0.0/0"]
    }
  }
  outbound_rule {
    protocol    = "udp"
    ports       = "all"
    destinations {
      addresses = ["0.0.0.0/0"]
    }
  }
  outbound_rule {
    protocol    = "icmp"
    ports       = "all"
    destinations {
      addresses = ["0.0.0.0/0"]
    }
  }
}

resource "digitalocean_loadbalancer" "app_lb" {
  name     = "app-lb-${var.region}"
  region   = var.region
  project  = var.project_id
  vpc_uuid = digitalocean_vpc.app_vpc.id

  forwarding_rule {
    entry_protocol    = "http"
    entry_port        = 80
    target_protocol   = "http"
    target_port       = 8080 # Assuming your Perl app listens on 8080
    healthcheck {
      port     = 8080
      path     = "/"
      protocol = "http"
    }
  }

  # Add HTTPS forwarding rule if SSL is terminated at the LB
  # forwarding_rule {
  #   entry_protocol    = "https"
  #   entry_port        = 443
  #   target_protocol   = "http"
  #   target_port       = 8080
  #   certificate       = "your-ssl-certificate-id" # Replace with actual certificate ID
  #   private_key       = "your-ssl-private-key-id" # Replace with actual private key ID
  #   healthcheck {
  #     port     = 8080
  #     path     = "/"
  #     protocol = "http"
  #   }
  # }
}

Region-Specific Application Module (`modules/app/main.tf`)

# modules/app/main.tf

variable "region" {
  description = "The DigitalOcean region for this deployment."
  type        = string
}

variable "project_id" {
  description = "The DigitalOcean Project ID."
  type        = string
}

variable "vpc_id" {
  description = "The ID of the VPC to deploy Droplets into."
  type        = string
}

variable "firewall_id" {
  description = "The ID of the firewall to associate Droplets with."
  type        = string
}

variable "loadbalancer_id" {
  description = "The ID of the load balancer to add Droplets to."
  type        = string
}

variable "droplet_count" {
  description = "Number of application Droplets to deploy."
  type        = number
  default     = 2
}

variable "droplet_size" {
  description = "The size slug for the Droplets."
  type        = string
  default     = "s-2vcpu-4gb"
}

variable "droplet_image" {
  description = "The image slug for the Droplets."
  type        = string
  default     = "ubuntu-22-04-x64"
}

variable "ssh_keys" {
  description = "List of SSH key slugs to enable for Droplet access."
  type        = list(string)
}

variable "app_version" {
  description = "The version of the Perl application to deploy."
  type        = string
}

resource "digitalocean_droplet" "app_server" {
  count              = var.droplet_count
  name               = "app-${var.region}-${count.index + 1}"
  region             = var.region
  size               = var.droplet_size
  image              = var.droplet_image
  vpc_uuid           = var.vpc_id
  ssh_keys           = var.ssh_keys
  monitoring         = true
  project            = var.project_id
  tags               = ["app", "perl", var.region, "v${var.app_version}"]

  # User data for initial setup and application deployment
  user_data = templatefile("${path.module}/cloud-init.yaml", {
    app_version = var.app_version
  })

  # Ensure firewall is created before Droplets are provisioned
  depends_on = [digitalocean_firewall.app_firewall]
}

resource "digitalocean_loadbalancer_droplet_mapping" "app_lb_mapping" {
  loadbalancer_id = var.loadbalancer_id
  droplet_ids     = digitalocean_droplet.app_server[*].id
}

# Associate Droplets with the firewall
resource "digitalocean_firewall_droplet_association" "app_droplet_firewall" {
  droplet_ids = digitalocean_droplet.app_server[*].id
  firewall_id = var.firewall_id
}

Cloud-Init for Application Deployment (`modules/app/cloud-init.yaml`)

# modules/app/cloud-init.yaml
#cloud-config
package_update: true
packages:
  - perl
  - cpanminus
  - nginx # Or your preferred web server
  - git

run_commands:
  - |
    # Install necessary Perl modules
    cpanm --notest --no-dry-run DBI DBD::mysql # Example: MySQL driver
    cpanm --notest --no-dry-run Mojolicious # Example: Web framework
    # Add other required modules here

  - |
    # Clone or download your Perl application
    # For simplicity, we'll assume a git repository
    git clone --branch v${app_version} https://your-git-repo.com/your-app.git /opt/your-app
    cd /opt/your-app
    # Install application-specific dependencies if any
    # cpanm --installdeps .

  - |
    # Configure your web server (e.g., Nginx)
    # This is a simplified example. You'll need to adapt it.
    cat <<EOF > /etc/nginx/sites-available/your-app
    server {
        listen 8080;
        server_name _;

        location / {
            # Proxy pass to your Perl application's actual listener
            # This assumes your Perl app is running via a FastCGI or similar setup
            # For Mojolicious, you might run it directly or via a PSGI server
            proxy_pass http://127.0.0.1:3000; # Example for a PSGI app
            proxy_set_header Host \$host;
            proxy_set_header X-Real-IP \$remote_addr;
            proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto \$scheme;
        }
    }
    EOF
    ln -sf /etc/nginx/sites-available/your-app /etc/nginx/sites-enabled/
    rm -f /etc/nginx/sites-enabled/default
    systemctl restart nginx

  - |
    # Start your Perl application (example using systemd)
    # You'll need to create a systemd service file for your application
    # Example: /etc/systemd/system/your-app.service
    # [Unit]
    # Description=Your Perl Application
    # After=network.target nginx.service
    #
    # [Service]
    # User=www-data
    # Group=www-data
    # WorkingDirectory=/opt/your-app
    # ExecStart=/usr/bin/perl /opt/your-app/your_app_entry_point.pl
    # Restart=always
    #
    # [Install]
    # WantedBy=multi-user.target

    # For demonstration, we'll just ensure it's running if a service file exists
    if systemctl list-unit-files | grep -q "your-app.service"; then
        systemctl enable your-app
        systemctl start your-app
    else
        echo "Systemd service file for your-app.service not found. Manual startup required."
    fi

# Ensure SSH keys are added for access
ssh_authorized_keys:
  # Add your public SSH keys here, or manage them via Terraform variables
  # - ssh-rsa AAAAB3NzaC1yc2EAAA... [email protected]

Root Terraform Configuration (`main.tf`)

# main.tf

provider "digitalocean" {
  token = var.do_token
}

variable "do_token" {
  description = "DigitalOcean API Token."
  type        = string
  sensitive   = true
}

variable "ssh_key_fingerprints" {
  description = "List of SSH key fingerprints for Droplet access."
  type        = list(string)
}

# Fetch SSH key slugs from fingerprints
data "digitalocean_ssh_key" "deployer" {
  count = length(var.ssh_key_fingerprints)
  fingerprint = var.ssh_key_fingerprints[count.index]
}

locals {
  regions = ["nyc3", "ams3"] # Example regions
  app_version = "1.2.0" # Pin your application version
}

module "core_nyc3" {
  source     = "./modules/core"
  region     = local.regions[0]
  project_id = "your-do-project-id" # Replace with your Project ID
}

module "app_nyc3" {
  source          = "./modules/app"
  region          = local.regions[0]
  project_id      = "your-do-project-id" # Replace with your Project ID
  vpc_id          = module.core_nyc3.app_vpc.id
  firewall_id     = module.core_nyc3.app_firewall.id
  loadbalancer_id = module.core_nyc3.app_lb.id
  ssh_keys        = data.digitalocean_ssh_key.deployer[*].slug
  app_version     = local.app_version
}

module "core_ams3" {
  source     = "./modules/core"
  region     = local.regions[1]
  project_id = "your-do-project-id" # Replace with your Project ID
}

module "app_ams3" {
  source          = "./modules/app"
  region          = local.regions[1]
  project_id      = "your-do-project-id" # Replace with your Project ID
  vpc_id          = module.core_ams3.app_vpc.id
  firewall_id     = module.core_ams3.app_firewall.id
  loadbalancer_id = module.core_ams3.app_lb.id
  ssh_keys        = data.digitalocean_ssh_key.deployer[*].slug
  app_version     = local.app_version
}

output "loadbalancer_ips" {
  description = "Public IPs of the load balancers in each region."
  value = {
    (local.regions[0]) = module.core_nyc3.app_lb.ip
    (local.regions[1]) = module.core_ams3.app_lb.ip
  }
}

Deployment Workflow

To deploy this infrastructure:

  • Initialize Terraform: Run terraform init in the root directory.
  • Review the plan: Execute terraform plan -var="do_token=YOUR_DO_TOKEN" -var='ssh_key_fingerprints=["YOUR_SSH_KEY_FINGERPRINT_1", "YOUR_SSH_KEY_FINGERPRINT_2"]' to see the resources that will be created.
  • Apply the configuration: Run terraform apply -var="do_token=YOUR_DO_TOKEN" -var='ssh_key_fingerprints=["YOUR_SSH_KEY_FINGERPRINT_1", "YOUR_SSH_KEY_FINGERPRINT_2"]' to provision the infrastructure in both regions.

Data Replication and Synchronization

For a stateful Perl application, data consistency across regions is paramount. This typically involves a database. For multi-region redundancy, we need a strategy for replicating database changes.

Database Replication Strategies

DigitalOcean Managed Databases offer built-in read replicas, which can be a good starting point. However, for true active-active or active-passive multi-region setups, more advanced solutions are often required.

Option 1: Asynchronous Replication (Primary-Replica)

This is the most common setup. A primary database in one region handles all writes, and changes are asynchronously replicated to read replicas in other regions. In a disaster scenario, a replica can be promoted to primary.

Perl Application Considerations: Your Perl application will need logic to connect to the primary for writes and can use read replicas for reads. Connection strings must be dynamically updated during a failover event.

Option 2: Multi-Master Replication (Complex)

Some databases support multi-master replication, allowing writes to any node. This is significantly more complex to manage, especially regarding conflict resolution. For most Perl applications, this is overkill unless strict low-latency writes across regions are essential.

Option 3: Application-Level Replication

For specific data types or if your database doesn’t support robust multi-region replication, you might implement replication at the application level. This could involve a background Perl script that tails transaction logs or polls for changes and applies them to other regions.

# Example: Basic application-level replication logic (simplified)
# This would run as a separate daemon or cron job.

use DBI;
use strict;
use warnings;

my $primary_dsn = "dbi:mysql:database=your_db;host=primary-db-host;port=3306";
my $primary_user = "replication_user";
my $primary_pass = "your_password";

my $replica_dsn = "dbi:mysql:database=your_db;host=replica-db-host;port=3306";
my $replica_user = "replication_user";
my $replica_pass = "your_password";

# Track the last processed event ID or timestamp
my $last_processed_id = 0;

sub get_recent_changes {
    my ($dbh) = @_;
    my $sth = $dbh->prepare("SELECT id, data, timestamp FROM changes WHERE id > ? ORDER BY id ASC");
    $sth->execute($last_processed_id);
    my @changes;
    while (my $row = $sth->fetchrow_hashref) {
        push @changes, $row;
    }
    return @changes;
}

sub apply_change {
    my ($dbh, $change) = @_;
    # This is where you'd construct and execute the INSERT/UPDATE/DELETE statement
    # based on the $change->{data}
    my $sth = $dbh->prepare("INSERT INTO your_table (id, data_column, updated_at) VALUES (?, ?, ?)");
    $sth->execute($change->{id}, $change->{data}, $change->{timestamp});
    return $dbh->commit;
}

# Main loop
while (1) {
    my $primary_dbh = DBI->connect($primary_dsn, $primary_user, $primary_pass, { RaiseError => 1, AutoCommit => 0 });
    my $replica_dbh = DBI->connect($replica_dsn, $replica_user, $replica_pass, { RaiseError => 1, AutoCommit => 0 });

    my @changes = get_recent_changes($primary_dbh);

    foreach my $change (@changes) {
        if (apply_change($replica_dbh, $change)) {
            $last_processed_id = $change->{id};
            $replica_dbh->commit;
            print "Applied change ID: $last_processed_id\n";
        } else {
            $replica_dbh->rollback;
            warn "Failed to apply change ID: $change->{id}. Retrying later.\n";
            # Implement retry logic or error alerting
        }
    }

    $primary_dbh->disconnect;
    $replica_dbh->disconnect;

    sleep(60); # Poll every minute
}

Automated Failover and Health Checks

Manual failover is prone to human error and delays. Automation is key for effective disaster recovery.

Health Check Implementation

Your Perl application should expose a health check endpoint (e.g., /health). This endpoint should:

  • Check database connectivity and basic query success.
  • Verify essential external service dependencies.
  • Return an HTTP 200 OK for healthy, and a non-200 status code (e.g., 503 Service Unavailable) for unhealthy.
# Example /health endpoint in a Mojolicious application

package YourApp::Controller::Base;
use Mojo::Base 'Mojolicious::Controller';

sub health {
    my $self = shift;

    # Check database connection
    my $db_healthy = eval {
        my $dbh = $self->app->db->dbh; # Assuming you have a DB connection pool
        $dbh->ping;
        return 1;
    };

    unless ($db_healthy) {
        $self->render(text => "Database connection failed", status => 503);
        return;
    }

    # Add checks for other critical services here...

    $self->render(text => "OK", status => 200);
}
1;

Orchestrating Failover

A separate orchestration service or script is needed to monitor health checks across regions and initiate failover.

This could be:

  • A custom daemon written in Perl, Python, or Go.
  • A managed service like AWS Route 53 (if using AWS) or a similar DNS-based failover solution.
  • A CI/CD pipeline triggered by alerts.

The orchestrator would periodically poll the health check endpoints of the load balancers in each region. If the primary region’s health check fails consistently, the orchestrator would:

  • Attempt to promote a read replica in a secondary region to a primary (if using managed databases).
  • Update DNS records (if using a global DNS service) to point traffic to the healthy secondary region’s load balancer.
  • Trigger re-configuration of application instances to point to the new primary database.

DNS-Based Failover Example (Conceptual)

While DigitalOcean doesn’t have a direct equivalent to Route 53’s health checks for DNS records, you can achieve similar results by using an external monitoring service that updates DNS records via the DigitalOcean API.

#!/bin/bash

# This script would be run by an external monitoring service (e.g., UptimeRobot, custom script on another cloud)
# It checks the health of each region's load balancer and updates DNS.

PRIMARY_REGION="nyc3"
SECONDARY_REGION="ams3"
DO_API_TOKEN="YOUR_DO_API_TOKEN"
DNS_ZONE_ID="YOUR_DNS_ZONE_ID" # e.g., yourdomain.com
RECORD_NAME="@" # For the root domain

# Function to check health of a region's load balancer
check_region_health() {
    local region_lb_ip=$1
    # Use curl to hit the health check endpoint on the load balancer
    # Adjust timeout and expected response as needed
    curl --connect-timeout 5 --max-time 10 "http://${region_lb_ip}/health" > /dev/null 2>&1
    return $?
}

# Get current DNS record IP
CURRENT_IP=$(curl -X GET "https://api.digitalocean.com/v2/domains/${DNS_ZONE_ID}/records?name=${RECORD_NAME}&type=A" \
    -H "Authorization: Bearer ${DO_API_TOKEN}" | jq -r '.domain_records[0].data')

# Get load balancer IPs from Terraform output or DO API
# For simplicity, hardcoding here, but ideally fetch dynamically
NYC3_LB_IP="YOUR_NYC3_LB_IP"
AMS3_LB_IP="YOUR_AMS3_LB_IP"

# Check primary region health
if check_region_health $NYC3_LB_IP; then
    echo "Primary region ($PRIMARY_REGION) is healthy."
    if [ "$CURRENT_IP" != "$NYC3_LB_IP" ]; then
        echo "Updating DNS to point to primary region ($NYC3_LB_IP)..."
        # Use DO API to update the A record
        # This requires constructing a JSON payload for the PUT request
        # Example using jq and curl (simplified):
        RECORD_ID=$(curl -X GET "https://api.digitalocean.com/v2/domains/${DNS_ZONE_ID}/records?name=${RECORD_NAME}&type=A" \
            -H "Authorization: Bearer ${DO_API_TOKEN}" | jq -r '.domain_records[0].id')

        curl -X PUT "https://api.digitalocean.com/v2/domains/${DNS_ZONE_ID}/records/${RECORD_ID}" \
            -H "Authorization: Bearer ${DO_API_TOKEN}" \
            -H "Content-Type: application/json" \
            -d "{\"type\":\"A\",\"name\":\"${RECORD_NAME}\",\"data\":\"${NYC3_LB_IP}\",\"ttl\":300}"
        echo "DNS updated."
    fi
else
    echo "Primary region ($PRIMARY_REGION) is unhealthy. Checking secondary region..."
    if check_region_health $AMS3_LB_IP; then
        echo "Secondary region ($SECONDARY_REGION) is healthy."
        if [ "$CURRENT_IP" != "$AMS3_LB_IP" ]; then
            echo "Updating DNS to point to secondary region ($AMS3_LB_IP)..."
            RECORD_ID=$(curl -X GET "https://api.digitalocean.com/v2/domains/${DNS_ZONE_ID}/records?name=${RECORD_NAME}&type=A" \
                -H "Authorization: Bearer ${DO_API_TOKEN}" | jq -r '.domain_records[0].id')

            curl -X PUT "https://api.digitalocean.com/v2/domains/${DNS_ZONE_ID}/records/${RECORD_ID}" \
                -H "Authorization: Bearer ${DO_API_TOKEN}" \
                -H "Content-Type: application/json" \
                -d "{\"type\":\"A\",\"name\":\"${RECORD_NAME}\",\"data\":\"${AMS3_LB_IP}\",\"ttl\":300}"
            echo "DNS updated."
        fi
    else
        echo "Both regions are unhealthy. Manual intervention required."
        # Trigger alerts here
    fi
fi

Monitoring and Alerting

Comprehensive monitoring is crucial. Beyond application health checks, monitor:

  • DigitalOcean Droplet metrics (CPU, RAM, Disk I/O, Network).
  • Load Balancer metrics (latency, error rates, connection counts).
  • Database performance and replication lag.
  • Application logs for errors and warnings.

Configure alerts for any metric that deviates from acceptable thresholds, especially for database replication lag and application error rates. Tools like Prometheus with Alertmanager, Datadog, or DigitalOcean’s own monitoring can be integrated.

Conclusion

Automating multi-region redundancy for Perl architectures on DigitalOcean is an achievable goal through a combination of Infrastructure as Code (Terraform), robust data replication strategies, automated health checks, and proactive monitoring. By codifying your infrastructure and deployment processes, you significantly reduce the risk of manual errors and ensure a faster, more reliable recovery in the event of a regional outage.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Leveraging Laravel Octane and Docker Swarm for High-Performance, Scalable WordPress Headless Deployments
  • Migrating Legacy WordPress to Headless with Laravel: A Performance and Security Deep Dive
  • Leveraging PHP 8’s JIT Compiler and Vector APIs for Extreme Web Application Performance
  • Leveraging PHP 8 JIT and AWS Lambda for High-Performance, Serverless WordPress REST API Backends
  • Beyond the Basics: Leveraging PHP 8.3’s JIT Compiler and Fibers for High-Concurrency Laravel Applications

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (664)
  • Desktop Applications (14)
  • DevOps (11)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (6)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (15)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (10)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (650)
  • SEO & Growth (492)
  • Server (118)
  • Softwares (1)
  • Ubuntu (9)
  • Uncategorized (20)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (26)
  • WordPress Plugin Development (728)
  • WordPress Theme Development (357)

Recent Posts

  • Leveraging Laravel Octane and Docker Swarm for High-Performance, Scalable WordPress Headless Deployments
  • Migrating Legacy WordPress to Headless with Laravel: A Performance and Security Deep Dive
  • Leveraging PHP 8's JIT Compiler and Vector APIs for Extreme Web Application Performance

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (728)
  • Debugging & Troubleshooting (664)
  • Security & Compliance (650)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala