camzabriskie.com

Primers › Deployment

Deploying to an on-prem Windows server

01 / 02 updated Oct 12, 2026
On this page
  1. A few terms
  2. Who opens the connection
  3. CodeDeploy on a non-AWS server
    1. The hook script
    2. Registering the server ${only: AWS}
  4. Ansible over WinRM
    1. The control node can’t be Windows
    2. Authentication
  5. Systems Manager Run Command
  6. Which one to pick
  7. References

Say a team builds its app in AWS, and one of the places it runs is a Windows server in a company data center, app01.example.com. Every release produces a new installer, reporting-service-2.4.0.msi, and the release pipeline has to get it installed on app01 without anyone logging in to click through it. Three tools can do that job, and the biggest difference between them is which machine opens the network connection. That direction decides what the data center’s firewall has to allow, and the firewall usually decides the answer.

A few terms

Who opens the connection

The arrows point from the machine that opens the connection. With both agents, app01 reaches out and asks for work, so the firewall only has to let it out. With Ansible, the control node reaches in, so the firewall has to let something in.

Both agents work the same way: the agent on app01 opens a connection out to the AWS service and asks whether there’s anything to do, and the work comes back over that connection. Nothing outside ever connects in to the server [6], [7]. Ansible is the other way around. The control node connects to WinRM on the server and runs the steps over that connection [8], so a path from the control node to app01 has to exist and the firewall has to allow it.

CodeDeployAnsible over WinRMSystems Manager
Who opens the connectionThe agent on the serverThe control nodeThe agent on the server
Inbound rule on the serverNoneTCP 5985 (HTTP) or 5986 (HTTPS)None
Outbound from the serverHTTPS 443 to AWSNoneHTTPS 443 to AWS
Runs an installerYes, from a hook scriptYesYes, as a command
Runs a playbook against the serverOnly through a Linux control nodeYesOnly through a Linux control node

The rest of this primer takes them one at a time, then how to choose.

CodeDeploy on a non-AWS server

AWS only

CodeDeploy treats any machine that can run its agent and reach AWS’s public endpoints as an on-premises instance, and deploys to it the same way it deploys to EC2 [4], [9]. The agent is tested on Windows Server 2016, 2019, 2022, and 2025 [6], and its only network requirement is outbound HTTPS on port 443 [9].

A deployment is a bundle of files with an appspec.yml at its root. The AppSpec file says where the files go and which scripts to run at each lifecycle event, the named steps of a deployment such as BeforeInstall, AfterInstall, and ValidateService [10]. For app01, the bundle holds the MSI and one script:

version: 0.0
os: windows
files:
  - source: installer
    destination: C:\deploy\reporting-service
hooks:
  AfterInstall:
    - location: scripts\install-msi.ps1
      timeout: 1800

That’s the shape of AWS’s AppSpec example, with the Windows changes its docs call for: os: windows, a fully qualified destination path, and no runas setting, which Windows Server doesn’t support [11]. timeout is how long the script may run before it counts as failed. It can be at most 3600 seconds (an hour) for everything in one lifecycle event, and a script that runs past it fails the deployment on that server [10].

The hook script

CodeDeploy treats an exit code of 0 as success and anything else as failure [11]. Windows Installer doesn’t quite agree. It reports success with 0, and also with 3010 (installed, restart needed) and 1641 (installed, restart started) [12]. A script that passes msiexec’s exit code straight through fails a deployment that worked, so install-msi.ps1 translates it:

$msi = 'C:\deploy\reporting-service\reporting-service-2.4.0.msi'
$log = 'C:\deploy\reporting-service\install.log'

# /i installs, /qn shows no UI, /l*v writes a detailed log
$p = Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /qn /l*v `"$log`"" -Wait -PassThru

# 0, 3010, and 1641 all mean the install succeeded
if ($p.ExitCode -in 0, 3010, 1641) { exit 0 }
exit $p.ExitCode

msiexec’s /i installs the package, /qn turns off its user interface, and /l*v writes a log of everything it did [1]. -Wait makes Start-Process wait for the installer to finish, and -PassThru returns the process so the script can read its exit code [13]. Without -Wait, the script would return before the install did. A restart-required result still needs a restart scheduled somewhere, and this script deliberately leaves that decision to whoever owns the server.

Registering the server {only: AWS}

Before app01 can receive deployments, it has to be registered with CodeDeploy, using an administrator account on the server [9]. AWS recommends registering with an IAM session ARN, which gives the agent temporary credentials, for production deployments of any size. The other method, an IAM user with permanent access keys, is only meant for non-production use [14].

The temporary credentials expire after an hour at most, so something on your side has to keep replacing them, either a scheduled job that copies fresh credentials from your own identity system to the server or one on the server that requests them from AWS. If the credentials aren’t refreshed, deployments to the server fail [15]. That refresh job is the part of this option most likely to be forgotten, and the first thing to check when deployments that used to work start failing.

Ansible over WinRM

Ansible doesn’t install an agent on the machines it manages. The control node connects to each Windows host and runs the playbook’s steps over that connection. It can use WinRM, PowerShell remoting over WinRM, or SSH, which Ansible has officially supported for Windows since version 2.18 [8]. Targets need Windows Server 2016 or Windows 10 or newer [8].

WinRM listens on TCP 5985 for HTTP and 5986 for HTTPS [2], and both are inbound to the server. The control node needs a network path to app01, through a VPN or a private connection if it runs outside the data center, or none at all if it runs inside.

The control node can’t be Windows

Ansible’s control node has to be a UNIX-like machine with Python, such as Linux or macOS. Windows only works under WSL (the Windows Subsystem for Linux), and native Windows isn’t supported [3], [8]. So a playbook can’t simply run on app01 itself. It has to run from a Linux machine that reaches app01 over WinRM.

To check that the control node can reach the server and log in, Ansible has a test module that runs a trivial command over the connection [16]:

ansible app01.example.com -m ansible.windows.win_ping

The control node is always Linux or macOS, so this one has no PowerShell version.

Authentication

WinRM supports several ways to log in, and they differ in which accounts work and whether the traffic is protected without HTTPS [2]:

MethodLocal accountsDomain accountsEncrypted over plain HTTP
BasicYesNoNo
CertificateYesNoNo
NTLMYesYesYes
KerberosNoYesYes
CredSSPYesYesYes

Ansible’s guidance is Kerberos when the server is in a domain, and Basic or NTLM over HTTPS (port 5986) for local accounts. It warns against Basic or NTLM over an HTTP listener, since they offer no encryption or very weak encryption there, and it treats CredSSP as a last resort, because it hands the server credentials it can reuse elsewhere [2].

Systems Manager Run Command

AWS only

Run Command is the part of AWS Systems Manager that runs commands on managed nodes: EC2 instances, or other machines set up for Systems Manager [5]. Setting up app01 takes a hybrid activation, an activation code and ID you create in AWS with an IAM role attached, which you give to SSM Agent when you install it on the server [17].

The agent then connects out over HTTPS on port 443 to the Systems Manager endpoints (ssm, ssmmessages, and ec2messages in your Region). It opens every connection itself, so the firewall needs no inbound rule [7]. Running the installer is one Run Command invocation that executes the same PowerShell as the CodeDeploy hook above, without a bundle, an AppSpec file, or lifecycle events. Systems Manager can also run Ansible playbooks, but its AWS-ApplyAnsiblePlaybooks document runs Ansible on the managed node itself, and the dependencies it installs are Linux packages, so it fits a Linux control node, not app01 [18].

The pricing changed in 2026. Registering machines outside AWS is now free, and since September 30, 2026, Run Command on those machines costs $0.002 per invocation, while it stays free on EC2 [19], [5]. Check the pricing page before relying on it, since it changed twice in 2026.

Which one to pick

In most cases the firewall picks for you. If nothing may connect in to app01, the choice is between the two agents.

References

Show all 19 sourcesHide sources
  1. [1] ↩ a b Microsoft, “msiexec,” Windows Server Documentation. Accessed: Oct. 12, 2026. [Online]. Available: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec
  2. [2] ↩ a b c d Ansible project contributors, “Windows Remote Management,” Ansible Community Documentation. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.ansible.com/projects/ansible/latest/os_guide/windows_winrm.html
  3. [3] ↩ a b Ansible project contributors, “Installing Ansible,” Ansible Community Documentation. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.ansible.com/projects/ansible/latest/installation_guide/intro_installation.html
  4. [4] ↩ a b Amazon Web Services, “Working with on-premises instances for CodeDeploy,” AWS CodeDeploy User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/codedeploy/latest/userguide/instances-on-premises.html
  5. [5] ↩ a b c Amazon Web Services, “AWS Systems Manager Run Command,” AWS Systems Manager User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/systems-manager/latest/userguide/run-command.html
  6. [6] ↩ a b Amazon Web Services, “Working with the CodeDeploy agent,” AWS CodeDeploy User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/codedeploy/latest/userguide/codedeploy-agent.html
  7. [7] ↩ a b Amazon Web Services, “Improve the security of EC2 instances by using VPC endpoints for Systems Manager,” AWS Systems Manager User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-create-vpc.html
  8. [8] ↩ a b c d Ansible project contributors, “Managing Windows hosts with Ansible,” Ansible Community Documentation. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.ansible.com/projects/ansible/latest/os_guide/intro_windows.html
  9. [9] ↩ a b c Amazon Web Services, “Prerequisites for configuring an on-premises instance,” AWS CodeDeploy User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/codedeploy/latest/userguide/instances-on-premises-prerequisites.html
  10. [10] ↩ a b Amazon Web Services, “AppSpec "hooks" section,” AWS CodeDeploy User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html
  11. [11] ↩ a b Amazon Web Services, “AppSpec file example,” AWS CodeDeploy User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-example.html
  12. [12] ↩ Microsoft, “MsiExec.exe and InstMsi.exe error messages,” Windows Installer Documentation. Accessed: Oct. 12, 2026. [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/msi/error-codes
  13. [13] ↩ Microsoft, “Start-Process,” PowerShell 7.6 Documentation. Accessed: Oct. 12, 2026. [Online]. Available: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/start-process
  14. [14] ↩ Amazon Web Services, “Register an on-premises instance,” AWS CodeDeploy User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/codedeploy/latest/userguide/on-premises-instances-register.html
  15. [15] ↩ Amazon Web Services, “Use the register-on-premises-instance command (IAM session ARN),” AWS CodeDeploy User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/codedeploy/latest/userguide/register-on-premises-instance-iam-session-arn.html
  16. [16] ↩ Ansible project contributors, “ansible.windows.win_ping module,” Ansible Community Documentation. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.ansible.com/projects/ansible/latest/collections/ansible/windows/win_ping_module.html
  17. [17] ↩ Amazon Web Services, “Create a hybrid activation to register nodes with Systems Manager,” AWS Systems Manager User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/systems-manager/latest/userguide/hybrid-activation-managed-nodes.html
  18. [18] ↩ Amazon Web Services, “Creating associations that run Ansible playbooks,” AWS Systems Manager User Guide. Accessed: Oct. 12, 2026. [Online]. Available: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-state-manager-ansible.html
  19. [19] ↩ Amazon Web Services, “AWS Systems Manager pricing.” Accessed: Oct. 12, 2026. [Online]. Available: https://aws.amazon.com/systems-manager/pricing/

Every source cited across the primers is collected on one page.