Asynchronous Replication of Snapshots from an ActiveCluster Pod to a Third Array

Page content

I’ve been rebuilding my three-site SQL Server demo lab, and I ran into something I’ve wanted for a long time. If you’ve ever designed a SQL Server environment on ActiveCluster, you know the pattern: two FlashArrays running a synchronously replicated pod for zero RPO between sites, and a third array somewhere else for a longer retention, disaster recovery copy. The problem was that you couldn’t get the data to that third array directly from the pod. Protection groups inside a stretched pod simply couldn’t have an array target.

That’s changed, and it’s been possible longer than a lot of us realize. You can create a protection group inside an ActiveCluster pod, add a third FlashArray as a target, and asynchronously replicate snapshots to it on a schedule. Your synchronously replicated data gets a third copy, and you don’t have to build a parallel set of non-pod volumes to make it happen.

In this post, I’m going to show you how to configure this end to end with the Pure Storage PowerShell SDK2, so you can automate it. Let’s go.

The Problem We’re Solving

Here’s the architecture most of us end up wanting for a business critical SQL Server:

  • Zero RPO between two local sites: ActiveCluster keeps a pod synchronously replicated between two FlashArrays. Your SQL Server volumes live in that pod, and both arrays serve I/O.
  • A third, geographically distant copy: Async replication to a third array for regional disaster recovery, long term snapshot retention, or a SafeMode protected copy that lives outside the metro cluster.

Getting both used to mean compromise. Since a protection group in a pod couldn’t target an array, the workarounds were all awkward. You either replicated from outside the pod using non-pod volumes, which defeats the point of ActiveCluster, or you leaned on an offload target, which has different restore characteristics than array to array replication.

Now the pod itself is the replication source. One protection group, inside the pod, with a target array. That’s it.

This configuration has a name, and knowing it makes the documentation much easier to find: Pure calls it Active-Active Asynchronous Replication. The Async Replication Best Practices Guide describes it as using “protection groups inside of ActiveCluster pods configured with an asynchronous replication target and schedule.”

Let me be precise about what changed, because protection groups inside pods aren’t new. They’ve been there all along for managing snapshot schedules on pod volumes. What arrived in a later release is the ability to give one of those protection groups an async replication schedule and a target array. That’s the piece that unlocks the three site design.

What hasn’t changed is the boundary. A protection group inside a pod only protects volumes inside that pod. It can’t span a pod boundary, so you can’t build one group that covers pod volumes and non-pod volumes together.

There’s a second constraint worth knowing before we build, because it shapes the whole design. An ActiveCluster volume can be an async replication source, but it can’t be an async target. Snapshots flow out of the pod to the third array, and nothing flows back into a pod that way. On the third array those snapshots land as ordinary protection group snapshots, so when you recover over there, you’re copying them out to regular volumes, not into a stretched pod. That’s fine for a DR copy, and it’s worth knowing before you sketch a topology that assumes replication into a pod.

My Lab Setup

Here’s what I’m working with. The arrays are running Purity//FA 6.10.6 with SafeMode in use.

Component Name Role
ActiveCluster array 1 flasharray1 Pod member, source of async replication
ActiveCluster array 2 flasharray2 Pod member, synchronous peer
Third array flasharray3 Async replication target
Pod SQL-AC-FTDemo Stretched pod holding the SQL Server volumes
Volume SQL-AC-FTDemo::SQL-ACTIVEC01-FTDemo SQL Server data volume inside the pod
Protection group SQL-AC-FTDemo::ASYNC Lives inside the pod, targets the third array

And here’s the protection design I’m building. It’s the classic pattern of a short local retention with a longer remote retention:

  • Snapshot Schedule: Take a snapshot on the source every 5 minutes, retain each one on the source for 3 days.
  • Replication Schedule: Replicate a snapshot to the target every 5 minutes, retain each one on the target for 2 weeks.

This is me testing in my lab. I went looking for a clean GA release for this exact workflow and couldn’t find one called out. The nearest hard version marker I found is that ActiveCluster over Fibre Channel coexisting with ActiveDR or async replication on the same system is supported on Purity//FA 6.1.3 and later, and Pure’s Async Replication Best Practices Guide has documented Active-Active Asynchronous Replication since 2021. The ActiveCluster FAQ is the place to check exact features, caveats, and version requirements before you design around any of this. Your mileage will certainly vary.

Configuring It with PowerShell

Now let me walk you through the whole build with the Pure Storage PowerShell SDK2. All of the output below is from an actual run in my lab.

Step 0: Connect to Both Arrays

First, let’s define our names and connect. I’m connecting to one of the ActiveCluster member arrays and to the third array, since parts of this configuration have to be performed from each side.

Import-Module PureStoragePowerShellSDK2

# ActiveCluster member array we'll drive the configuration from
$SourceArrayName = 'flasharray1'

# The third array, our async replication target
$TargetArrayName = 'flasharray3'

# Purity object names. The pod prefix is what makes this work.
$PodName    = 'SQL-AC-FTDemo'
$VolumeName = 'SQL-AC-FTDemo::SQL-ACTIVEC01-FTDemo'
$PGroupName = 'SQL-AC-FTDemo::ASYNC'

$Credential = Get-Credential

$SourceFlashArray = Connect-Pfa2Array -Endpoint $SourceArrayName -Credential $Credential -IgnoreCertificateError
$TargetFlashArray = Connect-Pfa2Array -Endpoint $TargetArrayName -Credential $Credential -IgnoreCertificateError

# Purity refers to arrays by their array name, which may differ from the endpoint
# you connected to. Pull the real names so every later step uses the right value.
$SourceArrayShortName = (Get-Pfa2Array -Array $SourceFlashArray).Name
$TargetArrayShortName = (Get-Pfa2Array -Array $TargetFlashArray).Name

"Source array: $SourceArrayShortName"
"Target array: $TargetArrayShortName"
Source array: flasharray1
Target array: flasharray3

That last bit matters more than it looks. Several of the cmdlets coming up want the array’s Purity name, not the endpoint you connected to, and in a lab with DNS aliases those two drift apart quickly.

Step 1: Confirm the Pod Is Stretched

Before building anything, let’s confirm the pod is actually stretched across two arrays. Get-Pfa2PodArray returns the pod and its array members, and note the property you expand is _Member, with the leading underscore.

$PodMembers = Get-Pfa2PodArray -Array $SourceFlashArray -GroupName $PodName |
    Select-Object -ExpandProperty _Member

$PodMembers | Format-Table Name, Id -AutoSize
Name         Id
----         --
flasharray1  ac5fc11f-8b3b-49a0-8261-43baf50b281b
flasharray2  081f096d-1c16-42a6-9855-92678d705a1c

Two array members means synchronous replication is in play for everything in this pod. That’s the starting condition we care about. Let’s grab the name of the other member while we’re here, because we’ll want an array connection from it too:

$PeerArrayShortName = ($PodMembers | Where-Object Name -ne $SourceArrayShortName).Name
"ActiveCluster peer: $PeerArrayShortName"
ActiveCluster peer: flasharray2

Step 2: Check the Array Connection to the Third Array

The third array has to be a connected array before it can be a protection group target, so let’s confirm that connection is there.

Get-Pfa2ArrayConnection -Array $SourceFlashArray -Name $TargetArrayShortName |
    Format-Table Name, Type, Status, ReplicationTransport, Encryption -AutoSize
Name        Type              Status    ReplicationTransport Encryption
----        ----              ------    -------------------- ----------
flasharray3 async-replication connected ip                   unencrypted

If you don’t already have that connection, you create it by pulling a connection key from the target and using it on the source:

# Get the connection key from the TARGET array
$ConnectionKey = (Get-Pfa2ArrayConnectionKey -Array $TargetFlashArray).ConnectionKey

# Create the connection from the SOURCE array
New-Pfa2ArrayConnection -Array $SourceFlashArray `
    -ManagementAddress $TargetArrayName `
    -ConnectionKey $ConnectionKey `
    -Type 'async-replication' `
    -Encryption 'unencrypted'

The -Type 'async-replication' parameter is what distinguishes this connection from the sync-replication connection that already exists between your two ActiveCluster arrays. -Encryption defaults to unencrypted, which is what my lab is running. If this replication traffic is leaving your data center, set it to encrypted deliberately.

Important: Async connections from both ActiveCluster member arrays to the third array are required, not just a good idea. Pure’s Async Replication Best Practices Guide is explicit about it: “Asynchronous replication connections from both of the ActiveCluster source arrays to the async target array are required.” The target array transfers snapshot updates from both source arrays at the same time, and if one ActiveCluster array goes down, “async replication continues automatically from the surviving ActiveCluster array,” so your RPO holds no matter which side survives.

Step 3: Create the Protection Group Inside the Pod

This is the part that used to be a dead end. You create the protection group with the pod prefixed name, pod::pgroup, and Purity creates it inside the pod.

New-Pfa2ProtectionGroup -Array $SourceFlashArray -Name $PGroupName
Name                : SQL-AC-FTDemo::ASYNC
Id                  : a99154ee-3c4c-1f82-c9d6-9b65350456ec
Context             : @{Id='ac5fc11f-8b3b-49a0-8261-43baf50b281b'; Name='flasharray1'; ResourceType='remote-arrays'}
Destroyed           : False
EradicationConfig   : @{ManualEradication='enabled'}
HostCount           : 0
HostGroupCount      : 0
IsLocal             : True
Pod                 : @{Id='4840a8a0-acea-b473-1c75-844f2199424b'; Name='SQL-AC-FTDemo'}
ReplicationSchedule : @{Enabled=False; Frequency=14400000}
RetentionLock       : unlocked
SnapshotSchedule    : @{Enabled=False; Frequency=3600000}
Source              : @{Name='SQL-AC-FTDemo'}
SourceRetention     : @{AllForSec=86400; Days=7; PerDay=4; PerPeriod=4; PeriodLengthMs=86400000}
TargetCount         : 0
TargetRetention     : @{AllForSec=86400; Days=7; PerDay=4; PerPeriod=4; PeriodLengthMs=86400000}
VolumeCount         : 0

Look at the Pod and Source properties. The protection group knows it belongs to SQL-AC-FTDemo, and the pod is its source. That Source value is what determines the name this protection group gets on the target array, which is why you end up with SQL-AC-FTDemo:ASYNC over there instead of flasharray1:ASYNC.

Notice also that both schedules come up disabled with default frequencies, and both retention policies are the Purity defaults. We’ll fix all of that in Step 7.

If you’ve built and torn this down before, you’ll hit Name belongs to a protection group that has been destroyed and is pending eradication. A destroyed protection group holds onto its name for the whole eradication pending window. Either pick a different name or eradicate the old one first.

Step 4: Add the Volume

Next, we add our SQL Server volume as a member. Both the group and the member need their fully qualified, pod prefixed names.

New-Pfa2ProtectionGroupVolume -Array $SourceFlashArray `
    -GroupName $PGroupName `
    -MemberName $VolumeName

Get-Pfa2ProtectionGroupVolume -Array $SourceFlashArray -GroupName $PGroupName |
    Format-Table @{n='Group';e={$_.Group.Name}}, @{n='Member';e={$_.Member.Name}} -AutoSize
Group                Member
-----                ------
SQL-AC-FTDemo::ASYNC SQL-AC-FTDemo::SQL-ACTIVEC01-FTDemo

A protection group in a pod can only contain volumes from that same pod. If you try to add a volume that lives outside the pod, it’ll fail, and that restriction is the whole reason the naming is so explicit.

Step 5: Add the Third Array as a Target

Now we add the target. New-Pfa2ProtectionGroupTarget takes the protection group in -GroupName and the target array in -MemberName.

New-Pfa2ProtectionGroupTarget -Array $SourceFlashArray `
    -GroupName $PGroupName `
    -MemberName $TargetArrayShortName
Context : @{Id='ac5fc11f-8b3b-49a0-8261-43baf50b281b'; Name='flasharray1'; ResourceType='remote-arrays'}
Allowed : True
Group   : @{Id='a99154ee-3c4c-1f82-c9d6-9b65350456ec'; Name='SQL-AC-FTDemo::ASYNC'}
Member  : @{Id='f269a914-00c5-408f-b6ba-a3c4048cdbfd'; Name='flasharray3'; ResourceType='array'}
Status  : replicating

Allowed came back True and Status reads replicating immediately. Purity sets that flag on its own, based on replication limits and the health of the array connection, so a healthy configuration gives you True without any action on your part. If yours comes back False, don’t reach for a cmdlet first. Go look at connection health and your replication limits, because that’s what Purity is telling you about, and replication won’t take place while the flag is False.

Step 6: Allow the Replication From the Target Array

This is the detail that will bite you when you script it. On a normal async replication relationship, the protection group on the target array is named sourcearray:pgroupname, and I built exactly that string in my previous post on checking replication status. Here the source identity isn’t an array, it’s the pod, so the replicated protection group is named after the pod rather than whichever array happened to send the data. That makes sense once you think about it, because the pod is stretched across two arrays and either one could be doing the sending.

# The protection group name AS SEEN FROM THE TARGET: pod:pgroup, single colon
$RemotePGroupName = $PodName + ':' + ($PGroupName -split '::')[-1]
"Protection group as seen on the target: $RemotePGroupName"

# Allowed must be set FROM the target array
Update-Pfa2ProtectionGroupTarget -Array $TargetFlashArray `
    -GroupName $RemotePGroupName `
    -MemberName $TargetArrayShortName `
    -Allowed $true
Protection group as seen on the target: SQL-AC-FTDemo:ASYNC

Context : @{Id='f269a914-00c5-408f-b6ba-a3c4048cdbfd'; Name='flasharray3'; ResourceType='remote-arrays'}
Allowed : True
Group   : @{Id='0cb14734-4710-7089-1916-954bdfcc2e73'; Name='SQL-AC-FTDemo:ASYNC'}
Member  : @{Id='f269a914-00c5-408f-b6ba-a3c4048cdbfd'; Name='flasharray3'; ResourceType='array'}
Status  : replicating

Update-Pfa2ProtectionGroupTarget is the explicit control over that Allowed flag, and it has to be run from the target array. In practice you’ll reach for it in the other direction, passing -Allowed $false to stop a source array from replicating into this one. Running it with $true against an already allowed target just confirms the state.

Compare the Group Id here, 0cb14734, against the a99154ee we saw from the source array in Step 5. They’re two different objects. The source array has SQL-AC-FTDemo::ASYNC and the target array has its own SQL-AC-FTDemo:ASYNC, named after the pod. Build that string from $PodName, never from your source array name.

Step 7: Set the Snapshot and Replication Schedules

With the plumbing in place, we can configure the two schedules we laid out at the top of the post. This is where I lost the most time, so let me save you the trouble. You can’t set schedules and retention in the same call. Purity rejects the whole PATCH with Invalid combination of parameters specified. (schedule, retention). There’s a quieter trap too: set a frequency and its enabled flag together and the frequency gets discarded. So this takes three calls, in this order. Watch the units while you’re at it, because schedule frequencies are in milliseconds and retention values are in seconds.

$FiveMinutesMs = 5 * 60 * 1000        # 300000 ms, aka 5 minutes
$ThreeDaysSec  = 3 * 24 * 60 * 60     # 259200 seconds, source retention, which is 3 days
$TwoWeeksSec   = 14 * 24 * 60 * 60    # 1209600 seconds, target retention, which is 14 days

# 1. Frequencies
Update-Pfa2ProtectionGroup -Array $SourceFlashArray -Name $PGroupName `
    -SnapshotScheduleFrequency $FiveMinutesMs `
    -ReplicationScheduleFrequency $FiveMinutesMs

# 2. Turn both schedules on
Update-Pfa2ProtectionGroup -Array $SourceFlashArray -Name $PGroupName `
    -SnapshotScheduleEnabled $true `
    -ReplicationScheduleEnabled $true

# 3. Retention
Update-Pfa2ProtectionGroup -Array $SourceFlashArray -Name $PGroupName `
    -SourceRetentionAllForSec $ThreeDaysSec `
    -SourceRetentionDays 0 `
    -TargetRetentionAllForSec $TwoWeeksSec `
    -TargetRetentionDays 0

Let’s read the result back and confirm all four settings stuck:

Get-Pfa2ProtectionGroup -Array $SourceFlashArray -Name $PGroupName |
    Select-Object Name, SnapshotSchedule, ReplicationSchedule, SourceRetention, TargetRetention |
    Format-List
Name                : SQL-AC-FTDemo::ASYNC
SnapshotSchedule    : @{Enabled=True; Frequency=300000}
ReplicationSchedule : @{Enabled=True; Frequency=300000}
SourceRetention     : @{AllForSec=259200; Days=0; PerDay=4; PerPeriod=4; PeriodLengthMs=86400000}
TargetRetention     : @{AllForSec=1209600; Days=0; PerDay=4; PerPeriod=4; PeriodLengthMs=86400000}

That’s the design expressed in numbers. Both schedules enabled at 300000 ms, which is 5 minutes. SourceRetention.AllForSec of 259200 is 3 days, TargetRetention.AllForSec of 1209600 is 2 weeks. Setting Days to 0 on both means every snapshot is kept for the full retention window, then it’s gone, with no thinning. If you want the classic tiered retention where snapshots get thinned to a few per day after the initial window, set Days and PerDay instead.

Step 8: Take an On Demand Snapshot and Replicate It Now

Scheduled replication is great for steady state, but when you’re doing a T-SQL snapshot backup or coordinating with an application quiesce, you want to take a snapshot and push it immediately. Give it a suffix you’ll recognize later.

$SnapshotSuffix = 'BOOYAA'

$Snapshot = New-Pfa2ProtectionGroupSnapshot -Array $SourceFlashArray `
    -SourceName $PGroupName `
    -Suffix $SnapshotSuffix `
    -ReplicateNow $true `
    -ApplyRetention $true

$Snapshot | Format-List Name, Created, Suffix, Pod, Source
Name    : SQL-AC-FTDemo::ASYNC.BOOYAA
Created : 8/18/2026 9:32:55 PM
Suffix  : BOOYAA
Pod     : @{Id='4840a8a0-acea-b473-1c75-844f2199424b'; Name='SQL-AC-FTDemo'}
Source  : @{Id='a99154ee-3c4c-1f82-c9d6-9b65350456ec'; Name='SQL-AC-FTDemo::ASYNC'}

The -ReplicateNow $true parameter pushes this snapshot to every allowed target right away instead of waiting for the next scheduled interval. -ApplyRetention $true makes sure the local and remote retention policies apply to it, so it ages out with everything else rather than living forever.

Step 9: Verify the Snapshot Landed on the Target

Here’s where the naming pays off. On the target array, we look for SQL-AC-FTDemo:ASYNC.BOOYAA.

$TargetSnapshotName = $RemotePGroupName + '.' + $SnapshotSuffix

Get-Pfa2ProtectionGroupSnapshot -Array $TargetFlashArray -Name $TargetSnapshotName |
    Select-Object Name, Created, @{ n='SnapshotsGB'; e={ [math]::Round($_.Space.Snapshots / 1GB, 2) } }
Name                       Created              SnapshotsGB
----                       -------              -----------
SQL-AC-FTDemo:ASYNC.BOOYAA 8/18/2026 9:32:55 PM        0.00

The snapshot is there with the same creation timestamp, and SnapshotsGB reads 0.00. That’s not a failure, that’s data reduction doing its job. This array had already received earlier snapshots of this same volume, so almost every block in this one deduplicated against what was already sitting there. The first replication of this dataset carried the full 140 GB across the wire. The first replication pays the full cost and every one after it is close to free.

If you want to watch the transfer itself instead of just confirming the result, Get-Pfa2ProtectionGroupSnapshotTransfer run against the target array gives you progress and completion. A Progress of 1 with a populated Completed field means you’re done:

Get-Pfa2ProtectionGroupSnapshotTransfer -Array $TargetFlashArray `
    -Name $RemotePGroupName `
    -Filter "completed and name='$TargetSnapshotName'" |
    Format-List Name, Started, Completed, Progress, DataTransferred, PhysicalBytesWritten

This is the check you want in front of any downstream job that depends on the data being at the third site. I went into depth on this pattern, including what the output looks like mid transfer, in Part 5 of my PowerShell SDK2 series. Everything there works here, as long as you build the target side name from the pod.

Why This Matters for SQL Server

If you’re running SQL Server on ActiveCluster, this closes a real gap in the design:

  1. One data copy, three locations: Your SQL Server volumes stay in the pod with zero RPO between metro sites, and the same volumes feed the async copy at your DR site. No duplicate volume layouts, no shadow protection groups.
  2. Recovery from a snapshot, not a restore: Because that third copy is an array to array protection group snapshot, you can copy it to a new volume on the third array and mount it. That’s a nearly instant recovery of a multi terabyte database instead of a multi hour restore. That new volume is a regular volume, not a pod member, since a pod can’t be an async target.
  3. Survives an array failure at the metro site: With the array connection configured from both pod members, losing one ActiveCluster array doesn’t stop your DR replication.
  4. SafeMode where it counts: The third array can carry its own SafeMode retention lock, giving you an immutable copy outside the blast radius of the metro cluster.
  5. It automates cleanly: Every step above is a single SDK2 cmdlet, so this drops straight into whatever provisioning or backup automation you already run.

Wrapping Up

Being able to replicate a protection group from inside an ActiveCluster pod to a third array removes the last awkward workaround from the three site FlashArray design for SQL Server. Create the protection group in the pod, add the volumes, add the target, allow it from the target, and set your schedules.

Two things to keep in your head when you script this. The source identity is the pod, not the array, so the protection group on your target array is named pod:pgroup. And Update-Pfa2ProtectionGroup needs three separate calls to configure a schedule, because schedule and retention fields can’t go in the same PATCH.

Before you design around this, read the ActiveCluster FAQ for the current feature and version details. Then get out in your lab and try it. If you’re already running ActiveCluster, take a look at whether you can collapse a parallel replication configuration down into your existing pod. Let me know how it works in your environment.