Build an Azure DevOps SQL Lab on AWS
Provision two Windows SQL nodes and deploy Azure DevOps Server with Terraform.
Introduction
30 Second Summary
Practising how a critical service recovers from failure usually needs more than a spare laptop. You need a realistic environment that stays separate from live systems.
In this project, you will use AWS CloudShell with Terraform 1.16.4 to create a two-node Windows Server 2022 lab on Amazon EC2. You'll install SQL Server 2022 Developer on both nodes while Azure DevOps Server 2022.2 Patch 12 runs on the primary.
What You'll Build
You'll load the Azure DevOps landing page from the primary node while a second SQL node stands ready for restore and log-shipping exercises.
By the end of this project, you'll have:
- A repeatable Terraform configuration that recreates two disposable Windows Server 2022 nodes in AWS.
- Secure browser access to both Windows desktops through AWS Systems Manager Fleet Manager with no inbound RDP exposure.
- A running SQL data tier on both nodes plus a working Azure DevOps Server 2022.2 Patch 12 deployment on the primary. Its configuration and collection databases form the recovery baseline for later backup exercises.
- Secret Mission: Stop and restart the complete lab. Then prove that every required service and database returns cleanly.
Are there any prerequisites?
A modern browser plus an AWS account with the required EC2, VPC, IAM, and Systems Manager permissions are required. You also need a valid full Azure DevOps Server 2022 license entitlement plus authorisation to incur temporary pay-as-you-go AWS charges.
Before We Start
Before any hands-on work, define this project as a temporary non-production recovery lab. The scope places Azure DevOps Server plus SQL Server on the primary node, SQL Server alone on the secondary node, with every AWS resource stopped or destroyed after practice.
Set Up a Browser-Only Terraform Workspace
A repeatable recovery lab starts with configuration files that can rebuild the same infrastructure on demand. AWS CloudShell gives you a browser-hosted workspace for creating those files with Terraform.
CloudShell keeps files under $HOME between sessions in the selected Region. This removes local operating-system differences while keeping the lab configuration available for later steps.
In this step, get ready to:
- Start a public CloudShell environment in us-east-1.
- Install and verify Terraform CLI 1.16.4 inside the persistent CloudShell home directory.
- Create the EC2 key pair and the complete Terraform workspace.
Launch CloudShell and pin Terraform
A public CloudShell environment includes authenticated access to your AWS account. Selecting the Region first keeps the shell workspace beside the infrastructure you provision later.
- Sign in to the AWS Management Console with permission to create EC2 resources.
- Use the console Region selector to choose us-east-1.
- Launch AWS CloudShell from the console toolbar.
- Keep the default public CloudShell environment selected.
- Wait until the terminal prompt is ready.
- Check the Terraform version currently available by running:
terraform -version
What does this check do?
The command reports the Terraform CLI version currently found on the shell path. The result determines which setup state applies to you.
✔️ I see the required version
Your current shell can already run Terraform CLI 1.16.4.
- Continue to the persistent installation below so the pinned binary lives under $HOME/bin.
ⓧ I see an older version
An older Terraform binary appears earlier on the current shell path. The persistent installation below adds the required release under $HOME/bin.
- Continue without removing the older binary.
- Use the final version check to confirm that 1.16.4 takes priority.
ⓧ Command not found
Terraform is absent from the current shell path. The persistent installation below downloads the required Linux archive into your CloudShell home directory.
- Continue to install Terraform CLI 1.16.4.
- Use the final version check to confirm the new binary is available.
The archive checksum proves that the downloaded file matches the release published by HashiCorp. The installation places the binary under persistent storage instead of an area CloudShell recycles.
- Create the persistent binary directory and lab workspace by running the commands below:
mkdir -p "$HOME/bin" "$HOME/ado-sql-lab"
cd "$HOME/ado-sql-lab"
curl --remote-name https://releases.hashicorp.com/terraform/1.16.4/terraform_1.16.4_linux_amd64.zip
printf '%s %s\n' 'dc94af0eef1147718ad7c8daea792ed199e3e0492eec180d0adafa2a65a879df' 'terraform_1.16.4_linux_amd64.zip' | sha256sum --check
unzip -o terraform_1.16.4_linux_amd64.zip -d "$HOME/bin"
export PATH="$HOME/bin:$PATH"
printf '\nexport PATH="$HOME/bin:$PATH"\n' >> "$HOME/.bashrc"
terraform -version
What does this installation do?
- The first two commands create $HOME/bin and make $HOME/ado-sql-lab the active workspace.
- The download command retrieves the official Linux AMD64 archive for Terraform CLI 1.16.4.
- The checksum command compares the archive with the published SHA-256 value before extraction.
- The path commands make the pinned binary available now and in later CloudShell sessions.
You should see the archive pass its checksum check. The final command should print Terraform v1.16.4.
Terraform version still incorrect?
Close the current CloudShell session if the older path remains cached. Launch CloudShell again in us-east-1 so the updated shell profile loads.
If the download or checksum fails, delete the incomplete archive before running the installation commands again.
Help me diagnose my CloudShell Terraform installation
Create the EC2 key pair
An Amazon EC2 RSA key pair lets Fleet Manager decrypt the Windows administrator password later. AWS stores the public key while the private PEM file stays in your CloudShell workspace.
Private keys deserve extra care. The command redirects the key material straight into ado-sql-lab.pem so the secret does not appear in normal terminal output.
- Create the EC2 key pair and protect its private key by running:
aws ec2 create-key-pair --key-name ado-sql-lab --key-type rsa --key-format pem --query "KeyMaterial" --output text > ado-sql-lab.pem
chmod 400 ado-sql-lab.pem
What do these commands do?
- The first command creates the RSA key pair named ado-sql-lab in EC2.
- The output redirection saves the private key as $HOME/ado-sql-lab/ado-sql-lab.pem.
- The permission command limits the PEM file to access by your CloudShell user.
- Confirm the protected PEM file exists by running:
ls -l ado-sql-lab.pem
What confirms the key is protected?
The file listing should show ado-sql-lab.pem with read access limited to its owner. Keep the file contents private because this PEM file can unlock the Windows administrator password.
That sensitive part is complete. Your key pair now exists in AWS while its protected private key remains inside the persistent lab workspace.
Key-pair creation failed?
A duplicate-name failure usually means an EC2 key pair named ado-sql-lab already exists in us-east-1. Remove that unused key pair before retrying only when you are certain no other instance depends on it.
A permissions failure means your AWS identity cannot create EC2 key pairs. Ask your AWS administrator to grant the required EC2 permission.
Help me troubleshoot the EC2 key-pair command
Build the Terraform configuration
Infrastructure as code records the lab network and Windows nodes as versioned configuration. These files also pin Terraform CLI 1.16.4 and HashiCorp AWS Provider 6.66.0 so later runs use the intended interfaces.
Why use a browser workspace?
A local Terraform installation can behave differently across operating systems. CloudShell gives everyone the same browser-hosted Linux environment with AWS credentials already available.
Keeping the files under $HOME/ado-sql-lab preserves the configuration between CloudShell sessions in this Region. The workspace becomes the repeatable source for the disposable lab.
Finding every brace in a long infrastructure file can feel fiddly. The full-file references below give each destination filename and its exact final contents.
- Open the five Terraform source files in the CloudShell visual editor by running each command below:
edit terraform.tf
edit variables.tf
edit main.tf
edit outputs.tf
edit terraform.tfvars.example
What does the edit command do?
Each command opens the named file in CloudShell's visual file editor. A missing file is created when you save its contents.
✔️ I have the files ready
Your five editor tabs are ready for their matching Terraform content. Use the filenames to keep each block in the correct file.
ⓧ I'd like to double check the full code
Use this reference to compare every character in each file.
terraform.tf
terraform {
required_version = "= 1.16.4"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.66.0"
}
}
}
provider "aws" {
region = var.aws_region
}
What does terraform.tf control?
This file pins the Terraform and AWS provider versions. It also connects the provider Region to aws_region.
variables.tf
variable "aws_region" {
description = "AWS Region for the temporary lab"
type = string
}
variable "key_name" {
description = "Existing EC2 RSA key-pair name"
type = string
}
variable "instance_type" {
description = "EC2 instance type for both Windows nodes"
type = string
default = "t3.2xlarge"
}
variable "root_volume_size" {
description = "Root gp3 volume size in GiB for each node"
type = number
default = 100
}
What does variables.tf control?
This file defines the Region and key-pair inputs. It also sets the shared instance type and root-volume size for both Windows nodes.
main.tf
data "aws_ssm_parameter" "windows_server_2022" {
name = "/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base"
}
locals {
nodes = {
primary = "ado-sql-primary"
secondary = "ado-sql-secondary"
}
}
resource "aws_vpc" "lab" {
cidr_block = "10.42.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "ado-sql-lab"
Project = "ado-sql-lab"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.lab.id
cidr_block = "10.42.10.0/24"
map_public_ip_on_launch = true
tags = {
Name = "ado-sql-lab-public"
Project = "ado-sql-lab"
}
}
resource "aws_internet_gateway" "lab" {
vpc_id = aws_vpc.lab.id
tags = {
Name = "ado-sql-lab"
Project = "ado-sql-lab"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.lab.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.lab.id
}
tags = {
Name = "ado-sql-lab-public"
Project = "ado-sql-lab"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_security_group" "lab" {
name = "ado-sql-lab"
description = "No inbound access; Fleet Manager provides administrative access"
vpc_id = aws_vpc.lab.id
tags = {
Name = "ado-sql-lab"
Project = "ado-sql-lab"
}
}
resource "aws_vpc_security_group_egress_rule" "internet" {
security_group_id = aws_security_group.lab.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
resource "aws_iam_role" "ssm" {
name = "ado-sql-lab-ssm"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
tags = {
Project = "ado-sql-lab"
}
}
resource "aws_iam_role_policy_attachment" "ssm_core" {
role = aws_iam_role.ssm.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ssm" {
name = "ado-sql-lab-ssm"
role = aws_iam_role.ssm.name
}
resource "aws_instance" "windows" {
for_each = local.nodes
ami = data.aws_ssm_parameter.windows_server_2022.insecure_value
instance_type = var.instance_type
key_name = var.key_name
subnet_id = aws_subnet.public.id
associate_public_ip_address = true
vpc_security_group_ids = [aws_security_group.lab.id]
iam_instance_profile = aws_iam_instance_profile.ssm.name
root_block_device {
volume_type = "gp3"
volume_size = var.root_volume_size
encrypted = true
delete_on_termination = true
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
}
tags = {
Name = each.value
Project = "ado-sql-lab"
Role = each.key
}
depends_on = [aws_iam_role_policy_attachment.ssm_core]
}
What does main.tf describe?
This file describes the lab VPC and its outbound network path. It also defines the IAM access that lets both Windows nodes register with AWS Systems Manager.
The final resource declares the primary and secondary EC2 instances with encrypted 100 GiB gp3 root volumes.
outputs.tf
output "primary_instance_id" {
value = aws_instance.windows["primary"].id
}
output "primary_private_ip" {
value = aws_instance.windows["primary"].private_ip
}
output "primary_public_ip" {
value = aws_instance.windows["primary"].public_ip
}
output "secondary_instance_id" {
value = aws_instance.windows["secondary"].id
}
output "secondary_private_ip" {
value = aws_instance.windows["secondary"].private_ip
}
output "secondary_public_ip" {
value = aws_instance.windows["secondary"].public_ip
}
What does outputs.tf expose?
This file makes both instance IDs and their IP addresses available after provisioning. Those values identify the nodes you connect to in the next step.
terraform.tfvars.example
aws_region = "us-east-1"
key_name = "ado-sql-lab"
instance_type = "t3.2xlarge"
root_volume_size = 100
What does the example file provide?
This file records the exact variable values used for the lab. It stays as a reusable template for rebuilding the workspace.
terraform.tfvars
aws_region = "us-east-1"
key_name = "ado-sql-lab"
instance_type = "t3.2xlarge"
root_volume_size = 100
What does terraform.tfvars provide?
Terraform reads this file as the active values for the workspace. Its Region matches the CloudShell session while its key name matches the EC2 key pair you created.
- Populate terraform.tf with its matching full-file reference.
- Populate variables.tf with its matching full-file reference.
- Return to the CloudShell terminal and confirm the first pair exists by running:
ls terraform.tf variables.tf
What should this list show?
The output should list terraform.tf and variables.tf. This confirms the version and input declarations are saved in the active workspace.
- Populate main.tf with its matching full-file reference.
- Populate outputs.tf with its matching full-file reference.
- Return to the CloudShell terminal and confirm the infrastructure pair exists by running:
ls main.tf outputs.tf
What should this list show?
The output should list main.tf and outputs.tf. The workspace now contains the resource declarations and the values Terraform returns after provisioning.
- Populate terraform.tfvars.example with its matching full-file reference.
- Create the active variable file from the example by running:
cp terraform.tfvars.example terraform.tfvars
What does this copy do?
The command preserves the reusable example while creating terraform.tfvars for the active workspace. Both files contain the same Region and lab sizing values.
Before you run the final check, which outputs would prove that the pinned CLI, active directory, configuration files, and private key are all ready?
- Verify the complete browser-only workspace by running:
terraform -version
pwd
ls terraform.tf variables.tf main.tf outputs.tf terraform.tfvars.example terraform.tfvars ado-sql-lab.pem
What confirms the workspace is ready?
- The version output begins with Terraform v1.16.4.
- The active path ends with ado-sql-lab under your CloudShell home directory.
- The file listing contains all six Terraform files and ado-sql-lab.pem.
Strong start. Your persistent CloudShell workspace now contains the pinned Terraform CLI, the protected EC2 private key, and the complete lab configuration.
A workspace file is missing?
Check that the active path ends with ado-sql-lab before recreating the missing file. A file saved elsewhere in $HOME will not appear in this listing.
Compare the missing file with the full-code tab. Pay particular attention to its exact filename and extension.
Help me repair my Terraform workspace
Your browser-only workspace is ready. Next, you will review the plan and provision the two Windows nodes from these Terraform files.
Provision the Two Windows Nodes
Your browser workspace now holds a pinned Terraform configuration for the lab. This step turns that plan into two reachable Windows Server 2022 hosts.
A reachable desktop proves the AWS infrastructure layer works. You will also inspect the software layer so the next requirement is based on evidence.
In this step, get ready to:
- Check the Terraform configuration before creating resources.
- Provision the two-node AWS lab from the reviewed plan.
- Confirm both Windows desktops expose the expected software baseline.
Run the Terraform preflight
A Terraform preflight catches formatting or dependency problems before AWS receives a request. The plan also gives you a final review of every resource that Terraform intends to create.
- Switch back to the AWS CloudShell environment from the previous step.
- Initialize the provider and workspace metadata by running this command:
terraform init
What does initialization do?
Initialization prepares the existing workspace to use its pinned AWS provider. It does not create any cloud resources.
- Normalize the Terraform files by running these commands:
terraform fmt
terraform fmt -check
What does the format check prove?
- The first command applies Terraform's canonical layout to the existing configuration.
- The second command confirms that every Terraform file now follows that layout.
- Validate the configuration and build the proposed execution plan by running:
terraform validate
terraform plan
What should the plan contain?
- The network layer includes the ado-sql-lab VPC plus its public subnet.
- The route layer includes an internet gateway plus a public route table.
- The security group defines outbound access without an inbound rule.
- The identity layer includes the ado-sql-lab-ssm role plus its instance profile.
- The compute layer includes ado-sql-primary plus ado-sql-secondary.
The validation should complete successfully. The plan should show two Windows instances with encrypted gp3 root volumes.
Preflight not completing?
Confirm that CloudShell is still using $HOME/ado-sql-lab as its active directory. A different directory will not contain the configuration files from the previous step.
Check that terraform.tfvars still contains the selected AWS Region plus the ado-sql-lab key-pair name.
Help me diagnose my Terraform initialization or plan failure.
- Use the tabs below to confirm that the formatted files still match the lab configuration.
✔️ My configuration matches
Your five Terraform files are ready. Keep the successful plan visible for the resource review in the next substep.
ⓧ I'd like to double check the full code
Compare each file with the complete formatted configuration below. Preserve every identifier plus every value.
terraform {
required_version = "= 1.16.4"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.66.0"
}
}
}
provider "aws" {
region = var.aws_region
}
This file pins Terraform plus the AWS provider before setting the selected Region.
variable "aws_region" {
description = "AWS Region for the temporary lab"
type = string
}
variable "key_name" {
description = "Existing EC2 RSA key-pair name"
type = string
}
variable "instance_type" {
description = "EC2 instance type for both Windows nodes"
type = string
default = "t3.2xlarge"
}
variable "root_volume_size" {
description = "Root gp3 volume size in GiB for each node"
type = number
default = 100
}
This file defines the four values used to size and locate the temporary lab.
data "aws_ssm_parameter" "windows_server_2022" {
name = "/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base"
}
locals {
nodes = {
primary = "ado-sql-primary"
secondary = "ado-sql-secondary"
}
}
resource "aws_vpc" "lab" {
cidr_block = "10.42.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "ado-sql-lab"
Project = "ado-sql-lab"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.lab.id
cidr_block = "10.42.10.0/24"
map_public_ip_on_launch = true
tags = {
Name = "ado-sql-lab-public"
Project = "ado-sql-lab"
}
}
resource "aws_internet_gateway" "lab" {
vpc_id = aws_vpc.lab.id
tags = {
Name = "ado-sql-lab"
Project = "ado-sql-lab"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.lab.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.lab.id
}
tags = {
Name = "ado-sql-lab-public"
Project = "ado-sql-lab"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_security_group" "lab" {
name = "ado-sql-lab"
description = "No inbound access; Fleet Manager provides administrative access"
vpc_id = aws_vpc.lab.id
tags = {
Name = "ado-sql-lab"
Project = "ado-sql-lab"
}
}
resource "aws_vpc_security_group_egress_rule" "internet" {
security_group_id = aws_security_group.lab.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
resource "aws_iam_role" "ssm" {
name = "ado-sql-lab-ssm"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{ Effect = "Allow", Principal = { Service = "ec2.amazonaws.com" }, Action = "sts:AssumeRole" }]
})
tags = { Project = "ado-sql-lab" }
}
resource "aws_iam_role_policy_attachment" "ssm_core" {
role = aws_iam_role.ssm.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ssm" {
name = "ado-sql-lab-ssm"
role = aws_iam_role.ssm.name
}
resource "aws_instance" "windows" {
for_each = local.nodes
ami = data.aws_ssm_parameter.windows_server_2022.insecure_value
instance_type = var.instance_type
key_name = var.key_name
subnet_id = aws_subnet.public.id
associate_public_ip_address = true
vpc_security_group_ids = [aws_security_group.lab.id]
iam_instance_profile = aws_iam_instance_profile.ssm.name
root_block_device {
volume_type = "gp3"
volume_size = var.root_volume_size
encrypted = true
delete_on_termination = true
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
}
tags = { Name = each.value, Project = "ado-sql-lab", Role = each.key }
depends_on = [aws_iam_role_policy_attachment.ssm_core]
}
This file defines the network, access profile, security boundary, storage settings, and two Windows nodes.
output "primary_instance_id" { value = aws_instance.windows["primary"].id }
output "primary_private_ip" { value = aws_instance.windows["primary"].private_ip }
output "primary_public_ip" { value = aws_instance.windows["primary"].public_ip }
output "secondary_instance_id" { value = aws_instance.windows["secondary"].id }
output "secondary_private_ip" { value = aws_instance.windows["secondary"].private_ip }
output "secondary_public_ip" { value = aws_instance.windows["secondary"].public_ip }
This file exposes the identifiers plus network addresses needed to distinguish the two nodes.
aws_region = "us-east-1"
key_name = "ado-sql-lab"
instance_type = "t3.2xlarge"
root_volume_size = 100
This example records the expected shape and default sizing of the lab values.
aws_region = "us-east-1"
key_name = "ado-sql-lab"
instance_type = "t3.2xlarge"
root_volume_size = 100
This file supplies the active Region, key pair, instance type, and root-volume size.
Apply the plan and record the outputs
Applying this plan creates billable Amazon EC2 instances plus Amazon EBS storage. Keep the lab short-lived because the two t3.2xlarge instances begin incurring usage charges while they run.
Terraform pauses for confirmation after presenting the plan. This gives you one final chance to check the resource names before creation begins.
Before you apply the plan, how many Windows instances do you expect Terraform to create?
- Create the reviewed infrastructure by running this command:
terraform apply
What happens during the apply?
Terraform presents the execution plan before asking for confirmation. After approval, it creates the network and access resources before launching both Windows nodes.
- Confirm the apply when Terraform asks for approval.
Provisioning can take several minutes while AWS launches both hosts. The command eventually reports that the apply completed.
- Display the instance identifiers plus network addresses by running:
terraform output
Why save these outputs?
The instance IDs identify the primary and secondary resources in the AWS console. The private IP addresses become the stable node references inside the lab network.
- Record the primary instance ID as your primary instance ID.
- Record the primary private IP address as your primary private IP address.
- Record the secondary instance ID as your secondary instance ID.
- Record the secondary private IP address as your secondary private IP address.
Good work. Terraform now tracks the complete two-node AWS infrastructure baseline.
Apply not completing?
Check the first error in the CloudShell output. Common causes include missing account permissions or unavailable t3.2xlarge capacity in the selected Region.
Confirm that the ado-sql-lab key pair still exists in the same Region as the provider configuration.
Help me diagnose my Terraform apply failure.
Connect to both Windows desktops
AWS Systems Manager Fleet Manager carries each Remote Desktop connection through Systems Manager. The security group therefore needs no inbound RDP rule.
Using the PEM file touches a sensitive credential. You only download a temporary local copy so the browser can provide it to Fleet Manager.
- Switch back to the CloudShell environment from earlier.
- Choose Actions in the CloudShell interface.
- Choose Download file.
- Enter $HOME/ado-sql-lab/ado-sql-lab.pem in the Download file dialog.
- Choose Download.
- Save the downloaded PEM file in a private local folder.
Why download another copy?
The original private key remains in CloudShell's persistent home directory. Fleet Manager's browser file selector uses the temporary copy stored on your device.
- Return to the AWS Management Console from earlier.
- Choose Instances from the Amazon EC2 navigation pane.
- Select ado-sql-primary.
- Choose Connect.
- Select the RDP client tab.
- Choose Connect using Fleet Manager for Connection Type.
- Choose Fleet Manager Remote Desktop.
Where are you now?
The Fleet Manager Remote Desktop page opens in Systems Manager for the selected primary node. The next controls authenticate the browser session without exposing an inbound network port.
The first connection can take a few minutes while the new instance finishes registering as a managed node. A short wait here is expected.
- Select Key pair for Authentication type.
- Choose Browse local machine.
- Select the local ado-sql-lab.pem file.
- Choose Connect.
You should see the primary Windows desktop inside the browser. That session proves the primary node is running through the Systems Manager access path.
- Switch back to the Amazon EC2 Instances page.
- Select ado-sql-secondary.
- Choose Connect.
- Select the RDP client tab.
- Choose Connect using Fleet Manager for Connection Type.
- Choose Fleet Manager Remote Desktop.
The second Fleet Manager Remote Desktop page now targets the secondary node. Use the same local PEM copy for its browser session.
- Select Key pair for Authentication type.
- Choose Browse local machine.
- Select the local ado-sql-lab.pem file.
- Choose Connect.
You should now have browser sessions for both Windows desktops. Confirm that each host reports Windows Server 2022 in its system information.
Before you inspect the Windows services, do you think infrastructure provisioning also installed a database engine?
- Use the taskbar search in the primary desktop to open Services.
- Look for SQL Server (MSSQLSERVER) in the primary services list.
- Use the taskbar search in the secondary desktop to open Services.
- Look for SQL Server (MSSQLSERVER) in the secondary services list.
You should find no SQL Server (MSSQLSERVER) service on either node. This intended gap proves that Terraform delivered the Windows infrastructure without creating the SQL data tier.
Fleet Manager not connecting?
Allow a few more minutes for each new Windows instance to register with Systems Manager. Confirm that the console Region matches the Region used by Terraform.
Check that the instance uses the ado-sql-lab-ssm instance profile. The Systems Manager access path depends on that profile.
Help me troubleshoot my Fleet Manager connection.
Both Windows hosts are live through the browser. Next up, you will install the SQL engine plus its recovery-job services on both nodes.
Install SQL Server on Both Nodes
Your two Windows Server 2022 nodes are online through AWS Systems Manager Fleet Manager. They still lack the database layer required for the recovery lab.
Full Azure DevOps Server needs SQL Server to store its configuration data. SQL Server Agent provides the scheduled job service needed for later recovery exercises.
In this step, get ready to:
- Download SQL Server 2022 installation media on both Windows nodes.
- Install the required database features on both nodes.
- Verify the edition and major version on both nodes.
Download SQL Server on both nodes
SQL Server 2022 Developer supplies the database engine required by the lab. Its license permits non-production development and testing.
Why Developer edition?
The later recovery exercises depend on SQL Server Agent. Developer edition provides the required engine features for this temporary learning environment.
The full Azure DevOps deployment can use this SQL instance without limiting the lab to SQL Express.
The installation media is large, so a quiet download window for several minutes is normal.
- Switch back to the two Fleet Manager desktop sessions from earlier.
- Use Windows taskbar search in each session to open its installed web browser.
- Open the official Microsoft Evaluation Center on each node.
- Download the SQL Server 2022 64-bit installation media on each node.
Windows may request permission when the installer starts. This permission allows the Microsoft installer to make system changes.
- Start the downloaded SQL Server media on the primary node.
- Approve the Windows permission request if it appears.
- Repeat the installer launch on the secondary node.
- Choose Custom in each installation flow.
You should reach the Edition page in both sessions.
- Select Specify a free edition on both nodes.
- Choose Developer as the edition on both nodes.
- Continue to the feature choices on both nodes.
Install the required SQL features
Each selected feature supports a specific part of the lab. The database engine stores Azure DevOps data while Full-Text supports its database requirements.
- Select Database Engine Services in each setup wizard.
- Select Full-Text and Semantic Extractions for Search in each setup wizard.
Both selected features should now appear in the feature summary on each node.
- Select Client Tools Connectivity in each setup wizard.
- Keep MSSQLSERVER as the default instance on each node.
The two installations now use the same default-instance layout. This keeps later database commands consistent across the lab.
- Choose Windows authentication on each node.
- Add the current Windows administrator as a SQL administrator on each node.
The administrator account from each Fleet Manager session can now manage its local SQL instance.
This is the slow stretch of the step. Each installer may spend several minutes copying components and configuring Windows services.
- Set the SQL Server Agent startup setting to Automatic on each node.
- Start the installation on both nodes.
- Wait until each setup wizard reports successful completion.
- Return to the Services window from earlier on each node.
- Find SQL Server (MSSQLSERVER) on each node.
- Find SQL Server Agent (MSSQLSERVER) on each node.
- Start either service from the Services window if its status is stopped.
Both services should show a running status on both nodes. That is the installation complete across the entire SQL data tier.
Missing a service or running status?
- Reopen SQL Server setup if SQL Server (MSSQLSERVER) is missing.
- Confirm that Database Engine Services was selected during setup.
- Check that SQL Server Agent uses the Automatic startup setting if its service remains stopped.
Help me diagnose a missing or stopped SQL Server service on Windows Server.
Verify each SQL installation
A running Windows service proves that the engine started. The SQL metadata check now confirms the installed edition and major version from inside each database engine.
- Select the Windows taskbar search in the primary desktop.
- Enter command prompt in the search field.
- Press Enter to open the result.
- Repeat the search steps on the secondary desktop.
- Connect each command prompt to its local default SQL instance by running:
sqlcmd
What does this command do?
The sqlcmd utility connects to the local default SQL instance with Windows authentication. It opens an interactive SQL prompt for the metadata query.
Before you run the query, do you expect both nodes to report matching editions and major versions?
- Query each local database engine by entering this statement at its interactive prompt:
SELECT SERVERPROPERTY('MachineName') AS ComputerName,
SERVERPROPERTY('ServerName') AS InstanceName,
SERVERPROPERTY('Edition') AS Edition,
SERVERPROPERTY('ProductVersion') AS ProductVersion,
SERVERPROPERTY('ProductLevel') AS ProductLevel;
GO
What does this query check?
- The machine and instance properties identify which node answered the query.
- The edition property confirms that each node uses Developer edition.
- The product version property reveals the installed SQL Server major version.
- The product level property reports the servicing level of the installation.
On both nodes, you should see a ProductVersion beginning with 16.. The Edition value should contain Developer.
You have now proved that both Windows nodes run the same SQL Server edition and major version. Their SQL Server Agent services are also ready for scheduled recovery jobs.
Cannot run the SQL metadata query?
- Confirm that the command prompt is running on the same node as the SQL installation.
- Check the Services window if the local database connection does not open.
- Verify that you entered the query in the interactive SQL prompt on both nodes.
Help me troubleshoot the local SQL metadata check.
Your two-node SQL data tier is ready. Next, you will turn the primary into the Azure DevOps application and database host.
Configure and Patch Azure DevOps Server
Both Windows nodes now run SQL Server 2022 Developer. The primary still needs the application tier that turns its SQL engine into a working Azure DevOps Server deployment.
SQL alone does not create the coordinated configuration and collection databases required for the later backup incident. This step turns the primary into the application and data tier while preserving the secondary as a clean SQL standby.
In this step, get ready to:
- Validate the full Azure DevOps Server installer with its published SHA-256 hash.
- Configure a Basic deployment against the primary node's existing SQL instance.
- Bring the deployment to 2022.2 Patch 12 and confirm its databases.
Why Full Azure DevOps Server?
Full Azure DevOps Server can use the existing SQL Server Developer instance on the primary. The later recovery labs depend on SQL Server Agent plus a non-Express SQL edition for scheduled jobs and log shipping.
Azure DevOps Server Express centers its Basic deployment on SQL Express. The full server keeps this lab aligned with the recovery topology you are building.
Verify the full installer
A SHA-256 hash is a fingerprint calculated from a file's contents. A match proves that the downloaded EXE is the published full-server installer before it changes the primary node.
- Switch back to the open AWS Systems Manager Fleet Manager Remote Desktop session for ado-sql-primary.
- Open the official Azure DevOps Server download page in the primary node's browser.
- Choose the full Azure DevOps Server 2022.2 EXE entry.
- Skip the Azure DevOps Server Express 2022.2 EXE entry.
- Save the full EXE in the browser's default download location.
PowerShell can calculate the file's hash without opening the installer. The command below requests the downloaded EXE path after it starts.
- Press the Windows key in the remote desktop.
- Type PowerShell into Windows search.
- Press Enter to open PowerShell.
- Prepare PowerShell to verify the downloaded EXE by running this command:
Get-FileHash -Algorithm SHA256
What Does This Command Do?
The command calculates a SHA-256 digest for the file path you provide. You can compare that digest with Microsoft's published value before running the installer.
- Paste the full installer's path when PowerShell requests it.
- Press Enter to calculate the digest.
- Compare the printed digest with 7DAB9F7BFFDD7BAF523533A800A019B1A114F015941E753450B52DD9FDA29245.
- Stop the installation if the two values differ.
- Run the downloaded full-server EXE after the values match.
- Approve the Windows administrator prompt if it appears.
- Complete the installation so Azure DevOps Server Configuration Center opens.
Installer Hash Does Not Match?
- Delete the downloaded EXE with the mismatched digest.
- Download the full-server EXE again from the official page.
Check that you selected the full server entry before comparing the new digest. Help me troubleshoot an Azure DevOps Server installer hash mismatch.
Configure the primary deployment
Basic configuration creates the first project collection while connecting the application tier to an existing SQL instance. The readiness checks protect the deployment from moving ahead with an unsupported or incomplete configuration.
Readiness checks can be fiddly because one failed prerequisite blocks configuration. Resolve each reported issue on the primary before continuing.
- Select New Deployment - Basic in Azure DevOps Server Configuration Center.
- Choose Use an existing SQL Server Instance.
- Select the primary node's local default SQL Server instance.
- Keep DefaultCollection as the project collection name.
- Leave Search unconfigured to reduce resource use in this temporary lab.
- Run the readiness checks.
- Resolve every reported readiness error.
- Rerun the readiness checks until every check passes.
- Start the configuration after all readiness checks pass.
- Keep the Remote Desktop session connected until configuration completes.
- Record the deployment URL shown by the wizard here: your Azure DevOps deployment URL.
- Open your Azure DevOps deployment URL in the primary node's browser.
You should see the Azure DevOps landing page load from the new deployment. That is the application tier alive and connected to the primary SQL data tier.
What Did Basic Configuration Create?
The Tfs_Configuration database stores deployment configuration. The collection database stores the projects and operational data associated with DefaultCollection.
Recovery depends on keeping the configuration database synchronized with every collection database. This pair becomes the baseline for the next backup lab.
Configuration Checks Still Failing?
- Confirm you selected the local default SQL Server instance on ado-sql-primary.
- Confirm Database Engine Services and Full-Text remain installed on the primary.
- Confirm the current Windows administrator still has SQL administrator access.
Use the exact readiness message from Configuration Center when asking for help. Help me resolve my Azure DevOps Server readiness check.
Install Patch 12 and verify the databases
The base deployment now works. Patch verification brings it onto the current supported 2022.2 Patch 12 servicing level before you record the database baseline.
- Open the official Azure DevOps Server 2022 Update 2 release notes in the primary node's browser.
- Find the download for Patch 12.
- Download devops2022.2patch12.exe.
- Switch back to PowerShell from earlier.
- Prepare PowerShell to verify the patch EXE by running this command:
Get-FileHash -Algorithm SHA256
Why Verify the Patch Again?
The patch is a separate executable with its own published digest. Verifying it establishes that both installation stages used the expected Microsoft files.
- Paste the path to devops2022.2patch12.exe when PowerShell requests it.
- Press Enter to calculate the patch digest.
- Compare the printed digest with EFB629EBE8B5A7AF4526F8F79A6EE7BC4F610B5057C0B051A84F320D2B6D1606.
- Stop the patch installation if the two values differ.
- Run devops2022.2patch12.exe after the values match.
- Approve the Windows administrator prompt if it appears.
- Complete the patch installation.
- Return to the primary node's browser.
- Reload your Azure DevOps deployment URL.
The Azure DevOps landing page should load again after patching. The application check proves that the serviced deployment still reaches its SQL data tier.
Landing Page Does Not Return?
- Confirm the patch installer reported completion before reloading the page.
- Return to Azure DevOps Server Configuration Center to confirm the deployment remains configured.
- Retry the recorded deployment URL from the browser on ado-sql-primary.
Share the patch stage that failed when requesting support. Help me troubleshoot the Azure DevOps landing page after Patch 12.
The landing page checks the application layer. A query against sys.databases now confirms the configuration and collection databases underneath it.
- Predict which Azure DevOps databases you expect to see on the primary.
- Start a local SQL session on ado-sql-primary by running this command:
sqlcmd
What Does This Command Do?
The command connects to the local default SQL Server instance with your current Windows identity. The interactive session accepts the database query next.
- List the primary instance's database metadata by running this query in the sqlcmd session:
SELECT name, database_id, create_date FROM sys.databases;
What Does This Query Show?
- The name column identifies every database on the primary instance.
- The database_id column provides the instance-level identifier for each database.
- The create_date column records when each database was created.
You should see Tfs_Configuration plus the collection database associated with DefaultCollection. These rows prove that Basic configuration created the coordinated SQL baseline.
- Record every returned Azure DevOps database row here: your database names, IDs, and creation dates.
Missing an Azure DevOps Database?
- Confirm the command prompt is running on ado-sql-primary.
- Confirm Azure DevOps Server Configuration Center reports a completed deployment.
- Run the database-list query again against the primary's local default SQL instance.
Keep the secondary untouched while diagnosing the primary. Help me find a missing Azure DevOps database on the primary.
That is the recovery baseline established: the patched Azure DevOps deployment loads while its database identities are recorded. The ado-sql-secondary node remains a clean SQL-only standby for the later restore and log-shipping labs.
Secret mission
Prove the Lab Survives a Cold Restart
A recovery lab needs evidence that its full stack returns after an infrastructure interruption. Stop both nodes, restart them, and compare every recovered layer with a saved baseline.
Clean Up Your Resources
Clean Up Your Resources
Your restarted lab is still using paid AWS infrastructure. Decide whether to keep it running, pause it for later, or delete it after reviewing the cost difference.
Cost warning
Running Windows Amazon EC2 instances incurs compute charges. Attached Amazon EBS volumes keep incurring storage charges while the instances are stopped.
Your t3.2xlarge instances launch in Unlimited mode. Sustained CPU use can add surplus CPU-credit charges.
A full Azure DevOps Server license is separate from the AWS charges.
- Choose Pause to stop compute charges while retaining the attached storage.
- Choose Delete to remove the AWS lab resources.
Resources you used:
- Terraform-managed VPC named ado-sql-lab.
- Public subnet named ado-sql-lab-public.
- Internet gateway attached to ado-sql-lab.
- Public route table for the lab network.
- Outbound-only security group named ado-sql-lab.
- IAM role named ado-sql-lab-ssm for AWS Systems Manager access.
- IAM instance profile named ado-sql-lab-ssm.
- Primary Windows instance named ado-sql-primary.
- Encrypted 100 GiB gp3 root volume attached to ado-sql-primary.
- Secondary Windows instance named ado-sql-secondary.
- Encrypted 100 GiB gp3 root volume attached to ado-sql-secondary.
- RSA EC2 key pair named ado-sql-lab.
- Private PEM file stored at $HOME/ado-sql-lab/ado-sql-lab.pem in AWS CloudShell.
Keep everything running
No action needed. Choose this if you are continuing directly into the backup labs.
- Keep both lab instances running.
- Retain the Terraform state in $HOME/ado-sql-lab.
- Retain the private PEM file in $HOME/ado-sql-lab.
- Retain the baseline note containing the matching database lists.
- Continue directly into Lab 2.
- Monitor your AWS charges while the lab remains active.
Both Windows nodes remain ready for the marked-transaction backup exercises. Compute usage can continue generating charges until you stop or destroy the instances.
Pause - I'll come back to this later
Stop both Windows nodes to end their instance usage charges. Keep the lab files so you can resume from the same baseline.
- Return to the EC2 instance list in the signed-in AWS Console.
- Select both lab instances in the instance table.
- Stop the selected instances using the instance controls.
- Wait until each instance shows a stopped state.
- Keep the Terraform state in $HOME/ado-sql-lab.
- Keep ado-sql-lab.pem in the same workspace.
- Keep the baseline note for the next lab.
Your two EC2 nodes are now paused. The attached EBS volumes continue generating storage charges.
Delete - I don't want to use this again
Deleting the stack permanently removes the lab infrastructure. The destroy preview gives you one final check before Terraform removes the Windows nodes.
- Return to the $HOME/ado-sql-lab workspace in the CloudShell session from earlier.
- Preview every Terraform-managed deletion by running this command:
terraform plan -destroy
What does this command do?
Terraform compares the saved state with the current AWS resources. The -destroy option produces a removal plan without executing it.
- Review the preview for both EC2 instances.
- Confirm the preview includes the lab networking resources.
- Confirm the preview includes the IAM role.
- Confirm the preview includes the IAM instance profile.
Destroy preview fails?
- Confirm the CloudShell prompt is using $HOME/ado-sql-lab.
- Confirm the Terraform state from the provisioned lab remains in that workspace.
- Help me diagnose the destroy preview.
Terraform stays busy while EC2 terminates both Windows nodes. Keep the CloudShell session available until the teardown finishes.
- Read Terraform's destruction summary when the command pauses for confirmation.
- Approve the destruction only if the summary matches the reviewed preview.
- Start the destruction by running this command:
terraform destroy
What does this command remove?
Terraform removes the resources recorded in the lab state. Terminating each EC2 instance also deletes its root volume because the configuration sets delete_on_termination to true.
- Wait for Terraform to report that destruction completed without errors.
- Return to the EC2 instance list in the AWS Console.
- Confirm no instances named ado-sql-primary remain.
- Confirm no instances named ado-sql-secondary remain.
- Check the EC2 volume list for the lab root volumes.
- Confirm no lab root volumes remain.
Terraform cannot finish the teardown?
- Keep the CloudShell session available so Terraform can preserve its progress.
- Use the failed resource name from the terminal output to identify the blocked deletion.
- Help me finish the Terraform teardown.
- Delete the EC2 key pair created outside Terraform by running this command:
aws ec2 delete-key-pair --key-name ado-sql-lab
What does this command remove?
This removes the account-side registration for the ado-sql-lab key pair. The PEM file remains in CloudShell until the next command.
- Return to the EC2 key-pair list in the AWS Console.
- Confirm ado-sql-lab no longer appears.
Key pair deletion fails?
- Confirm the CloudShell session still uses the AWS account that created the key pair.
- Confirm the CloudShell session still uses the target Region.
- Help me remove the EC2 key pair.
- Remove the local private key by running this command:
rm -f $HOME/ado-sql-lab/ado-sql-lab.pem
What does this command remove?
This deletes the private PEM from CloudShell storage. Your Terraform configuration remains in $HOME/ado-sql-lab.
The Terraform state also remains as a record of the completed teardown.
- Check the files shown for $HOME/ado-sql-lab in CloudShell.
- Confirm ado-sql-lab.pem is absent.
Cleanup complete. Terraform has removed the billable lab stack.
The EC2 key pair is gone. The local PEM is gone.
Nice Work!
Nice Work!
Excellent work! Your reproducible two-node recovery lab on AWS returned cleanly from a controlled cold restart.
You've learned how to:
- Created a browser-operated infrastructure workflow with Terraform 1.16.4 inside AWS CloudShell. Pinned HashiCorp AWS Provider 6.66.0 in versioned files so the lab can be recreated consistently.
- Provisioned two Windows Server nodes on Amazon EC2 with no inbound RDP exposure. Reached both desktops through AWS Systems Manager Fleet Manager. Installed the Developer edition of SQL Server 2022 on each node. Set SQL Server Agent to start automatically.
- Configured a full Azure DevOps deployment on ado-sql-primary. Applied Azure DevOps Server 2022.2 Patch 12. Recorded Tfs_Configuration plus DefaultCollection as the database baseline for later recovery labs. Preserved ado-sql-secondary as a clean SQL-only standby.
- Completed the Secret Mission cold-restart test by stopping both nodes in Amazon EC2. Restarted both nodes after they reached the stopped state. Verified SQL Server on each node. Verified SQL Server Agent on each node. Loaded Azure DevOps Server on the primary. Matched the post-restart database list to the recorded baseline.
Ready to quiz yourself?