Build an Ansible CI/CD Pipeline to AWS
Validate and deploy an Ansible-managed NGINX site to AWS with GitHub Actions.
Introduction
30 Second Summary
Server changes are easy to make once. The real challenge is repeating them safely when a deployment fails.
In this project, you will build a secure Ansible CI/CD pipeline that validates an Ansible playbook before it changes AWS. Each push uses GitHub Actions to configure an Amazon EC2 web server through AWS Systems Manager with temporary credentials.
What You'll Build
A push to main produces a live page that says Deployed by Ansible after GitHub Actions verifies every deployment stage.
By the end of this project, you'll have:
- A visible CI safety net that turns red when a malformed Ansible module could reach AWS.
- A secure deployment path through OpenID Connect temporary credentials over AWS Systems Manager connections without stored access keys or inbound SSH.
- A repeatable recovery loop that traces a broken Amazon S3 transfer setting from workflow logs before confirming changed=0 on the repaired run.
- Secret Mission: Add a content-level health gate that blocks a deployment when the live page is missing its expected release marker.
Are there any prerequisites?
You'll need accounts for GitHub plus AWS. Your Windows computer must already have Git plus Visual Studio Code.
Your AWS identity needs permission to create the cloud resources used in this project.
Before We Start
Before any hands-on work, lock in why CI must catch playbook defects before they can change AWS. GitHub Actions will validate an Ansible playbook before using GitHub OIDC and AWS Systems Manager to deploy an NGINX landing page to Amazon EC2.
Set Up the Repository and AWS Target
A secure pipeline needs a reproducible AWS target. It also needs a deployment role that trusts only your repository.
In this step, you will publish the infrastructure template to GitHub. You will use AWS CloudFormation to create the target resources before any deployment code runs.
In this step, get ready to:
- Create the remote repository with its matching local Git repository.
- Provision the AWS target from the supplied CloudFormation template.
- Store the CloudFormation outputs as GitHub repository variables.
Create the repository and publish the template
The repository becomes the source of truth for every later pipeline run. The first commit contains the complete AWS infrastructure definition.
- Return to your signed-in GitHub account.
- Open the menu in the upper-right corner.
- Select New repository.
- Select the account that will own the repository from the Owner menu.
- Enter ansible-aws-cicd in the Repository name field.
- Choose your preferred repository visibility.
- Leave the options for a README, a .gitignore file, or a license unselected.
- Select Create repository.
Your empty repository is ready to receive the infrastructure template. Its owner becomes part of the role trust policy that restricts access to your repository.
- Record the selected repository owner here: your GitHub owner.
- Copy the repository URL shown in the Quick Setup area into this field: https://github.com/your-owner/ansible-aws-cicd.git.
- Press the Windows key to open Windows search.
- Type Visual Studio Code.
- Press Enter to open Visual Studio Code.
- Select File from the top menu.
- Select Open Folder....
- Select New Folder in the Desktop location.
- Name the folder ansible-aws-cicd.
- Select Select Folder.
Why open the whole folder?
Visual Studio Code treats the opened ansible-aws-cicd folder as one workspace. The editor terminal will start inside that same folder.
- Select the new-file icon in the Explorer sidebar.
- Name the file infrastructure.yml.
- Select the second tab below to compare your file with the complete template.
- Copy the complete template into infrastructure.yml.
- Press Ctrl+S to save the file.
✔️ Awesome, I've got everything!
Your saved infrastructure.yml file should match the complete template in the next tab.
ⓧ I'd like to double check the full code
AWSTemplateFormatVersion: "2010-09-09"
Description: Secure Ansible CI/CD target using GitHub OIDC and AWS Systems Manager
Parameters:
GitHubOwner:
Type: String
Description: GitHub user or organization that owns the repository
GitHubRepository:
Type: String
Default: ansible-aws-cicd
Description: GitHub repository name without the owner
ExistingGitHubOidcProviderArn:
Type: String
Default: ""
Description: Optional existing GitHub OIDC provider ARN in this AWS account
LatestAmiId:
Type: "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>"
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
Conditions:
CreateGitHubOidcProvider: !Equals [!Ref ExistingGitHubOidcProviderArn, ""]
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: ansible-cicd-vpc
InternetGateway:
Type: AWS::EC2::InternetGateway
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref Vpc
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
AvailabilityZone: !Select [0, !GetAZs ""]
CidrBlock: 10.0.1.0/24
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: ansible-cicd-public-subnet
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
DefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow public HTTP without inbound SSH
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
WebServerRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
WebServerInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref WebServerRole
AnsibleTransferBucket:
Type: AWS::S3::Bucket
GitHubOidcProvider:
Type: AWS::IAM::OIDCProvider
Condition: CreateGitHubOidcProvider
Properties:
Url: https://token.actions.githubusercontent.com
ClientIdList:
- sts.amazonaws.com
GitHubDeployRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Federated: !If
- CreateGitHubOidcProvider
- !Ref GitHubOidcProvider
- !Ref ExistingGitHubOidcProviderArn
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
token.actions.githubusercontent.com:aud: sts.amazonaws.com
StringLike:
token.actions.githubusercontent.com:sub:
- !Sub "repo:${GitHubOwner}/${GitHubRepository}:ref:refs/heads/main"
- !Sub "repo:${GitHubOwner}@*/${GitHubRepository}@*:ref:refs/heads/main"
Policies:
- PolicyName: AnsibleSessionAndTransfer
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: ssm:StartSession
Resource:
- !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/${WebServer}"
- !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/SSM-SessionManagerRunShell"
- Effect: Allow
Action:
- ssm:ResumeSession
- ssm:TerminateSession
- ssmmessages:OpenDataChannel
Resource: "*"
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
Resource: !GetAtt AnsibleTransferBucket.Arn
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource: !Sub "${AnsibleTransferBucket.Arn}/*"
WebServer:
Type: AWS::EC2::Instance
DependsOn: DefaultPublicRoute
Properties:
ImageId: !Ref LatestAmiId
InstanceType: t3.micro
IamInstanceProfile:
Name: !Ref WebServerInstanceProfile
SubnetId: !Ref PublicSubnet
SecurityGroupIds:
- !Ref WebSecurityGroup
Tags:
- Key: Name
Value: ansible-cicd-web
- Key: Project
Value: ansible-cicd-demo
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -euxo pipefail
dnf install -y python3 curl-minimal
yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
systemctl enable amazon-ssm-agent
systemctl start amazon-ssm-agent
Outputs:
GitHubDeployRoleArn:
Value: !GetAtt GitHubDeployRole.Arn
InstanceId:
Value: !Ref WebServer
AnsibleTransferBucket:
Value: !Ref AnsibleTransferBucket
AwsRegion:
Value: !Ref AWS::Region
WebsiteUrl:
Value: !Sub "http://${WebServer.PublicIp}"
What does this template create?
- The network resources create a dedicated public subnet with inbound HTTP access on port 80.
- The instance profile lets the web server register with AWS Systems Manager.
- The deployment role limits GitHub access to the configured repository on the main branch.
- The outputs expose the five values that the workflow needs later.
The template defines the infrastructure declaratively. CloudFormation can now create the same target from the same file whenever you need it.
- Open the integrated terminal by selecting View from the top menu.
- Select Terminal.
- Prepare the Git repository on main by running these commands:
git init -b main
git add .
git commit -m "Initial project version"
git remote add origin [[YOUR_REPO_URL="https://github.com/your-owner/ansible-aws-cicd.git"]]
git push origin main
What do these commands do?
- The first command creates the local repository with main as its initial branch.
- The next two commands record infrastructure.yml in the first commit.
- The remote command connects the local repository to its GitHub copy under the name origin.
- The final command publishes the commit to the remote main branch.
GitHub may open a browser sign-in during the first push. Complete the sign-in with the account that owns the repository.
- Refresh the ansible-aws-cicd repository page in GitHub.
- Confirm the branch selector shows main.
- Confirm infrastructure.yml appears in the file list.
Your first commit is now visible on GitHub. The AWS target can be recreated from the exact template stored on main.
Push rejected or authentication failed?
Confirm that the copied repository URL belongs to the account you used during browser authentication. An empty remote repository should accept the first push without a merge.
Still blocked? Help me diagnose why my first Git push to an empty GitHub repository is failing.
Create the AWS target
The template creates a dedicated network plus an Amazon EC2 instance. It also creates the roles needed for AWS Systems Manager access.
AWS costs start here
This action creates resources that can incur AWS charges. The cleanup section shows how to stop or delete them after the project.
- Return to your signed-in AWS Management Console.
- Enter IAM in the console search bar.
- Select IAM from the search results.
- Select Identity providers from the navigation pane.
- Check whether an identity provider for https://token.actions.githubusercontent.com already exists.
No GitHub provider exists
The template can create the OpenID Connect provider for this account. You will leave ExistingGitHubOidcProviderArn empty during stack creation.
A GitHub provider exists
AWS allows one provider for a given URL in the account. The template accepts the existing provider ARN to avoid creating a duplicate.
- Select the identity provider for https://token.actions.githubusercontent.com.
- Record its ARN here: arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com.
Why use Systems Manager?
Systems Manager lets Ansible reach the instance through an AWS-managed session. The security group only permits public HTTP traffic because the pipeline does not need inbound SSH.
Direct SSH would require a private key plus additional network access. The Systems Manager connection keeps those concerns out of this deployment path.
- Enter CloudFormation in the console search bar.
- Select CloudFormation from the search results.
- Select US East (N. Virginia) us-east-1 from the region selector.
- Select Create stack.
- Select With new resources (standard).
- Select Choose an existing template.
- Select Upload a template file.
- Select Choose file.
Where are you now?
CloudFormation is ready to read the infrastructure definition from your computer. The next screen collects the values that bind the deployment role to your repository.
- Select infrastructure.yml from the ansible-aws-cicd folder on your Desktop.
- Select Next.
- Enter ansible-cicd-demo in the Stack name field.
- Enter your GitHub owner for GitHubOwner.
- Confirm ansible-aws-cicd appears for GitHubRepository.
- Paste arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com into ExistingGitHubOidcProviderArn only if you recorded an existing provider.
- Leave ExistingGitHubOidcProviderArn empty if no matching provider exists.
- Keep the default value for LatestAmiId.
The owner plus repository values narrow the deployment role to this project. The role trust also requires the workflow to run from main.
- Select Next.
- Keep the default stack options.
- Select Next.
- Review the stack parameters.
- Select the acknowledgement for IAM resource creation.
- Select Submit.
CloudFormation now provisions several connected resources. Expect the stack to remain in progress for several minutes while the instance initializes.
Before you check the finished stack, which status do you expect after every resource has been created successfully?
- Refresh the stack events until the stack status stops changing.
- Confirm the final stack status is CREATE_COMPLETE.
- Select the Resources tab.
- Confirm the stack lists the network, instance, IAM, Amazon S3, and OIDC resources from the template.
The stack now has a dedicated target named ansible-cicd-web. Its security group exposes HTTP while Systems Manager provides the administration path.
Stack creation failed?
Open the Events tab. Read the first failed resource event because later failures are often consequences of that first problem.
A duplicate OIDC provider usually means the existing provider ARN was left out. Help me diagnose my failed CloudFormation stack from its first failed event.
Add repository variables and verify the target
The workflow needs the stack outputs at runtime. Repository variables provide those non-sensitive values without hard-coding them into the workflow.
- Select the Outputs tab for ansible-cicd-demo.
- Confirm the output list contains GitHubDeployRoleArn.
- Confirm the output list contains InstanceId.
- Confirm the output list contains AnsibleTransferBucket.
- Confirm the output list contains AwsRegion.
- Confirm the output list contains WebsiteUrl.
Match each output to a variable
- Use GitHubDeployRoleArn as the value for AWS_ROLE_ARN.
- Use AwsRegion as the value for AWS_REGION.
- Use InstanceId as the value for EC2_INSTANCE_ID.
- Use AnsibleTransferBucket as the value for ANSIBLE_S3_BUCKET.
- Use WebsiteUrl as the value for WEBSITE_URL.
- Return to the ansible-aws-cicd repository in GitHub.
- Select the repository Settings tab.
- Select Secrets and variables from the left sidebar.
- Select Actions.
- Select the Variables tab.
Each repository variable follows the same four-field flow. Repeat it once for every output mapping above.
- Select New repository variable.
- Enter the repository variable name in the Name field.
- Paste its matching CloudFormation output into the Value field.
- Select Add variable.
- Repeat the same flow until all five mappings have been added.
Before the final check, predict which five output names should be visible in CloudFormation and which five variable names should be visible in GitHub.
- Return to the Outputs tab for the ansible-cicd-demo stack.
- Confirm the stack status remains CREATE_COMPLETE.
- Confirm the outputs are GitHubDeployRoleArn, InstanceId, AnsibleTransferBucket, AwsRegion, and WebsiteUrl.
- Return to the GitHub repository variable list.
- Confirm the variable names are AWS_ROLE_ARN, AWS_REGION, EC2_INSTANCE_ID, ANSIBLE_S3_BUCKET, and WEBSITE_URL.
That is the infrastructure foundation complete. GitHub now has every non-sensitive value required to target the stack from a workflow.
A variable does not match its output?
Compare each GitHub variable with the current value in the CloudFormation Outputs tab. Copy the value again if whitespace or a partial selection changed it.
Need another pair of eyes? Help me verify my CloudFormation output to GitHub variable mappings.
Your repository and AWS target are ready. Next, you will add a minimal Ansible playbook and earn the pipeline's first green validation run.
Get the First Green CI Run
Your AWS target is ready. Your GitHub repository now carries the CloudFormation template that created it.
Before the pipeline changes AWS, it needs fast proof that the pinned Ansible toolchain installs. It also needs proof that the playbook loads.
This step turns GitHub Actions into that safety signal. A green CI result becomes the baseline for every deployment change that follows.
In this step, get ready to:
- Pin the validation toolchain to reproducible versions.
- Create a minimal Ansible playbook that CI can load.
- Get a green GitHub Actions validation result.
Pin the validation toolchain
A pinned toolchain gives every workflow run the same package versions. This keeps validation behavior consistent across future commits.
- In the Visual Studio Code window from earlier, select the ansible-aws-cicd folder in the Explorer sidebar.
- Create requirements.txt inside ansible-aws-cicd using the new-file control at the top of the Explorer sidebar.
You should see the empty requirements.txt file directly inside ansible-aws-cicd.
- Add the pinned Python packages to requirements.txt by pasting this code:
ansible-core==2.21.5
ansible-lint==26.10.0
boto3==1.43.111
botocore==1.43.111
What do these pins control?
- The ansible-core==2.21.5 pin supplies the playbook engine and syntax checker.
- The ansible-lint==26.10.0 pin checks the playbook for structural problems.
- The boto3==1.43.111 pin supplies the AWS software development kit for Python.
- The botocore==1.43.111 pin keeps the underlying AWS client compatible with the selected Boto3 release.
- Save requirements.txt.
- Confirm the Explorer sidebar shows requirements.txt directly inside ansible-aws-cicd.
Does the filename look wrong?
Check that the filename ends with .txt once. Rename the file if Visual Studio Code shows requirements.txt.txt.
Still stuck? Help me check the location and contents of my requirements.txt file.
✔️ Awesome, I've got everything!
Your Python dependencies are pinned. Make sure requirements.txt is saved before continuing.
ⓧ I'd like to double check the full code
ansible-core==2.21.5
ansible-lint==26.10.0
boto3==1.43.111
botocore==1.43.111
Ansible packages AWS-specific functionality in a collection. The collection requirements file locks that content to version 11.4.0.
- Create requirements.yml inside ansible-aws-cicd using the new-file control in the Explorer sidebar.
You should now see requirements.txt beside the empty requirements.yml file.
- Pin the AWS collection in requirements.yml by pasting this code:
---
collections:
- name: amazon.aws
version: "11.4.0"
What does this file control?
- The collections section tells Ansible Galaxy that this file describes collection dependencies.
- The amazon.aws name selects the AWS collection used later by the pipeline.
- The 11.4.0 value prevents a future collection release from changing this project unexpectedly.
- Save requirements.yml.
- Confirm the Explorer sidebar shows both requirements files directly inside ansible-aws-cicd.
Is the collection file missing?
Check that requirements.yml sits beside requirements.txt. Move it out of any nested folder if the indentation in the Explorer sidebar differs.
Need another pair of eyes? Help me verify my Ansible collection requirements file.
✔️ Awesome, I've got everything!
Your AWS collection is pinned. The two requirements files now define a repeatable validation toolchain.
ⓧ I'd like to double check the full code
---
collections:
- name: amazon.aws
version: "11.4.0"
Build a minimal playbook
A playbook is a YAML description of the server state Ansible should apply. This first version contains one harmless debug task so CI can prove that the file loads.
- Create an ansible folder inside ansible-aws-cicd using the new-folder control in the Explorer sidebar.
You should see the empty ansible folder beneath the two requirements files.
- Create site.yml inside the ansible folder using the new-file control.
The Explorer sidebar should now show site.yml nested under ansible.
- Add the minimal web play to ansible/site.yml by pasting this code:
---
- name: Configure the Ansible demo web server
hosts: web
gather_facts: true
become: true
tasks:
- name: Confirm CI can load this playbook
ansible.builtin.debug:
msg: CI can load this playbook
What does this playbook prove?
- The play targets the web inventory group that the deployment pipeline uses.
- The gather_facts setting asks Ansible to collect information about the target host.
- The become setting allows later tasks to manage system-level configuration.
- The ansible.builtin.debug task gives CI a valid module to parse without changing AWS.
- Save ansible/site.yml.
- Confirm the last line reads msg: CI can load this playbook.
Does the YAML look misaligned?
Make sure tasks aligns with hosts. Keep the debug module nested beneath its task name.
Need help checking the structure? Help me find the indentation issue in my minimal Ansible playbook.
✔️ Awesome, I've got everything!
Your minimal playbook is ready for CI. Make sure ansible/site.yml is saved.
ⓧ I'd like to double check the full code
---
- name: Configure the Ansible demo web server
hosts: web
gather_facts: true
become: true
tasks:
- name: Confirm CI can load this playbook
ansible.builtin.debug:
msg: CI can load this playbook
Create and run the validation workflow
A workflow turns every push to main into an automated check. This validation job installs Python 3.13.16 before loading the pinned Ansible toolchain.
- Create a .github folder inside ansible-aws-cicd using the new-folder control in the Explorer sidebar.
- Create a workflows folder inside .github using the same control.
You should see the nested path .github/workflows in the Explorer sidebar.
- Create ansible.yml inside .github/workflows using the new-file control.
The empty workflow file should appear at .github/workflows/ansible.yml.
- Define the workflow trigger and validation job by pasting this first block into .github/workflows/ansible.yml.
name: Ansible CI/CD
on:
push:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v7.0.1
What does the workflow skeleton do?
- The push trigger watches the main branch.
- The workflow_dispatch trigger supports manual runs from GitHub Actions.
- The permissions block gives the workflow read access to repository contents.
- The permissions block also allows the workflow to request an OpenID Connect token.
- The validate job begins by checking out the repository on a GitHub-hosted runner.
- Save .github/workflows/ansible.yml.
- Confirm the workflow file contains the validate job beneath jobs.
Is the workflow in the wrong folder?
Check that the full path is .github/workflows/ansible.yml. GitHub does not load workflow files stored elsewhere in the repository.
Need help with the path? Help me verify my GitHub Actions workflow location.
The remaining workflow steps install the pinned dependencies. They then run linting and syntax validation against ansible/site.yml.
- Complete the validate job by pasting this block directly below the checkout step:
- name: Set up Python
uses: actions/setup-python@v7.0.0
with:
python-version: "3.13.16"
cache: pip
- name: Install Python requirements
run: python -m pip install -r requirements.txt
- name: Install Ansible collections
run: ansible-galaxy collection install -r requirements.yml
- name: Lint the playbook
run: ansible-lint ansible/site.yml
- name: Check playbook syntax
run: ansible-playbook --syntax-check ansible/site.yml
How does validation work?
- The Python setup action installs version 3.13.16 on the runner.
- The pip command installs the four package pins from requirements.txt.
- The ansible-galaxy command installs the collection pinned in requirements.yml.
- The ansible-lint command checks the playbook for fatal issues.
- The ansible-playbook command verifies that Ansible can parse the playbook syntax.
- Save .github/workflows/ansible.yml.
- Confirm the final workflow step is named Check playbook syntax.
Does the workflow indentation look uneven?
Keep every step marker aligned beneath steps. Keep each uses or run key nested beneath its matching step name.
Want help checking the file? Help me find the YAML indentation problem in my validation workflow.
✔️ Awesome, I've got everything!
Your validation-only workflow is complete. Make sure all four new files are saved.
ⓧ I'd like to double check the full code
name: Ansible CI/CD
on:
push:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@v7.0.0
with:
python-version: "3.13.16"
cache: pip
- name: Install Python requirements
run: python -m pip install -r requirements.txt
- name: Install Ansible collections
run: ansible-galaxy collection install -r requirements.yml
- name: Lint the playbook
run: ansible-lint ansible/site.yml
- name: Check playbook syntax
run: ansible-playbook --syntax-check ansible/site.yml
Before you push, predict whether every validation step will finish successfully.
- Stage the new files, commit them, and push main by running these commands in the Visual Studio Code terminal:
git add .
git commit -m "Add Ansible validation workflow"
git push origin main
What do these commands do?
- The first command stages the new requirements files, playbook, and workflow.
- The second command records those files in the local repository with a descriptive message.
- The third command sends the commit to main on origin. That push starts the workflow.
The first run can take a few minutes while GitHub installs Python and the pinned dependencies. The live log keeps updating during that setup.
- Return to the ansible-aws-cicd repository page from earlier.
- Select the Actions tab.
- Select the latest Ansible CI/CD run.
- Select the validate job.
- Expand the Lint the playbook step.
- Expand the Check playbook syntax step.
You should see green check marks beside both validation steps. The validate job should finish green.
The lint step reports no fatal issue. The syntax-check step finishes successfully against ansible/site.yml.
Is the validate job red?
- Open the first failed step because later failures may only be consequences of that earlier problem.
- Check that requirements.txt and requirements.yml sit directly inside ansible-aws-cicd.
- Check that the playbook path is exactly ansible/site.yml.
Still seeing a failed run? Help me diagnose my first Ansible validation workflow failure.
That first green run proves your pinned Ansible toolchain can load the playbook before AWS changes. Next, you will test whether the same safety gate can stop a broken module.
Watch CI Block a Broken Playbook
Your latest GitHub Actions run proved that the valid playbook passes CI. A validation pipeline becomes trustworthy when it can also stop a defect before AWS changes.
In this step, you will introduce one controlled defect into the minimal Ansible playbook. You will use the workflow logs to pinpoint the invalid module reference.
In this step, get ready to:
- Introduce a controlled invalid module reference into the playbook.
- Inspect the validation logs to identify the reported defect.
- Preserve the validation rules while preparing the repository for deployment.
Introduce a controlled module defect
The current task uses a valid debug module. Changing only its module name keeps the surrounding YAML structure intact.
This controlled change isolates module resolution as the only variable. Keep the workflow unchanged throughout the experiment.
- In the Visual Studio Code Explorer sidebar, select ansible/site.yml.
- Find this module reference:
ansible.builtin.debug:
What does this line do?
The module reference tells Ansible which task implementation to load. The existing reference points to the valid debug module.
- Replace the module reference with the following deliberate defect:
ansible.builtin.packag:
What changed?
This edit changes only the module name. The rest of the task remains valid YAML, so the validation result can focus on module resolution.
- Save ansible/site.yml.
- Use the Git workflow from the previous step to commit the deliberate defect with the message Break playbook to prove CI validation.
Before you push, do you think the validation job will accept this module reference?
- Push the commit to main using the Git workflow from earlier.
- Return to your GitHub repository.
- Select the Actions tab.
- Open the new workflow run.
You will see the validate job turn red. The deliberate defect has been stopped before any deployment code reaches AWS.
That red run is useful. Your validation job now has visible evidence that it can block a broken playbook.
Did the run stay green?
- Confirm that ansible/site.yml contains ansible.builtin.packag without the final e.
- Confirm that the changed file was included in the commit you pushed to main.
- Refresh the workflow history to check for the run linked to your latest commit.
Still seeing a green result? Help me identify why my deliberate Ansible module defect did not fail the GitHub Actions validation job.
Inspect the failed validation log
A workflow log records each validation step in execution order. The first meaningful error usually identifies the layer that rejected the change.
- Select the failed validate job.
- Expand the first failed lint or syntax-check step.
- Locate the earliest message about the unresolved module.
The log names ansible/site.yml as the affected file. It points to the task named Confirm CI can load this playbook.
You will also see ansible.builtin.packag identified as the unknown module. Ansible Lint can report this category with the syntax-check[unknown-module] signature.
How does the log narrow the problem?
- The file path identifies where the defect lives.
- The task name identifies the affected unit of configuration.
- The module reference identifies the exact value that Ansible cannot resolve.
Restore the valid module and prove recovery
Keep the guardrail intact. Repair the module reference while leaving every lint and syntax check enabled.
- Switch back to ansible/site.yml in Visual Studio Code.
- Find the deliberate defect:
ansible.builtin.packag:
Why restore this line?
The failed run isolated this module reference as the defect. Repairing the source preserves the validation policy that caught it.
- Replace the defective reference with the valid module reference:
ansible.builtin.debug:
What does this repair preserve?
The debug module restores the valid temporary task. Its message still gives CI a small playbook that lint and syntax validation can load.
- Save ansible/site.yml.
✔️ Awesome, I've got everything!
Your saved playbook is back to the valid temporary version. The validation workflow remains unchanged.
ⓧ I'd like to double check the full code
- Compare your saved ansible/site.yml with this complete reference:
---
- name: Configure the Ansible demo web server
hosts: web
gather_facts: true
become: true
tasks:
- name: Confirm CI can load this playbook
ansible.builtin.debug:
msg: CI can load this playbook
What should match?
The task uses the valid debug module. The message remains CI can load this playbook.
- Use the Git workflow from earlier to commit the repair with the message Restore valid Ansible module.
Before you push, do you expect the same validation checks to pass without changing the workflow?
- Push the repair commit to main.
- Return to the repository's Actions tab.
- Open the newest workflow run.
You will see the newest validate job turn green. The workflow history now shows a red defect run followed by a green repaired run.
Strong recovery. You fixed the playbook while preserving the checks that protect AWS from invalid configuration.
Is the repaired run still red?
- Confirm that the module name is exactly ansible.builtin.debug.
- Check the indentation beneath the task name against the full-file reference.
- Confirm that the repair commit includes the saved ansible/site.yml file.
Need help reading the new failure? Help me diagnose why my restored Ansible playbook still fails GitHub Actions validation.
Your pipeline has now proved both sides of the guardrail. Next, you will replace the temporary task with the real NGINX configuration and deploy it through AWS Systems Manager.
Deploy NGINX Through Systems Manager
Your red GitHub Actions run proved that an invalid Ansible module cannot reach AWS. The restored playbook gives you a clean starting point for the deployment.
Now you will add the secure CD path. OpenID Connect supplies temporary credentials. AWS Systems Manager Session Manager carries the connection to Amazon EC2 without inbound SSH.
In this step, get ready to:
- Replace the temporary playbook with the final NGINX configuration.
- Add a deploy job that uses temporary AWS credentials and Systems Manager.
- Run the completed pipeline and verify the live landing page.
Build the final NGINX playbook
The final playbook expresses the server as declarative configuration. NGINX reaches the same desired state whenever the pipeline runs.
Ansible handlers restart NGINX only when its configuration or landing page changes. This supports idempotence during later runs.
- In ansible/site.yml, replace the temporary playbook with this valid deployment skeleton:
---
- name: Configure the Ansible demo web server
hosts: web
gather_facts: true
become: true
tasks:
- name: Install NGINX
ansible.builtin.package:
name: nginx-1.30.5-1.amzn2023.0.1
state: present
- name: Ensure NGINX is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Restart NGINX
ansible.builtin.service:
name: nginx
state: restarted
What does this playbook skeleton do?
- The play targets the runtime host group named web.
- The package task installs the pinned NGINX package for Amazon Linux 2023.
- The service task starts NGINX and enables it for future instance starts.
- The handler provides one reusable restart action for configuration changes.
- Save ansible/site.yml.
- Commit the change with the message Add NGINX deployment skeleton.
- Push the commit to main from Visual Studio Code's source control view.
- Open the repository's Actions tab to check the new validation run.
You'll see the validate job turn green. This confirms that CI can load the pinned package task and service handler.
Validation turned red?
Check that the temporary debug task is gone. Confirm that tasks and handlers use the indentation shown above.
Still blocked? Help me diagnose my Ansible playbook validation failure.
- In ansible/site.yml, find the task named Ensure NGINX is running and enabled.
- Insert the web root task immediately above that service task by adding this code:
- name: Create the demo web root
ansible.builtin.file:
path: /srv/ansible-demo
state: directory
owner: root
group: root
mode: "0755"
What does this task do?
- The file module creates /srv/ansible-demo as the landing page directory.
- The owner and group keep the directory under the system administrator account.
- The quoted mode grants read and execute access while reserving writes for the owner.
- Keep your cursor above the task named Ensure NGINX is running and enabled.
- Add the NGINX configuration task by inserting this code:
- name: Configure NGINX for the demo site
ansible.builtin.copy:
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
content: |
user nginx;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
default_type text/html;
server {
listen 80 default_server;
server_name _;
root /srv/ansible-demo;
index index.html;
location / {
}
}
}
notify: Restart NGINX
How does this configuration work?
- The copy module manages /etc/nginx/nginx.conf as part of the declared server state.
- The server listens on public HTTP port 80.
- The document root points requests at /srv/ansible-demo.
- The notification runs the restart handler only when this managed file changes.
- Save ansible/site.yml.
- Commit the change with the message Configure NGINX web root.
- Push the commit to main.
- Return to the Actions tab to check the validation result.
The new validate job turns green. CI now accepts the managed directory and NGINX configuration.
Configuration tasks fail validation?
Confirm that both new tasks align with the existing package task. Check that notify: Restart NGINX matches the handler name exactly.
Need another pair of eyes? Help me fix the directory or NGINX configuration task.
- In ansible/site.yml, find the task named Ensure NGINX is running and enabled.
- Insert the landing page task immediately above that service task by adding this code:
- name: Publish the Ansible landing page
ansible.builtin.copy:
dest: /srv/ansible-demo/index.html
owner: root
group: root
mode: "0644"
content: |
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ansible CI/CD Demo</title>
<style>
body { font-family: system-ui, sans-serif; margin: 4rem; color: #172033; }
main { max-width: 42rem; padding: 2rem; border: 1px solid #d8deea; border-radius: 1rem; }
code { background: #f2f5f9; padding: 0.2rem 0.4rem; }
</style>
</head>
<body>
<main>
<h1>Deployed by Ansible</h1>
<p>GitHub Actions validated this playbook and deployed it to Amazon EC2 through AWS Systems Manager.</p>
<p>Run the pipeline again and look for <code>changed=0</code>.</p>
</main>
</body>
</html>
notify: Restart NGINX
What does the landing page task do?
- The copy module writes the complete page to /srv/ansible-demo/index.html.
- The heading provides the visible deployment marker Deployed by Ansible.
- The page describes the secure delivery path that produced it.
- The restart notification applies the page change during the same playbook run.
- Save ansible/site.yml.
- Commit the change with the message Publish Ansible landing page.
- Push the commit to main.
- Return to the latest workflow run to confirm that validate is green.
Your final playbook passes lint and syntax validation. The desired NGINX state is ready for the deployment job.
Landing page task fails validation?
Keep every HTML line indented beneath content: |. Align notify: Restart NGINX with the other copy module properties.
Still seeing a red job? Help me correct the Ansible landing page task.
✔️ Awesome, I've got everything!
Great. Double-check that you saved ansible/site.yml before moving to the workflow.
ⓧ I'd like to double check the full code
---
- name: Configure the Ansible demo web server
hosts: web
gather_facts: true
become: true
tasks:
- name: Install NGINX
ansible.builtin.package:
name: nginx-1.30.5-1.amzn2023.0.1
state: present
- name: Create the demo web root
ansible.builtin.file:
path: /srv/ansible-demo
state: directory
owner: root
group: root
mode: "0755"
- name: Configure NGINX for the demo site
ansible.builtin.copy:
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
content: |
user nginx;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
default_type text/html;
server {
listen 80 default_server;
server_name _;
root /srv/ansible-demo;
index index.html;
location / {
}
}
}
notify: Restart NGINX
- name: Publish the Ansible landing page
ansible.builtin.copy:
dest: /srv/ansible-demo/index.html
owner: root
group: root
mode: "0644"
content: |
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ansible CI/CD Demo</title>
<style>
body { font-family: system-ui, sans-serif; margin: 4rem; color: #172033; }
main { max-width: 42rem; padding: 2rem; border: 1px solid #d8deea; border-radius: 1rem; }
code { background: #f2f5f9; padding: 0.2rem 0.4rem; }
</style>
</head>
<body>
<main>
<h1>Deployed by Ansible</h1>
<p>GitHub Actions validated this playbook and deployed it to Amazon EC2 through AWS Systems Manager.</p>
<p>Run the pipeline again and look for <code>changed=0</code>.</p>
</main>
</body>
</html>
notify: Restart NGINX
- name: Ensure NGINX is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Restart NGINX
ansible.builtin.service:
name: nginx
state: restarted
Add the secure deploy job
The deployment runner needs credentials before it can contact your instance. The workflow exchanges its GitHub identity for short-lived access through the role created by CloudFormation.
Why Systems Manager instead of SSH?
Systems Manager lets Ansible reach the instance through the amazon.aws.aws_ssm connection. Your security group can stay free from inbound port 22.
Direct SSH from a hosted runner requires private key handling. It can also require broad ingress rules or repeated security group changes.
- Return to .github/workflows/ansible.yml in Visual Studio Code.
- Place your cursor after the final step in the existing validate job.
- Add the first half of the deploy job by inserting this code:
deploy:
needs: validate
runs-on: ubuntu-latest
env:
AWS_REGION: ${{ vars.AWS_REGION }}
EC2_INSTANCE_ID: ${{ vars.EC2_INSTANCE_ID }}
ANSIBLE_S3_BUCKET: ${{ vars.ANSIBLE_S3_BUCKET }}
WEBSITE_URL: ${{ vars.WEBSITE_URL }}
steps:
- name: Check out the repository
uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@v7.0.0
with:
python-version: "3.13.16"
cache: pip
- name: Install Python requirements
run: python -m pip install -r requirements.txt
- name: Install Ansible collections
run: ansible-galaxy collection install -r requirements.yml
- name: Configure temporary AWS credentials
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
How does authentication work?
- The needs: validate dependency keeps deployment behind the CI checks.
- The environment section reads the non-sensitive deployment values from repository variables.
- The runner installs the pinned Python and Ansible toolchain used by the validation job.
- The credentials action assumes the trusted role through OpenID Connect and supplies temporary credentials.
- Save .github/workflows/ansible.yml.
- Commit the change with the message Add OIDC deployment authentication.
Before you push, do you think the new job can authenticate without a stored AWS access key?
- Push the commit to main.
- Open the new workflow run in the repository's Actions tab.
- Select the deploy job to inspect its steps.
You'll see validate finish first. The temporary credentials step then succeeds without a repository access key.
Temporary credentials step failed?
Confirm that AWS_ROLE_ARN still matches the GitHubDeployRoleArn output from the ansible-cicd-demo stack. Confirm that the workflow runs from the main branch trusted by the role.
Need help isolating the trust problem? Help me diagnose the failed GitHub OIDC authentication step.
- In .github/workflows/ansible.yml, return to the end of the deploy job.
- Add the connection and deployment steps by inserting this code:
- name: Install the Session Manager plugin
run: |
curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb"
sudo dpkg -i session-manager-plugin.deb
session-manager-plugin --version
- name: Build the runtime inventory
run: |
cat > inventory.ini <<EOF
[web]
${EC2_INSTANCE_ID}
[web:vars]
ansible_connection=amazon.aws.aws_ssm
ansible_python_interpreter=/usr/bin/python3
ansible_aws_ssm_region=${AWS_REGION}
ansible_aws_ssm_bucket_name=${ANSIBLE_S3_BUCKET}
EOF
- name: Deploy with Ansible
run: ansible-playbook -i inventory.ini ansible/site.yml
- name: Check the live website
run: curl --fail --retry 5 --retry-delay 3 "$WEBSITE_URL"
How does the secure deployment work?
- The runner installs the Session Manager plugin and prints its version.
- The inventory maps the instance ID to the web host group at runtime.
- The amazon.aws.aws_ssm connection sends Ansible modules through Systems Manager.
- The bucket variable points Ansible at the Amazon S3 transfer bucket.
- The final request retries transient failures before deciding whether the website is reachable.
- Save .github/workflows/ansible.yml.
✔️ Awesome, I've got everything!
Your validation and deployment jobs are ready. Double-check that you saved the workflow.
ⓧ I'd like to double check the full code
name: Ansible CI/CD
on:
push:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@v7.0.0
with:
python-version: "3.13.16"
cache: pip
- name: Install Python requirements
run: python -m pip install -r requirements.txt
- name: Install Ansible collections
run: ansible-galaxy collection install -r requirements.yml
- name: Lint the playbook
run: ansible-lint ansible/site.yml
- name: Check playbook syntax
run: ansible-playbook --syntax-check ansible/site.yml
deploy:
needs: validate
runs-on: ubuntu-latest
env:
AWS_REGION: ${{ vars.AWS_REGION }}
EC2_INSTANCE_ID: ${{ vars.EC2_INSTANCE_ID }}
ANSIBLE_S3_BUCKET: ${{ vars.ANSIBLE_S3_BUCKET }}
WEBSITE_URL: ${{ vars.WEBSITE_URL }}
steps:
- name: Check out the repository
uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@v7.0.0
with:
python-version: "3.13.16"
cache: pip
- name: Install Python requirements
run: python -m pip install -r requirements.txt
- name: Install Ansible collections
run: ansible-galaxy collection install -r requirements.yml
- name: Configure temporary AWS credentials
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: Install the Session Manager plugin
run: |
curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb"
sudo dpkg -i session-manager-plugin.deb
session-manager-plugin --version
- name: Build the runtime inventory
run: |
cat > inventory.ini <<EOF
[web]
${EC2_INSTANCE_ID}
[web:vars]
ansible_connection=amazon.aws.aws_ssm
ansible_python_interpreter=/usr/bin/python3
ansible_aws_ssm_region=${AWS_REGION}
ansible_aws_ssm_bucket_name=${ANSIBLE_S3_BUCKET}
EOF
- name: Deploy with Ansible
run: ansible-playbook -i inventory.ini ansible/site.yml
- name: Check the live website
run: curl --fail --retry 5 --retry-delay 3 "$WEBSITE_URL"
Push and verify the live deployment
The completed workflow now has every piece required to change the instance. The next push tests validation, authentication, connection, configuration, and HTTP reachability as one delivery path.
- Commit .github/workflows/ansible.yml with the message Deploy NGINX through Systems Manager.
Before you push, which job do you expect to finish first?
- Push the completed workflow to main.
You'll see validate turn green before deploy begins. This ordering proves that the CI gate still protects AWS.
- Open the latest run from the repository's Actions tab.
- Select the deploy job.
- Expand the Install the Session Manager plugin step to check the reported version.
- Expand the Deploy with Ansible step to inspect the task results.
- Expand the Check the live website step to confirm the HTTP request succeeded.
The plugin reports version 1.2.764.0 or later. The Ansible recap reports failed=0.
The final website check is green. This confirms that WEBSITE_URL responds successfully over HTTP.
Deploy job failed?
A TargetNotConnected message points to the instance registration path. Check the instance profile, SSM Agent, outbound connectivity, and instance initialization.
A missing session-manager-plugin executable points to the runner installation step. S3 bucket or access failures point to ANSIBLE_S3_BUCKET or the deploy role's bucket permissions.
Still stuck? Help me identify the failing layer in my secure Ansible deployment.
Before you open the website, what deployment marker do you expect the page to show?
- Open the address stored in the WEBSITE_URL repository variable in a browser tab.
You'll see the heading Deployed by Ansible. The page also states that GitHub Actions deployed it through AWS Systems Manager.
That's the secure deployment path working end to end. NGINX is live without an inbound port 22 rule.
Your pipeline can now validate a playbook and apply it securely to a live server. Next, you'll break one deployment variable on purpose and diagnose the failure from the workflow logs.
Diagnose a Live Pipeline Failure
The last step left your GitHub Actions pipeline with a green validation job. Its deployment job also published the live page through the secure connection path.
Real pipelines eventually meet configuration drift or an incorrect variable. Rerunning the same job without reading its evidence can hide the failing layer.
You'll create a reversible runtime failure. Then you'll use its first meaningful error to recover the deployment without changing the Ansible playbook.
In this step, get ready to:
- Trigger a reversible transfer failure with a repository variable.
- Identify the failed pipeline layer from workflow logs.
- Prove recovery with an idempotent green deployment.
Trigger a controlled transfer failure
Repository variables supply runtime values without changing the committed workflow file. Your deployed website remains intact during this controlled test.
- In your GitHub repository, select Settings.
- Select Secrets and variables in the sidebar.
- Select Actions.
- Select the Variables tab.
- Locate the ANSIBLE_S3_BUCKET row.
- Use the row's edit control.
- Replace the Value with missing-ansible-bucket.
- Save the updated variable using the form's confirmation control.
Before you start the manual run, do you think the validation job and deployment job will reach the same outcome?
- Select the Actions tab in your repository.
- Select the Ansible CI/CD workflow in the left sidebar.
- Select Run workflow above the workflow run list.
- Select main from the Branch menu.
- Select Run workflow inside the menu.
The validate job turns green. The deploy job turns red during the Ansible connection or transfer setup.
This failure is intentional. The split result proves that the playbook remains valid while a runtime deployment input is broken.
Workflow did not fail as expected?
- Confirm that the ANSIBLE_S3_BUCKET row still shows missing-ansible-bucket.
- Confirm that you opened the newest run from main.
- Confirm that you changed the variable in the same repository that contains the workflow.
Still seeing a green deployment? Help me find why the workflow did not use the temporary ANSIBLE_S3_BUCKET value.
Trace the first meaningful error
A workflow often prints several follow-on messages after the original fault. The first meaningful error names a resource or connection state that explains the messages below it.
How to classify the failure
- A failure in Configure temporary AWS credentials points to an OpenID Connect trust problem before credentials are configured.
- A TargetNotConnected error points to the Amazon EC2 managed node or AWS Systems Manager Session Manager registration.
- A session-manager-plugin installation error points to the GitHub-hosted runner.
- An Amazon S3 error inside Deploy with Ansible points to the transfer bucket name or its permissions.
- Select the failed workflow run.
- Select the deploy job.
- Expand the Deploy with Ansible step.
- Read upward from the final failure summary until you reach the first line that identifies a concrete bucket or connection problem.
You should find the first meaningful error inside Deploy with Ansible. Its context shows that amazon.aws.aws_ssm cannot prepare module transfer with missing-ansible-bucket.
The successful credential setup and plugin installation narrow the fault to transfer configuration. You now know which input controls the failing layer.
Restore the bucket and prove idempotence
The failure is isolated to the transfer bucket input. The AWS CloudFormation output is the source of truth for the original bucket name.
- Return to the AWS Management Console from earlier.
- Enter CloudFormation in the console search bar.
- Select CloudFormation in the search results.
- Select the ansible-cicd-demo stack.
- Select the Outputs tab.
- Find the AnsibleTransferBucket row.
- Record the output value here: your AnsibleTransferBucket output.
That output names the bucket created for Ansible module transfer. Restoring it reconnects the workflow to the original infrastructure.
- Switch back to the GitHub repository Variables page from earlier.
- Locate the ANSIBLE_S3_BUCKET row.
- Use the row's edit control.
- Replace the Value with your AnsibleTransferBucket output.
- Save the restored variable using the form's confirmation control.
Before you rerun the workflow, what do you expect the Ansible recap to report when the host already matches the playbook?
- Select the Actions tab.
- Select the Ansible CI/CD workflow.
- Select Run workflow.
- Select main from the Branch menu.
- Select Run workflow inside the menu.
The validate job turns green. The deploy job also turns green.
In Deploy with Ansible, the final recap reports failed=0 and changed=0.
Why changed=0 matters
The failed=0 result confirms that every task completed. The changed=0 result demonstrates idempotence.
Ansible found that the host already matched the declared state. It applied zero changes during the recovered run.
Rerun still failing or changing tasks?
- Compare the repository variable character for character with the AnsibleTransferBucket output.
- Expand the first failed step if the deployment still reports an S3 transfer problem.
- Expand any task that reports a change if the recap does not show changed=0.
- Use the changed task name to identify the unexpected drift on the host.
Need help reading the recovered run? Help me diagnose why my restored Ansible deployment still fails or reports unexpected changes.
You recovered the pipeline without changing the workflow or deployed website. The logs now give you a repeatable way to isolate validation failures, connection failures, and configuration drift.
Secret mission
Add a Deployment Health Gate
Upgrade the final check so a green deployment proves that the live page contains the intended release marker. You will make the gate fail on purpose before restoring it to green.
Clean Up Your Resources
Clean Up Your Resources
Active AWS resources can keep accruing charges after you finish this project. Choose the cleanup path that fits what you want to do next.
Cost warning
Amazon EC2 lists a Linux t3.micro in us-east-1 at $0.0104 per hour. An in-use public IPv4 address adds $0.005 per hour.
gp3 storage is listed at $0.08 per GB-month.
Amazon S3 can add storage charges. Its requests can add further charges.
AWS credits or Free Tier eligibility may cover some usage. This project does not assume either.
Resources you used:
- The ansible-cicd-demo AWS CloudFormation stack.
- The stack-managed VPC networking resources.
- The stack-managed ansible-cicd-web Amazon EC2 instance.
- The public IPv4 address plus Amazon EBS storage attached to the instance.
- The stack-managed AWS Identity and Access Management roles plus instance profile.
- The stack-managed Amazon S3 transfer bucket.
- The stack-managed OpenID Connect provider for GitHub when the template created it.
- The GitHub repository ansible-aws-cicd.
Keep everything running
No action needed. Choose this if you are still testing the pipeline or demonstrating the live site.
- The ansible-cicd-demo stack remains active.
- Your live NGINX site remains available through WEBSITE_URL.
- Your GitHub Actions workflow remains ready for validation plus deployment runs.
- Your workflow history plus repository variables remain available.
- EC2 compute charges continue while the instance runs.
- Public IPv4 charges continue while the address stays allocated.
- EBS plus S3 charges can continue while those resources exist.
Pause - I'll come back to this later
Pausing stops the EC2 instance while preserving the stack for your return. Persistent resources can still incur charges.
- Return to the ansible-cicd-web instance in the Amazon EC2 console.
- Use the instance state control to stop the instance.
- Confirm that the instance has finished stopping.
- Start the same instance before running another deployment.
The page at WEBSITE_URL stops responding while the instance is stopped. Deployments that rely on AWS Systems Manager cannot finish.
Delete - I don't want to use this again
Remove the AWS stack plus the GitHub repository if you no longer need the live site or its pipeline. This stops charges from the stack-managed resources.
Deletion is permanent
Deletion is a big reset. Removing ansible-cicd-demo takes down the live site. Removing ansible-aws-cicd erases its workflow history plus repository variables.
Delete the AWS stack:
- Return to the ansible-cicd-demo stack in AWS CloudFormation.
- Select the Outputs tab.
- Locate AnsibleTransferBucket.
- Use the matching value to find the transfer bucket in Amazon S3.
- Empty the transfer bucket if it contains any objects.
- Return to the ansible-cicd-demo stack.
- Delete the selected stack.
- Complete the stack deletion confirmation.
- Confirm that the stack reaches DELETE_COMPLETE.
CloudFormation removes only the OpenID Connect provider created by this stack. A provider supplied through ExistingGitHubOidcProviderArn stays in the account.
Delete the GitHub repository:
- Return to ansible-aws-cicd on GitHub.
- Open the repository Settings.
- Scroll to the repository deletion area.
- Use the repository deletion control to remove ansible-aws-cicd.
- Complete the repository deletion confirmation.
- Confirm that the repository page no longer shows the project.
Nice Work!
Nice Work!
You did it! Your secure CI/CD pipeline now validates configuration before deploying a verified NGINX release to Amazon EC2.
You've learned how to:
- Built a GitHub Actions CI job with a pinned Ansible toolchain that blocks invalid playbooks before the EC2 instance changes.
- Secured each deployment with temporary OpenID Connect credentials plus AWS Systems Manager Session Manager connectivity without stored AWS access keys or inbound SSH.
- Demonstrated pipeline recovery by tracing a bad ANSIBLE_S3_BUCKET value from failed logs to a green rerun reporting changed=0.
- Secret Mission: Added a content-level deployment health gate that fails when the live page lacks its expected release marker.
Ready to quiz yourself?