SQL Server on EC2: Storage Layout and Log Shipping
-
Phase 0: Before you start
-
Phase 1: The foundation
-
Phase 2: Operations and security
-
Phase 3: SQL Server on AWS
-
Phase 4: Troubleshooting step by step
-
Phase 5: Final project and career
RDS can’t do everything. When an app needs SQL Agent jobs that touch the OS, linked servers, or features RDS blocks, SQL Server runs on EC2, and you’re the DBA and the sysadmin. SQL Server log shipping on EC2 is one of the oldest, most reliable ways to move a database there, or keep a warm standby, with almost no tooling.
You’ll build sql01 with separate data and log volumes, then ship transaction log backups from onprem-sql through S3 and restore them on sql01 in standby mode. After that you’ll break the log chain twice and repair it.
| Time | You need | By the end you can |
|---|---|---|
| 120 minutes | Lessons 46 to 49 done (onprem-sql stopped, your backup bucket), Start-SsmEndpoints.ps1, your laptop signed in as saat-admin. Cost: about USD 3 if you finish within two hours: two SQL Server Standard instances run for the whole lab. sql01 is deleted at the end; onprem-sql stays stopped. | Lay out EBS volumes for SQL Server, set default data and log paths, seed a standby with a full backup, ship and restore log backups through S3, automate it, and fix broken log chains. |
What it is, in one line
SQL Server log shipping on EC2 keeps a copy of a database in step by restoring the primary’s transaction log backups, in order, on another SQL Server running on EC2.
In Simple Words
The diary and the copy
Your friend keeps a diary. Once you copied the whole book. Now, every evening, they send you a photo of today’s page, and you copy it into your book.
As long as you get every page in order, your book stays the same as theirs. Miss Tuesday’s page and you can’t write Wednesday’s, because it starts halfway through Tuesday’s story.
Log shipping works like that. The full backup is the first copy; each log backup is one day’s page. The pages must arrive in order, with none missing.
Engineer's View
How log shipping and EC2 storage work
Log shipping needs the FULL (or BULK_LOGGED) recovery model on the primary. You restore a full backup on the secondary WITH NORECOVERY or WITH STANDBY (read-only between restores, using an undo file), then restore each log backup in LSN order. Built-in log shipping uses SQL Agent jobs and a shared folder, which needs both servers to reach the same share; across sites and accounts, teams often ship through S3 instead, as here.
On EC2, put data and log files on separate EBS volumes (gp3 for most, io2 for heavy IOPS), formatted NTFS with 64 KB clusters, and set the default paths so new databases land there. tempdb can go on instance store on instance types that have it, because it’s rebuilt at every start. Instance size limits total EBS throughput, so check the instance’s EBS bandwidth as well as the volume settings (Lesson 43).
An ad-hoc log backup outside the shipping process breaks the chain for the secondary, so other tools must use COPY_ONLY. Restoring the secondary WITH RECOVERY ends the restore sequence. At a planned cutover you take a tail-log backup WITH NORECOVERY on the primary, apply it, then recover the secondary.
How it works, step by step
Here is what happens in each shipping cycle.
1Back up the log
Every 5 minutes, Ship-Logs.ps1 runs BACKUP LOG on onprem-sql into a new, time-stamped .trn file.
2Upload it to S3
The script uploads every .trn file waiting in the folder, so a failed upload is retried next time, then moves it to a shipped folder.
3Restore on sql01, in order
Apply-Logs.ps1 lists the S3 prefix, sorts by name (the time stamp), skips files already applied, and restores the rest WITH STANDBY so the database stays readable between restores.
4Give SQL Server the right disks
Before any of this, sql01 gets a layout that matches how SQL Server uses disks:
Separate volumes let you size and tune data and log independently, and snapshot or grow one without touching the other.
✅ Checkpoint: You can explain the log chain, the difference between NORECOVERY, STANDBY and RECOVERY, and why data and log get separate volumes.
Hands-on lab: build sql01 and ship logs from onprem-sql
Sign in, start onprem-sql and add the helpers. Invoke-Win runs a script on a Windows instance with Run Command; Add-Orders writes new orders on the primary:
cd C:\aws-lab
aws login --profile saat-admin
$env:AWS_PROFILE = 'saat-admin'
$v = Get-Content C:\aws-lab\saat-vpc.json | ConvertFrom-Json
$acct = aws sts get-caller-identity --query Account --output text
$bak = "saat-sqlbak-$acct-ap-south-1-an"
.\Start-SsmEndpoints.ps1
$pri = aws ec2 describe-instances --filters "Name=tag:Name,Values=onprem-sql" "Name=instance-state-name,Values=stopped,running" --query "Reservations[0].Instances[0].InstanceId" --output text
aws ec2 start-instances --instance-ids $pri | Out-Null
do { Start-Sleep 20; $p = aws ssm describe-instance-information --filters "Key=InstanceIds,Values=$pri" --query "InstanceInformationList[0].PingStatus" --output text } until ($p -eq 'Online')
function Invoke-Win([string]$Id, [string]$Script) {
@{ commands = @($Script -split "`r?`n"); executionTimeout = @('1800') } | ConvertTo-Json | Set-Content win-cmd.json
$cmd = aws ssm send-command --document-name AWS-RunPowerShellScript --instance-ids $Id --parameters file://win-cmd.json --query Command.CommandId --output text
do { Start-Sleep 10; $st = aws ssm get-command-invocation --command-id $cmd --instance-id $Id --query Status --output text 2>$null } until ($st -in 'Success', 'Failed', 'TimedOut')
aws ssm get-command-invocation --command-id $cmd --instance-id $Id --query StandardOutputContent --output text
}
function Get-Count([string]$Id) { (Invoke-Win $Id 'sqlcmd -S . -E -C -h -1 -W -Q "SET NOCOUNT ON; SELECT COUNT(*) FROM SaatShop.dbo.Orders"').Trim() }
$shipCmd = 'powershell -NoProfile -ExecutionPolicy Bypass -File C:\sqlbak\Ship-Logs.ps1'
$applyCmd = 'powershell -NoProfile -ExecutionPolicy Bypass -File C:\sqlbak\Apply-Logs.ps1'
function Add-Orders([int]$N) { Invoke-Win $pri ("sqlcmd -S . -E -C -b -d SaatShop -Q ""INSERT dbo.Orders (Customer, Amount, CustomerId) SELECT TOP ($N) CONCAT(N'Ship ', ROW_NUMBER() OVER (ORDER BY (SELECT NULL))), 75.00, 1 FROM sys.all_objects"" | Out-Null; 'added $N'") }
Get-Count $priYou should see something like this:
501021Write the shipping scripts and give both servers S3 access
Paste this whole block. The scripts go to S3 so both servers can download them:
$ship = @'
param([string]$Bucket = 'BAK_HERE')
New-Item C:\sqlbak\logs, C:\sqlbak\shipped -ItemType Directory -Force | Out-Null
$f = "C:\sqlbak\logs\SaatShop_$(Get-Date -Format yyyyMMddHHmmssfff).trn"
sqlcmd -S . -E -C -b -Q "BACKUP LOG SaatShop TO DISK = '$f' WITH CHECKSUM, COMPRESSION" | Out-Null
Get-ChildItem C:\sqlbak\logs\*.trn | ForEach-Object {
Write-S3Object -BucketName $Bucket -Key "logship/logs/$($_.Name)" -File $_.FullName
Move-Item $_.FullName C:\sqlbak\shipped\ -Force
}
"shipped $(Split-Path $f -Leaf)"
'@
$apply = @'
param([string]$Bucket = 'BAK_HERE')
New-Item C:\sqlbak\in -ItemType Directory -Force | Out-Null
$done = 'C:\sqlbak\applied.txt'
if (-not (Test-Path $done)) { New-Item $done -ItemType File | Out-Null }
$applied = @(Get-Content $done)
$keys = @(Get-S3Object -BucketName $Bucket -KeyPrefix logship/logs/ | Sort-Object Key | Where-Object { $applied -notcontains $_.Key })
$n = 0
foreach ($k in $keys) {
$file = "C:\sqlbak\in\$(Split-Path $k.Key -Leaf)"
Read-S3Object -BucketName $Bucket -Key $k.Key -File $file | Out-Null
$o = sqlcmd -S . -E -C -b -Q "RESTORE LOG SaatShop FROM DISK = '$file' WITH STANDBY = 'L:\SQLLog\SaatShop_undo.tuf'"
if ($LASTEXITCODE -ne 0) { "applied $n, FAILED on $($k.Key)"; $o | Select-Object -First 2; return }
Add-Content $done $k.Key
$n++
}
"applied $n new log backups"
'@
$ship -replace 'BAK_HERE', $bak | Set-Content Ship-Logs.ps1
$apply -replace 'BAK_HERE', $bak | Set-Content Apply-Logs.ps1
aws s3 cp Ship-Logs.ps1 "s3://$bak/logship/scripts/Ship-Logs.ps1" --only-show-errors
aws s3 cp Apply-Logs.ps1 "s3://$bak/logship/scripts/Apply-Logs.ps1" --only-show-errors
@{ Version = '2012-10-17'; Statement = @(
@{ Effect = 'Allow'; Action = 's3:ListBucket'; Resource = "arn:aws:s3:::$bak" },
@{ Effect = 'Allow'; Action = @('s3:GetObject', 's3:PutObject'); Resource = "arn:aws:s3:::$bak/logship/*" }) } | ConvertTo-Json -Depth 5 | Set-Content logship-s3.json
aws iam put-role-policy --role-name saat-ec2-ssm-role --policy-name saat-logship-s3 --policy-document file://logship-s3.json
aws s3 ls "s3://$bak/logship/scripts/"You should see something like this:
2026-10-20 15:02:11 512 Apply-Logs.ps1
2026-10-20 15:02:10 498 Ship-Logs.ps1Instance access is limited to the logship/ prefix of one bucket. Real log shipping between companies or accounts often looks exactly like this: no shared folder, no domain trust, just an encrypted bucket.
2Build sql01 with separate data and log volumes
Paste this whole block. It launches SQL Server 2022 Standard with two extra gp3 volumes and prepares them (about 10 minutes):
'[{"DeviceName":"xvdb","Ebs":{"VolumeSize":20,"VolumeType":"gp3","Encrypted":true,"DeleteOnTermination":true}},{"DeviceName":"xvdc","Ebs":{"VolumeSize":10,"VolumeType":"gp3","Encrypted":true,"DeleteOnTermination":true}}]' | Set-Content bdm.json
$sqlAmi = aws ssm get-parameter --name /aws/service/ami-windows-latest/Windows_Server-2022-English-Full-SQL_2022_Standard --query Parameter.Value --output text
$sec = aws ec2 run-instances --image-id $sqlAmi --instance-type t3.xlarge --subnet-id $v.PrivateB --security-group-ids $v.AppSg --iam-instance-profile Name=saat-ec2-ssm-profile --metadata-options HttpTokens=required --block-device-mappings file://bdm.json --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=sql01},{Key=course,Value=saatvedha-aws},{Key=lesson,Value=50}]' 'ResourceType=volume,Tags=[{Key=course,Value=saatvedha-aws},{Key=lesson,Value=50}]' --query "Instances[0].InstanceId" --output text
do { Start-Sleep 30; $p = aws ssm describe-instance-information --filters "Key=InstanceIds,Values=$sec" --query "InstanceInformationList[0].PingStatus" --output text } until ($p -eq 'Online')
$setup = @'
foreach ($d in Get-Disk | Where-Object PartitionStyle -eq 'RAW') {
$letter = if ($d.Size -gt 15GB) { 'D' } else { 'L' }
$label = if ($letter -eq 'D') { 'SQLData' } else { 'SQLLog' }
Initialize-Disk -Number $d.Number -PartitionStyle GPT -PassThru | New-Partition -DriveLetter $letter -UseMaximumSize | Format-Volume -FileSystem NTFS -AllocationUnitSize 65536 -NewFileSystemLabel $label -Confirm:$false | Out-Null
}
New-Item D:\SQLData, L:\SQLLog, C:\sqlbak -ItemType Directory -Force | Out-Null
icacls D:\SQLData /grant 'NT SERVICE\MSSQLSERVER:(OI)(CI)F' | Out-Null
icacls L:\SQLLog /grant 'NT SERVICE\MSSQLSERVER:(OI)(CI)F' | Out-Null
$inst = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL').MSSQLSERVER
$reg = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$inst\MSSQLServer"
Set-ItemProperty $reg -Name DefaultData -Value 'D:\SQLData'
Set-ItemProperty $reg -Name DefaultLog -Value 'L:\SQLLog'
Stop-Service MSSQLSERVER -Force
net start MSSQLSERVER /mSQLCMD | Out-Null
Start-Sleep 15
sqlcmd -S . -E -C -b -Q "IF SUSER_ID('NT AUTHORITY\SYSTEM') IS NULL CREATE LOGIN [NT AUTHORITY\SYSTEM] FROM WINDOWS; ALTER SERVER ROLE sysadmin ADD MEMBER [NT AUTHORITY\SYSTEM];" | Out-Null
Stop-Service MSSQLSERVER -Force
Start-Service MSSQLSERVER
Get-Volume -DriveLetter D, L | ForEach-Object { "$($_.DriveLetter): $($_.FileSystemLabel) $($_.AllocationUnitSize / 1KB) KB clusters" }
sqlcmd -S . -E -C -h -1 -W -Q "SET NOCOUNT ON; SELECT CONCAT('data ', SERVERPROPERTY('InstanceDefaultDataPath'), ' | log ', SERVERPROPERTY('InstanceDefaultLogPath'))"
'@
Invoke-Win $sec $setupYou should see something like this:
D: SQLData 64 KB clusters
L: SQLLog 64 KB clusters
data D:\SQLData\ | log L:\SQLLog\sql01 is in a different Availability Zone from onprem-sql, with data and log on their own volumes and 64 KB clusters. New databases now default to D: and L:. The same script makes Run Command’s account sysadmin, as you did on onprem-sql in Lesson 47.
3Seed sql01 with a full backup
Paste this whole block:
$priSeed = @'
sqlcmd -S . -E -C -b -Q "ALTER DATABASE SaatShop SET RECOVERY FULL; BACKUP DATABASE SaatShop TO DISK = 'C:\sqlbak\ls-full.bak' WITH INIT, COMPRESSION, CHECKSUM" | Out-Null
Write-S3Object -BucketName 'BAK_HERE' -Key logship/ls-full.bak -File C:\sqlbak\ls-full.bak
Read-S3Object -BucketName 'BAK_HERE' -Key logship/scripts/Ship-Logs.ps1 -File C:\sqlbak\Ship-Logs.ps1 | Out-Null
"full backup uploaded"
'@
Invoke-Win $pri ($priSeed -replace 'BAK_HERE', $bak)
$seed = @'
Read-S3Object -BucketName 'BAK_HERE' -Key logship/scripts/Apply-Logs.ps1 -File C:\sqlbak\Apply-Logs.ps1 | Out-Null
Read-S3Object -BucketName 'BAK_HERE' -Key logship/FULL_HERE -File C:\sqlbak\seed.bak | Out-Null
$files = sqlcmd -S . -E -C -h -1 -W -s "|" -Q "SET NOCOUNT ON; RESTORE FILELISTONLY FROM DISK = 'C:\sqlbak\seed.bak'" | Where-Object { $_ -match '\|' }
$moves = foreach ($l in $files) { $p = $l -split '\|'; if ($p[2] -eq 'L') { "MOVE '$($p[0])' TO 'L:\SQLLog\$($p[0]).ldf'" } else { "MOVE '$($p[0])' TO 'D:\SQLData\$($p[0]).mdf'" } }
sqlcmd -S . -E -C -b -Q "RESTORE DATABASE SaatShop FROM DISK = 'C:\sqlbak\seed.bak' WITH $($moves -join ', '), REPLACE, STANDBY = 'L:\SQLLog\SaatShop_undo.tuf'" | Out-Null
sqlcmd -S . -E -C -h -1 -W -Q "SET NOCOUNT ON; SELECT CONCAT(name, ' | ', state_desc, ' | standby ', is_in_standby, ' | ', (SELECT COUNT(*) FROM SaatShop.dbo.Orders), ' orders') FROM sys.databases WHERE name = 'SaatShop'"
'@
Invoke-Win $sec ($seed -replace 'BAK_HERE', $bak -replace 'FULL_HERE', 'ls-full.bak')You should see something like this:
full backup uploaded
SaatShop | ONLINE | standby 1 | 50102 orderssql01’s copy is in standby: readable, but waiting for the next log backup. The data file went to D:\SQLData and the log to L:\SQLLog because of the MOVE clauses.

4Ship and apply logs, by hand then on a schedule
Paste this whole block. It does one cycle by hand, then schedules both scripts every 5 minutes and waits for two cycles:
Add-Orders 200
Invoke-Win $pri $shipCmd
Invoke-Win $sec $applyCmd
"primary $(Get-Count $pri) secondary $(Get-Count $sec)"
$task = 'Register-ScheduledTask -TaskName SaatLogShip -Action (New-ScheduledTaskAction -Execute powershell.exe -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\sqlbak\SCRIPT.ps1") -Trigger (New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 5)) -User SYSTEM -RunLevel Highest -Force | Out-Null; (Get-ScheduledTask SaatLogShip).State'
Invoke-Win $pri ($task -replace 'SCRIPT', 'Ship-Logs')
Invoke-Win $sec ($task -replace 'SCRIPT', 'Apply-Logs')
Add-Orders 50
Start-Sleep 660
"primary $(Get-Count $pri) secondary $(Get-Count $sec)"You should see something like this:
added 200
shipped SaatShop_20261020150912345.trn
applied 1 new log backups
primary 50302 secondary 50302
Ready
Ready
added 50
primary 50352 secondary 50352Both servers now run their script every 5 minutes, and sql01 follows onprem-sql with nobody touching it. In a real setup you’d also alert when the newest applied file is older than, say, 15 minutes.

✅ Checkpoint: sql01 has D: SQLData and L: SQLLog with 64 KB clusters, SaatShop on sql01 is in standby, and after the scheduled cycles both servers show 50,352 orders.
Break It, Then Fix It
Two ways the log chain breaks in real life. First pause the schedules so only your commands run:
Invoke-Win $pri 'Disable-ScheduledTask SaatLogShip | Out-Null; "paused"'
Invoke-Win $sec 'Disable-ScheduledTask SaatLogShip | Out-Null; "paused"'
Invoke-Win $sec $applyCmdYou should see something like this:
paused
paused
applied 0 new log backups1Break it: an ad-hoc log backup
Take a log backup outside the shipping folder, as backup software or a hurried DBA would, then ship as normal:
Invoke-Win $pri '$f = "C:\sqlbak\SaatShop_$(Get-Date -Format yyyyMMddHHmmssfff).trn"; sqlcmd -S . -E -C -b -Q "BACKUP LOG SaatShop TO DISK = ''$f''" | Out-Null; Split-Path $f -Leaf'
Add-Orders 10
Invoke-Win $pri $shipCmd
Invoke-Win $sec $applyCmdYou’ll get something like this:
SaatShop_20261020153001122.trn
added 10
shipped SaatShop_20261020153044871.trn
applied 0, FAILED on logship/logs/SaatShop_20261020153044871.trn
Msg 4305, Level 16, State 1, Server EC2AMAZ-SQL01, Line 1
The log in this backup set begins at LSN 41000000062400001, which is too recent to apply to the database. An earlier log backup that includes LSN 41000000061600001 can be restored.The ad-hoc backup took some log records and truncated them, so the next shipped file starts after a gap. SQL Server even tells you the LSN it’s missing. Fix: find that backup and ship it; Apply-Logs sorts by time stamp, so it applies the missing file first:
Invoke-Win $pri ('Get-ChildItem C:\sqlbak\SaatShop_*.trn | ForEach-Object { Write-S3Object -BucketName BAK_HERE -Key "logship/logs/$($_.Name)" -File $_.FullName; Move-Item $_.FullName C:\sqlbak\shipped\ -Force; "uploaded $($_.Name)" }' -replace 'BAK_HERE', $bak)
Invoke-Win $sec $applyCmd
"primary $(Get-Count $pri) secondary $(Get-Count $sec)"You should see something like this:
uploaded SaatShop_20261020153001122.trn
applied 2 new log backups
primary 50362 secondary 50362To stop this happening again, any backup outside the shipping process must use WITH COPY_ONLY, which doesn’t affect the log chain.
2Break it: someone recovers the standby
Bring sql01’s copy online, as someone “just checking” might, then ship another log:
Invoke-Win $sec 'sqlcmd -S . -E -C -b -Q "RESTORE DATABASE SaatShop WITH RECOVERY" | Out-Null; "recovered"'
Add-Orders 5
Invoke-Win $pri $shipCmd
Invoke-Win $sec $applyCmdYou’ll get something like this:
recovered
added 5
shipped SaatShop_20261020153512004.trn
applied 0, FAILED on logship/logs/SaatShop_20261020153512004.trn
Msg 3117, Level 16, State 1, Server EC2AMAZ-SQL01, Line 1
The log or differential backup cannot be restored because no files are ready to rollforward.
RESTORE WITH RECOVERY ends the restore sequence for good. It’s the right command at a planned cutover, after a tail-log backup WITH NORECOVERY on the primary, and the wrong one at any other time. Fix: reseed sql01 from a COPY_ONLY full backup, mark the older logs as done, then ship again:
$reseed = @'
sqlcmd -S . -E -C -b -Q "BACKUP DATABASE SaatShop TO DISK = 'C:\sqlbak\ls-reseed.bak' WITH INIT, COPY_ONLY, COMPRESSION, CHECKSUM" | Out-Null
Write-S3Object -BucketName 'BAK_HERE' -Key logship/ls-reseed.bak -File C:\sqlbak\ls-reseed.bak
"reseed backup uploaded"
'@
Invoke-Win $pri ($reseed -replace 'BAK_HERE', $bak)
Invoke-Win $sec ($seed -replace 'BAK_HERE', $bak -replace 'FULL_HERE', 'ls-reseed.bak')
Invoke-Win $sec "(Get-S3Object -BucketName '$bak' -KeyPrefix logship/logs/).Key | Set-Content C:\sqlbak\applied.txt; 'older logs marked as applied'"
Add-Orders 5
Invoke-Win $pri $shipCmd
Invoke-Win $sec $applyCmd
"primary $(Get-Count $pri) secondary $(Get-Count $sec)"You should see something like this:
reseed backup uploaded
SaatShop | ONLINE | standby 1 | 50367 orders
older logs marked as applied
added 5
shipped SaatShop_20261020154201733.trn
applied 1 new log backups
primary 50372 secondary 50372Now clean up. sql01 and its volumes go; onprem-sql goes back to simple recovery and stays stopped:
Invoke-Win $pri 'Unregister-ScheduledTask SaatLogShip -Confirm:$false; sqlcmd -S . -E -C -b -Q "ALTER DATABASE SaatShop SET RECOVERY SIMPLE" | Out-Null; Remove-Item C:\sqlbak\logs, C:\sqlbak\shipped, C:\sqlbak\ls-*.bak -Recurse -Force; "clean"'
aws ec2 terminate-instances --instance-ids $sec | Out-Null
aws ec2 stop-instances --instance-ids $pri | Out-Null
aws s3 rm "s3://$bak/logship/" --recursive --only-show-errors
aws iam delete-role-policy --role-name saat-ec2-ssm-role --policy-name saat-logship-s3
.\Stop-SsmEndpoints.ps1
Remove-Item Ship-Logs.ps1, Apply-Logs.ps1, logship-s3.json, bdm.json, win-cmd.json✅ Checkpoint: You fixed a broken chain by shipping the missing log backup and a recovered standby by reseeding from a COPY_ONLY full. sql01 is terminated with its volumes, the logship/ prefix and the policy are gone, and onprem-sql is stopping.
This happened at work
The nightly backup job that broke DR
A logistics firm shipped logs from its main SQL Server to an EC2 standby for disaster recovery. It worked for a year. Then a new backup product was installed on the primary, with default settings.
Every night at 1 a.m. the product took its own log backup to tape. The standby fell further behind each night, and the alert someone had set up years before went to an old mailbox nobody read.
They found out during a DR test: the standby was 41 days behind. The backup product was switched to COPY_ONLY, the standby was reseeded, and the lag alert now goes to the on-call channel with a 15-minute threshold.
Common mistakes and troubleshooting
| Mistake | What happens | What to do |
|---|---|---|
| Ad-hoc log backups without COPY_ONLY | Msg 4305, chain broken | COPY_ONLY for anything outside shipping |
| RESTORE WITH RECOVERY on the standby | Msg 3117, reseed needed | Only at a planned cutover after a tail-log backup |
| Data, log and tempdb on one volume | Slow writes and hard tuning | Separate volumes, 64 KB clusters |
| No alert on shipping lag | Standby silently days behind | Alert when the newest applied log is too old |
| Primary in SIMPLE recovery | No log backups possible | FULL recovery plus a full backup to start |
| Ignoring instance EBS bandwidth | Volumes never reach their IOPS | Size the instance for total EBS throughput |
Interview questions
Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.
Fresher level
1. Can you explain in simple words what log shipping is?
Why they ask this: They want to check that you understand the most basic standby method for SQL Server, and how its pieces fit together.
How you might answer: Log shipping keeps a warm standby copy of a database. You seed the secondary from a full backup, then keep restoring the primary’s transaction log backups on it, in order.
I explain it like sending a friend every page of your diary as you write it, so their copy is only a few pages behind.
2. What’s the difference between restoring WITH NORECOVERY and WITH STANDBY, and when would you use each?
Why they ask this: They want to see that you know the restore options and what users can and can’t do with the secondary.
How you might answer: Both leave the database ready to accept more log restores. NORECOVERY keeps it closed, so nobody can read it.
STANDBY uses an undo file so the database is read-only between restores. I use it when people want to run reports on the secondary.
3. Why would you put SQL Server data files and log files on separate EBS volumes?
Why they ask this: They want to know you understand that data and log files behave differently, and that storage layout affects performance.
How you might answer: Data files get mostly random I/O, while the log is written sequentially. Keeping them apart means I can size each volume and set its performance on its own.
It also makes troubleshooting easier, because I can see which volume is busy.
2–5 years experience
4. You come in on Monday and the log shipping secondary is failing with Msg 4305. What happened, and how do you fix it?
Why they ask this: They want to see if you understand the log chain. This error is common and usually caused by someone else’s backup.
How you might answer: Msg 4305 means a log backup is missing from the chain, and it’s often an ad-hoc backup someone took. The error message gives the LSN, so I find that backup file and restore it, or I reseed the secondary if I can’t find it.
To stop it happening again, I ask people to use COPY_ONLY for any one-off backup.
5. Walk me through a planned cutover from the primary to the log shipping secondary.
Why they ask this: They want to see that you know the exact order of steps, and that you remember the work after the restore, like logins and jobs.
How you might answer: First I stop the app and take a tail-log backup WITH NORECOVERY on the primary. Then I ship and restore all the remaining logs on the secondary and run RESTORE WITH RECOVERY.
After that I fix the logins and jobs, and point the app to the new server.
6. Your team is choosing between RDS and SQL Server on EC2. When would you push for EC2?
Why they ask this: They want to see that you choose a platform for real reasons, not personal taste, and that you understand what RDS doesn’t allow.
How you might answer: I’d go for EC2 when we need OS access, a feature RDS blocks, or a third-party agent on the server. It’s also the choice for certain HA designs, BYOL with dedicated hosts, or special maintenance needs.
If none of those apply, I’d rather use RDS and let AWS handle the patching and backups.
Senior level
7. Design disaster recovery for a 2 TB SQL Server on EC2 where the business can lose at most 15 minutes of data.
Why they ask this: They want to see how you turn an RPO into a real design, and whether you include testing and alerting and not just the technology.
How you might answer: I’d use log shipping or Always On to another Region. Log backups would run every 5 minutes to S3, with cross-Region replication, which keeps me well inside the 15 minutes.
I’d add alarms on lag, written runbooks and regular DR tests. AWS Backup would take full snapshots as an extra layer.
8. How would you lay out storage for a heavy OLTP SQL Server on EC2?
Why they ask this: They want to see that you can match the instance and disks to the workload, and that you know where the limits usually hit.
How you might answer: I’d pick a memory-optimized instance with high EBS bandwidth, and io2 or gp3 volumes sized for the IOPS and throughput I need. Data and log go on separate volumes, formatted with 64 KB clusters.
I’d put tempdb on instance store, with a startup script to rebuild the folder after a stop. Then I’d watch volume queue length and the throughput limits of both the volumes and the instance.
Quick quiz
Q1. What must the primary database use for log shipping?
- SIMPLE recovery
- FULL or BULK_LOGGED recovery
- Read-only mode
- Compression
Show answer
B — Log backups need it.
Q2. What does STANDBY add over NORECOVERY?
- Faster restores
- Read-only access between restores
- Encryption
- Compression
Show answer
B — An undo file makes it readable.
Q3. What caused Msg 4305?
- Wrong password
- A missing log backup in the chain
- Full disk
- Wrong edition
Show answer
B — The ad-hoc backup created a gap.
Q4. Which option should ad-hoc backups use?
- NORECOVERY
- COPY_ONLY
- STANDBY
- REPLACE
Show answer
B — It leaves the log chain alone.
Q5. When is RESTORE WITH RECOVERY on the secondary right?
- Every hour
- At a planned cutover after the tail-log
- Never
- After each log
Show answer
B — It ends the restore sequence.
Quick recap
- SQL Server on EC2 gives full control, and full responsibility.
- Separate data and log volumes with 64 KB clusters; know your instance EBS limits.
- Log shipping restores log backups in order on a standby seeded from a full backup.
- COPY_ONLY for ad-hoc backups; never recover the standby except at cutover.
- Alert on shipping lag, or you’ll find out during a disaster.
Next lesson
Lesson 51: CloudWatch Database Insights → You’ll find what is slowing saat-sql down by waits, SQL, hosts and users.
Further reading: SQL Server log shipping on EC2
Want to go deeper into SQL Server log shipping on EC2? Read About log shipping (SQL Server) and Best practices for SQL Server on Amazon EC2. You can also see the full plan for this course on the AWS Cloud Administration course page.
