
Running a production database in one cloud is no longer enough for a lot of teams. Regulatory pressure pushes for geographic separation. Architecture reviews flag single-provider dependency. Procurement wants leverage at renewal time. Whatever the driver, the question lands on the database team: can we keep this database alive in a second cloud and if the primary region disappears, can we actually fail over?
It's a fair question, and on paper the answer is well-known: replicate the database. In practice, the path gets narrower fast. Some replication technologies are tightly coupled to managed services that don't exist on the other side of the cloud boundary. Some assume both ends sit inside the same provider's network fabric. Some are gated behind premium tiers your organization hasn't licensed. By the time you've intersected your database, your two clouds, your network topology, and your RPO/RTO targets, the set of supported, blessed solutions can be empty.
That's the situation we recently worked through with a customer running Oracle on AWS, with a standby that needed to land on Azure. The targets a 15-minute RPO and a 1-hour RTO. This post walks through the architecture we landed on, the tradeoffs behind it, and how it's held up in testing.
Cross-cloud replication for Oracle SE2 pushes the design toward an older, more mechanical technique: archive log shipping with continuous recovery on the standby. The approach predates most managed replication services, and it's stayed relevant for exactly the reason it works here it doesn't assume anything about the network fabric, the managed service surface, or the licensing tier underneath.
The approach is straightforward in concept. The primary database generates archive logs as it runs. Those logs are copied to the standby site. A standby database, held in MOUNT state, applies the logs continuously. If the primary fails, the standby is recovered to the last applied log, activated, and opened read-write.
The requirements we were designing against:
Primary on AWS, standby on Azure
RPO of 15 minutes, RTO of 1 hour
No third-party replication tooling
Minimal day-2 operational overhead
The architecture we implemented uses a proven approach: manual archive log shipping combined with continuous recovery on the standby site. While it may not be as automated as Data Guard, it delivers reliable disaster recovery at a fraction of the cost.

Primary Site (AWS)
Oracle database in read-write mode
Local archive log destination
Automated rsync job via cron
Network Layer
Site-to-site IPSec VPN tunnel
AWS Virtual Private Gateway ↔ Azure Virtual Network Gateway
Secure, encrypted connectivity between clouds
Standby Site (Azure)
Oracle SE database provisioned via Tessell
Database in MOUNT mode
Continuous recovery script applying incoming archive logs
The beauty of this setup? Archive logs generated on the primary database in AWS are automatically pushed to Azure every few minutes via rsync over SSH. On the Azure side, a recovery script continuously applies these logs, keeping the standby database synchronized.
For this customer, the DR solution delivers:
RPO (Recovery Point Objective): 15 minutes
RTO (Recovery Time Objective): 1 hour
These metrics strike a balance between data protection and operational practicality for most business-critical applications. In practice, the rsync interval can be tightened if the workload demands a smaller RPO, at the cost of more frequent log switches on the primary.

Before walking through the build steps, it helps to see the moving parts.
Both sides follow the same pattern: a real gateway that terminates the tunnel, a reference object that represents the peer, and a connection resource that pairs them with the pre-shared key.
| Role | AWS | Azure |
|---|---|---|
| Local tunnel endpoint | Virtual Private Gateway (VGW) | Virtual Network Gateway (VNG) |
| Reference to the peer | Customer Gateway (CGW) | Local Network Gateway (LNG) |
| Connection resource | Site-to-Site VPN Connection | VNet Connection |
Active AWS and Azure subscriptions
Non-overlapping CIDR blocks on the two sides
Administrative access to create networking resources in both clouds
| Step | Microsoft Azure | Amazon Web Services (AWS) |
|---|---|---|
| 1. Network Foundation | 1) Create Resource Group. 2) Create a Virtual Network (10.5.*.*/16). 3) Create a Private Subnet (e.g. 10.5.*.*/24) for VMs. 4) Create a Gateway Subnet (e.g. 10.5.*.*/27). Note: this specific name is required for VPN gateways. | 1) Create VPC Environment (10.10.*.*/16). 2) Configure Public Subnets (for NAT/IGW) and Private Subnets (for workloads). 3) Ensure Internet Gateway and NAT Gateways are configured for basic internet access. |
| 2. Provision Gateways | Create Virtual Network Gateway: 1) Search for Virtual Network Gateway. 2) VPN Type: Route-based. 3) Associate it with the VNet created above. 4) Create a new Public IP Address. | Create Virtual Private Gateway (VGW): 1) Create a VGW resource. 2) Select Actions > Attach to VPC and select your VPC. |
| 3. Establish AWS Connection | Wait for the Virtual Network Gateway public IP to be assigned. Once deployed copy the Public IP from the Overview blade. | Create Customer Gateway: 1) Create a Customer Gateway. 2) Input the Azure Public IP. Then create VPN Connection: Site-to-Site VPN Connection with a) Target: Virtual Private Gateway b) Customer Gateway: the one created above c) Static IP Prefix: Azure CIDR (10.5.*.*/16). |
| 4. Exchange Keys | Wait for configuration data. | Download Configuration: 1) Select the VPN Connection. 2) Click Download Configuration with Vendor: Generic / Platform: Generic / Software: Vendor Agnostic. 3) Open the file to find the AWS Tunnel 1 Public IP and the Pre-Shared Key (PSK). |
| 5. Configure Azure Local Gateway | Create Local Network Gateway: 1) Create a Local Network Gateway resource. 2) IP Address: the AWS Tunnel 1 Public IP from the config file. 3) Address Space: the AWS VPC CIDR (10.10.*.*/16). | Wait for Azure configuration. |
| 6. Finalize Connection | Create Connection: 1) Go to the Virtual Network Gateway > Connections. 2) Click Add. 3) Connection Type: Site-to-Site (IPsec). 4) Select the Local Network Gateway created in Step 5. 5) Paste the Pre-Shared Key (PSK) from the AWS config file. | No action required. |
| 7. Routing | Azure typically propagates routes automatically via the Local Network Gateway settings. | Update Route Tables: 1) Go to VPC Route Tables (Public and Private). 2) Add a new route with Destination: 10.5.*.*/16 (Azure CIDR) and Target: Virtual Private Gateway. |
The tunnel itself doesn't grant traffic — you still need to allow it. Between the two database private IPs, open:
TCP 22 for rsync over SSH
TCP 1521 for the Oracle listener (used by applications during and after failover)
Keep the allow lists scoped to the specific private IPs, not the full CIDRs.
With the network in place, the database work is the familiar physical standby pattern.
Provision the standby. Stand up an Oracle instance on Azure with the same database name, version, and patch level as the primary. We provisioned through Tessell's control plane, which made matching the AWS configuration a parameter rather than a manual checklist (more on that below).
Establish passwordless SSH between the two database hosts, using key-based authentication for the Oracle OS user. This is what rsync will use.
Configure the primary for archive log mode, with a local archive destination sized for at least a day of log generation plus a buffer.
Prepare the standby as a physical standby restore from a primary backup, mount in standby mode, and confirm it can apply a manually-shipped log before automating.
Deploy the automation: the log-shipping cron on the primary and the recovery loop on the standby.
Archive log shipping (primary, AWS). A cron job runs every 5 minutes. It rsyncs new files from the local archive destination to the standby host over SSH. rsync's delta transfer means re-runs are cheap, and partial transfers are resumed cleanly.
Archive log recovery (standby, Azure). A loop on the standby host watches the incoming archive directory and applies new logs to the mounted standby database. The database stays in recovery mode throughout — it's never opened.
Keep both scripts idempotent and log their actions. The single most useful thing during a real failover is being able to read, at a glance, which log was last shipped and which was last applied.
A log-shipping pipeline fails quietly: the cron stops, a disk fills, an SSH key expires — and the standby drifts without anyone noticing. Two checks close that gap:
Apply-lag alert. Compare the highest archived sequence on the primary against the last applied sequence on the standby. Alert if the gap exceeds a threshold (we alert past two shipping intervals).
Heartbeat alert. Alert if no new log has been applied on the standby within N minutes, independent of the primary-side view this catches the case where the primary-side monitoring is down too.
Both checks are a handful of lines of SQL and shell, and they are the difference between a DR site and a false sense of one.
A region-level AWS outage triggers a defined sequence. None of the steps are reversible without a rebuild, so the runbook should be followed deliberately, not improvised.
| Role | AWS | Azure |
|---|---|---|
| Local tunnel endpoint | Virtual Private Gateway (VGW) | Virtual Network Gateway (VNG) |
| Reference to the peer | Customer Gateway (CGW) | Local Network Gateway (LNG) |
| Connection resource | Site-to-Site VPN Connection | VNet Connection |
For most production applications, the full sequence completes well inside the 1-hour RTO. The variable is step 4 how quickly application configuration can be updated and how quickly connection pools drain and reconnect.
A planned failback to AWS is a separate exercise: rebuild the original primary as a fresh standby of the now-promoted Azure database, let it catch up, then run the failover in reverse.
Most of what's described above is mechanical: configure a tunnel, ship logs, apply logs. The piece that scales poorly by hand is keeping the standby's configuration version, patches, parameters, storage layout identical to the primary's, in a different cloud, over time.
Provisioning the Azure standby through Tessell's control plane collapsed that work into a single parameterized deploy. The standby came up on Azure with the same Oracle version, the same patch level, and a matching storage layout as the AWS primary, because both were described by the same configuration rather than two parallel manual builds.
Beyond the initial provisioning, the same control plane handles automated backups and point-in-time recovery on the standby. That matters more than it sounds: a physical standby protects against site loss, but it doesn't protect against logical corruption a bad DELETE on the primary becomes a bad DELETE on the standby a few minutes later. PITR on the Azure side gives a second, independent recovery path for exactly that case.
The two databases also show up in a single inventory rather than as two independent installations, which makes drift between them visible.