Build a Monitored ECS Platform
Deploy a monitored ECS platform with CloudFormation and GitHub Actions.
Introduction
30 Second Summary
A polished web page can look simple even when the work behind it is complex. Without evidence, the choices that keep it available stay invisible.
In this project, you will build a public portfolio page on AWS with a container platform defined in CloudFormation. GitHub Actions handles each release while CloudWatch provides the operational evidence behind it.
What You'll Build
You open a public URL to see your healthy portfolio page backed by a repository that proves every stage from commit to monitored service.
By the end of this project, you'll have:
- A public portfolio page that a hiring manager can open through an Application Load Balancer. The page visibly reports your Linux, networking, IAM, infrastructure as code, container, CI/CD, and monitoring skills.
- A reproducible Amazon ECS service that runs on AWS Fargate inside a two-subnet Amazon VPC. You can demonstrate its logs, service metrics, dashboard, and healthy-host alarm in CloudWatch.
- A public GitHub repository where a push to main uses short-lived OIDC credentials to publish the image to Amazon ECR before refreshing the live service. Its README shows the architecture, successful workflow, monitoring evidence, trade-offs, and cleanup path.
- An optional Secret Mission: Simulate a controlled outage. Diagnose the missing healthy target through the load balancer and CloudWatch alarm. Restore the service to a healthy state.
Are there any prerequisites?
You only need a Windows device plus a browser because account setup leads into browser-based AWS CloudShell with no local cloud toolchain required.
The AWS Paid plan can accrue charges, so you will delete the stack in the same session to keep the project below its $10 target.
Before We Start
Your platform will connect infrastructure, delivery, security, and monitoring into one public portfolio project. Before the hands-on work begins, this checkpoint helps you lock in what you are building and why it matters for a cloud engineering role.
Set Up Secure AWS and GitHub Access
Your container platform needs secure access to AWS before any infrastructure can be built. It also needs a GitHub home before delivery can be automated.
Browser-based AWS CloudShell keeps the cloud toolchain inside AWS. SSH connects that shell to GitHub without installing cloud tools on Windows.
In this step, get ready to:
- Secure your AWS account with billing controls.
- Verify the CloudShell toolchain.
- Create the public repository with SSH access.
Create secure AWS and GitHub accounts
Account setup touches billing and credentials, so the guardrails matter. The Paid plan unlocks the required AWS services while same-session cleanup keeps this project focused on its $10 spending target.
- Open the AWS account plans page in your browser.
- Start the current AWS account sign-up flow.
- Select the Paid plan when AWS asks you to choose a plan.
- Set the project spend limit to $10 when that option is presented.
- Complete the required account verification prompts.
- Complete the multi-factor authentication prompts for your account.
- Skip any option to create root access keys.
- Sign in to the AWS Management Console with your secured account.
Why protect the root user?
Multi-factor authentication adds another identity check to the root sign-in. Root access keys create long-lived programmatic credentials with broad account power.
Your CloudShell session uses the signed-in console identity, so root access keys provide no benefit to this project.
- Create your GitHub account through the GitHub sign-up page.
That is the account groundwork complete. Your cloud access now has MFA protection while your source-control account is ready to hold the project.
Launch CloudShell and verify its tools
CloudShell provides an Amazon Linux environment that is already authenticated to your AWS account. Its standard environment includes AWS CLI v2, Git, and Docker.
- Set the AWS Management Console region to us-east-1.
- Select CloudShell from the console toolbar.
- Wait for the shell prompt to become available.
- Check the three preinstalled tools by running these commands:
aws --version
git --version
docker --version
What do these checks prove?
- The first output identifies the preinstalled AWS CLI release.
- The second output confirms that Git can manage the project history.
- The third output confirms that Docker can build the portfolio container inside CloudShell.
✔️ I see AWS CLI v2 and the other versions
Your managed shell has the complete toolchain. You can build from Amazon Linux without configuring a Windows development environment.
ⓧ I see an older AWS CLI version
- Refresh the AWS Management Console browser tab.
- Confirm that the selected region is us-east-1.
- Return to CloudShell from the console toolbar.
- Repeat the three version checks above.
Still seeing the older version?
Standard CloudShell provides AWS CLI v2. Confirm that you opened the browser-based CloudShell environment from your AWS Management Console.
If the older version remains, help me diagnose this CloudShell version mismatch.
ⓧ A command is missing
- Confirm that the current region is us-east-1.
- Refresh the AWS Management Console browser tab.
- Return to CloudShell from the console toolbar.
- Repeat the three version checks above.
Tool still missing?
The standard CloudShell environment supplies AWS CLI, Git, and Docker. A missing command usually means the expected CloudShell environment did not finish loading.
If a command remains unavailable, help me restore the standard CloudShell tools.
Create the repository and connect SSH
The empty repository gives the files you build later a clean remote destination. Leaving it empty also avoids an unrelated initial commit before CloudShell publishes your work.
- Select New repository from the menu in the upper-right corner of GitHub.
- Select your personal account from the Owner menu.
- Enter aws-cloud-engineer-capstone in the Repository name field.
- Choose Public as the repository visibility.
- Leave the README option unselected.
- Leave the .gitignore option unchanged.
- Leave the license option unchanged.
- Click Create repository.
Your public repository now shows its empty-repository setup instructions. That clean destination is ready for the project files you create next.
An Ed25519 key pair lets CloudShell prove its identity to GitHub. The private key stays inside CloudShell while GitHub receives only the public key.
- Replace your_email@example.com with the email address linked to your GitHub account.
- Press Enter when the command asks where to save the key.
- Protect the private key by entering a passphrase when prompted.
- Generate the Ed25519 key pair by running this command:
ssh-keygen -t ed25519 -C "your_email@example.com"
What does this command do?
- The Ed25519 option chooses the key algorithm used for this SSH identity.
- The email comment helps you recognize the key later.
- The command creates a private key plus a matching public key in your CloudShell home directory.
The terminal prints the saved key locations plus a fingerprint. Your CloudShell home directory now holds the SSH identity for this project.
Key generation not finishing?
Make sure you respond to the file-location prompt before entering the passphrase. The command remains active while it waits for each response.
If CloudShell reports an existing key file, avoid overwriting a key you still use. Help me create a separate Ed25519 key safely.
- Display the new public key by running this command:
cat ~/.ssh/id_ed25519.pub
Which part can GitHub receive?
The displayed line is the public half of the key pair. Sharing this line with GitHub allows authentication without revealing your private key.
- Copy the complete public-key line from the CloudShell output.
- Click your profile picture in the upper-right corner of GitHub.
- Click Settings.
- Select SSH and GPG keys from the sidebar.
- Click New SSH key.
- Enter CloudShell in the Title field.
- Select the authentication key type.
- Paste the public-key line into the Key field.
- Click Add SSH key.
- Confirm access to your GitHub account if prompted.
Why is the public key safe to add?
The public key can verify signatures created by its matching private key. It cannot reveal the private key stored in CloudShell.
- Enter your SSH key passphrase if CloudShell asks for it during the test.
- Type yes if the first-connection confirmation appears.
Before you run the final check, which command do you expect to prove that CloudShell can authenticate to GitHub?
- Verify the complete toolchain plus the GitHub connection by running these commands:
aws --version
git --version
docker --version
ssh -T git@github.com
What should you see?
- The first three commands print the installed AWS CLI, Git, and Docker versions.
- The final command prints a GitHub authentication success message that includes your username.
- GitHub also explains that it does not provide shell access. That message confirms the SSH test succeeded.
You now have the secure access path for the whole project. CloudShell can use AWS services while its Ed25519 key can authenticate Git operations with your empty public repository.
SSH authentication not succeeding?
Confirm that GitHub received the complete line printed from id_ed25519.pub. Extra missing characters prevent the public key from matching your CloudShell private key.
Confirm that the key appears under SSH and GPG keys in the same GitHub account that owns aws-cloud-engineer-capstone. Help me diagnose this SSH authentication failure.
Your accounts, browser-based toolchain, repository, and SSH connection are ready. Next, you will build the portfolio page as a container inside CloudShell.
Build the Portfolio Container
Your secure AWS CloudShell environment can now turn the empty repository into a working portfolio service. This gives you a small version of the final platform before any cloud infrastructure is deployed.
You will package the page with Docker and the pinned NGINX image. The first test stays inside your CloudShell session on port 8080.
In this step, get ready to:
- Create the application and automation files in CloudShell.
- Build the portfolio image and start its local container.
- Remove the test container while keeping the source files and image.
Create the project files
CloudShell preserves files stored in its home directory. The project folder will hold the page, container recipe, infrastructure template, delivery workflow, and portfolio documentation.
- Create the aws-cloud-engineer-capstone folder structure in your CloudShell home directory by running these commands:
mkdir ~/aws-cloud-engineer-capstone
cd ~/aws-cloud-engineer-capstone
mkdir -p .github/workflows
touch index.html Dockerfile infra.yml .github/workflows/deploy.yml README.md
What do these commands create?
- The first two commands create the project folder in persistent CloudShell storage and make it your current directory.
- The next command creates the nested directory used by GitHub Actions workflows.
- The final command creates the five empty project files that you will fill.
- Confirm the new files exist by running this command:
ls
You should see Dockerfile, README.md, index.html, and infra.yml in the output. The hidden .github directory also exists for the workflow.
- Open index.html in the CloudShell editor by running this command:
edit index.html
CloudShell opens index.html in an editor tab with syntax highlighting.
- Select the full-code tab below.
- Copy the complete page into index.html.
- Save index.html using the editor's save control.
✔️ My page is saved
Your portfolio page now describes the cloud engineering capabilities demonstrated by the finished platform.
ⓧ Double-check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AWS Cloud Engineer Capstone</title>
<style>
:root { color-scheme: dark; font-family: Inter, system-ui, sans-serif; background: #07111f; color: #e8f0ff; }
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: radial-gradient(circle at top, #17345d, #07111f 60%); }
main { width: min(900px, 88vw); padding: 3rem; border: 1px solid #31527c; border-radius: 20px; background: rgba(8, 23, 42, 0.92); box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35); }
h1 { margin-top: 0; font-size: clamp(2rem, 5vw, 4rem); }
.status { color: #70e1a1; font-weight: 700; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-top: 2rem; }
article { padding: 1rem; border-radius: 12px; background: #102540; }
article h2 { margin: 0 0 0.5rem; font-size: 1rem; color: #8ec5ff; }
article p { margin: 0; line-height: 1.5; }
footer { margin-top: 2rem; color: #9db1c9; }
</style>
</head>
<body>
<main>
<p class="status">Service status: healthy</p>
<h1>AWS Cloud Engineer Capstone</h1>
<p>This page is delivered by a container platform built as code and released through a secure CI/CD workflow.</p>
<section class="grid">
<article><h2>Linux</h2><p>Built and tested from an Amazon Linux shell in AWS CloudShell.</p></article>
<article><h2>Networking</h2><p>Runs across two public subnets behind an Application Load Balancer and layered security groups.</p></article>
<article><h2>Infrastructure as Code</h2><p>Provisioned as one reproducible AWS CloudFormation stack.</p></article>
<article><h2>Containers</h2><p>Packaged with Docker, stored in Amazon ECR, and run by Amazon ECS on AWS Fargate.</p></article>
<article><h2>CI/CD and IAM</h2><p>Deployed from GitHub Actions through short-lived OIDC credentials and a scoped IAM role.</p></article>
<article><h2>Monitoring</h2><p>Observed through CloudWatch logs, service metrics, a dashboard, and a healthy-host alarm.</p></article>
</section>
<footer>Built as evidence for a cloud engineering role.</footer>
</main>
</body>
</html>
What does this page contain?
The page presents a healthy service status and six capability cards. Its embedded styles create the complete layout without another application file.
- Return to the CloudShell terminal.
- Open Dockerfile in the editor by running this command:
edit Dockerfile
CloudShell opens the container recipe in a new editor tab.
- Select the full-code tab below.
- Copy the container recipe into Dockerfile.
- Save Dockerfile using the editor's save control.
✔️ My Dockerfile is saved
Your container recipe now pins the NGINX base and places the portfolio page in its static content directory.
ⓧ Double-check the full code
FROM nginx:1.31.6-alpine
COPY index.html /usr/share/nginx/html/index.html
What does this container recipe do?
- The FROM instruction pins the supported nginx:1.31.6-alpine base image.
- The COPY instruction replaces the default static page with your index.html file.
- Confirm the two saved files contain data by running this command:
ls -l index.html Dockerfile
You should see both file names with sizes greater than zero. This confirms that CloudShell saved the page and container recipe.
- Open infra.yml in the CloudShell editor by running this command:
edit infra.yml
CloudShell opens the infrastructure template in a new editor tab.
- Select the full-code tab below.
- Copy the complete template into infra.yml.
- Save infra.yml using the editor's save control.
✔️ My template is saved
Your infrastructure definition is ready for the deployment step.
ⓧ Double-check the full code
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS cloud engineer capstone with VPC, ECS Fargate, GitHub OIDC, and CloudWatch.
Parameters:
GitHubSubject:
Type: String
Description: Immutable GitHub OIDC subject for the main branch.
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: cloud-engineer-capstone
InternetGateway:
Type: AWS::EC2::InternetGateway
GatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref Vpc
InternetGatewayId: !Ref InternetGateway
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
DefaultRoute:
Type: AWS::EC2::Route
DependsOn: GatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
SubnetRouteAssociationA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicRouteTable
SubnetRouteAssociationB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetB
RouteTableId: !Ref PublicRouteTable
LoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow public HTTP traffic to the load balancer.
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
ServiceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP only from the load balancer.
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
ContainerRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: cloud-engineer-capstone
ImageTagMutability: MUTABLE
EmptyOnDelete: true
Cluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: cloud-engineer-capstone
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/cloud-engineer-capstone
RetentionInDays: 7
TaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: cloud-engineer-capstone-task-execution
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: cloud-engineer-capstone
Cpu: '256'
Memory: '512'
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
ContainerDefinitions:
- Name: web
Image: !Sub '${ContainerRepository.RepositoryUri}:latest'
Essential: true
PortMappings:
- ContainerPort: 80
Protocol: tcp
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: ecs
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: cloud-engineer-capstone
Scheme: internet-facing
Type: application
SecurityGroups:
- !Ref LoadBalancerSecurityGroup
Subnets:
- !Ref PublicSubnetA
- !Ref PublicSubnetB
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: cloud-engineer-capstone
VpcId: !Ref Vpc
Port: 80
Protocol: HTTP
TargetType: ip
HealthCheckEnabled: true
HealthCheckPath: /
Matcher:
HttpCode: '200'
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
Service:
Type: AWS::ECS::Service
DependsOn: Listener
Properties:
ServiceName: cloud-engineer-capstone
Cluster: !Ref Cluster
TaskDefinition: !Ref TaskDefinition
DesiredCount: 0
LaunchType: FARGATE
PlatformVersion: LATEST
AvailabilityZoneRebalancing: ENABLED
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 100
DeploymentCircuitBreaker:
Enable: true
Rollback: true
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- !Ref ServiceSecurityGroup
Subnets:
- !Ref PublicSubnetA
- !Ref PublicSubnetB
LoadBalancers:
- ContainerName: web
ContainerPort: 80
TargetGroupArn: !Ref TargetGroup
GitHubOidcProvider:
Type: AWS::IAM::OIDCProvider
Properties:
Url: https://token.actions.githubusercontent.com
ClientIdList:
- sts.amazonaws.com
GitHubDeployRole:
Type: AWS::IAM::Role
Properties:
RoleName: cloud-engineer-capstone-github-deploy
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Federated: !Ref GitHubOidcProvider
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
'token.actions.githubusercontent.com:aud': sts.amazonaws.com
'token.actions.githubusercontent.com:sub': !Ref GitHubSubject
Policies:
- PolicyName: PushImageAndDeployService
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: ecr:GetAuthorizationToken
Resource: '*'
- Effect: Allow
Action:
- ecr:BatchCheckLayerAvailability
- ecr:CompleteLayerUpload
- ecr:InitiateLayerUpload
- ecr:PutImage
- ecr:UploadLayerPart
Resource: !GetAtt ContainerRepository.Arn
- Effect: Allow
Action:
- ecs:DescribeServices
- ecs:UpdateService
- cloudformation:DescribeStacks
Resource: '*'
HealthyHostAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: cloud-engineer-capstone-health
AlarmDescription: Alarm when the load balancer has fewer than one healthy target.
Namespace: AWS/ApplicationELB
MetricName: HealthyHostCount
Dimensions:
- Name: LoadBalancer
Value: !GetAtt ApplicationLoadBalancer.LoadBalancerFullName
- Name: TargetGroup
Value: !GetAtt TargetGroup.TargetGroupFullName
Statistic: Average
Period: 60
EvaluationPeriods: 1
DatapointsToAlarm: 1
Threshold: 1
ComparisonOperator: LessThanThreshold
TreatMissingData: breaching
MonitoringDashboard:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: cloud-engineer-capstone
DashboardBody: !Sub |
{
"widgets": [
{"type":"metric","x":0,"y":0,"width":12,"height":6,"properties":{"region":"${AWS::Region}","title":"ECS service utilization","view":"timeSeries","metrics":[["AWS/ECS","CPUUtilization","ClusterName","cloud-engineer-capstone","ServiceName","cloud-engineer-capstone"],[".","MemoryUtilization",".",".",".","."]]}},
{"type":"metric","x":12,"y":0,"width":12,"height":6,"properties":{"region":"${AWS::Region}","title":"Load balancer traffic and health","view":"timeSeries","metrics":[["AWS/ApplicationELB","RequestCount","LoadBalancer","${ApplicationLoadBalancer.LoadBalancerFullName}"],["AWS/ApplicationELB","HealthyHostCount","TargetGroup","${TargetGroup.TargetGroupFullName}","LoadBalancer","${ApplicationLoadBalancer.LoadBalancerFullName}"]]}}
]
}
Outputs:
ApplicationUrl:
Description: Public URL for the portfolio service.
Value: !Sub 'http://${ApplicationLoadBalancer.DNSName}'
GitHubRoleArn:
Description: Store this non-secret ARN in the GitHub variable AWS_ROLE_ARN.
Value: !GetAtt GitHubDeployRole.Arn
RepositoryUri:
Description: Amazon ECR repository URI.
Value: !GetAtt ContainerRepository.RepositoryUri
DashboardName:
Description: CloudWatch dashboard name.
Value: !Ref MonitoringDashboard
What does this template define?
- The networking resources create two public subnets with internet routing and layered security groups.
- The container resources define the ECR repository, ECS cluster, Fargate task, load balancer, and service.
- The security and operations resources define GitHub OIDC access, execution roles, logs, a dashboard, and an alarm.
- Return to the CloudShell terminal.
- Open .github/workflows/deploy.yml in the editor by running this command:
edit .github/workflows/deploy.yml
CloudShell opens the deployment workflow inside the nested .github/workflows directory.
- Select the full-code tab below.
- Copy the complete workflow into .github/workflows/deploy.yml.
- Save .github/workflows/deploy.yml using the editor's save control.
✔️ My workflow is saved
Your delivery workflow is ready to build and deploy the container after the cloud resources exist.
ⓧ Double-check the full code
name: Deploy to Amazon ECS
on:
push:
branches:
- main
workflow_dispatch:
permissions:
id-token: write
contents: read
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: cloud-engineer-capstone
ECS_CLUSTER: cloud-engineer-capstone
ECS_SERVICE: cloud-engineer-capstone
IMAGE_TAG: latest
jobs:
deploy:
runs-on: ubuntu-24.04
steps:
- name: Check out repository
uses: actions/checkout@v7.0.1
- name: Configure short-lived AWS credentials
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
role-session-name: ${{ github.run_id }}
aws-region: ${{ env.AWS_REGION }}
- name: Build and push image
shell: bash
run: |
set -euo pipefail
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGISTRY="$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$REGISTRY"
docker build -t "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
docker push "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: Deploy image to ECS
shell: bash
run: |
set -euo pipefail
aws ecs update-service \
--cluster "$ECS_CLUSTER" \
--service "$ECS_SERVICE" \
--desired-count 1 \
--force-new-deployment
aws ecs wait services-stable \
--cluster "$ECS_CLUSTER" \
--services "$ECS_SERVICE"
aws cloudformation describe-stacks \
--stack-name cloud-engineer-capstone \
--query 'Stacks[0].Outputs[?OutputKey==`ApplicationUrl`].OutputValue' \
--output text
What does this workflow automate?
A push to main requests short-lived AWS credentials through OIDC. The job builds the image, pushes latest to ECR, and updates the ECS service.
- Confirm the infrastructure and workflow files contain data by running this command:
ls -l infra.yml .github/workflows/deploy.yml
You should see both paths with sizes greater than zero. This confirms that the infrastructure and delivery definitions were saved.
- Open README.md in the CloudShell editor by running this command:
edit README.md
CloudShell opens the portfolio documentation in a new editor tab.
- Select the full-code tab below.
- Copy the complete documentation into README.md.
- Save README.md using the editor's save control.
✔️ My README is saved
Your repository documentation now explains the architecture, skills, security decisions, evidence plan, and cleanup command.
ⓧ Double-check the full code
# AWS Cloud Engineer Capstone
A portfolio project that deploys a containerized web page to Amazon ECS with AWS Fargate using CloudFormation and GitHub Actions.
## Live demonstration
Application URL: Add the CloudFormation `ApplicationUrl` output here while the stack is running.
## Architecture
```mermaid
flowchart LR
Developer[Git push] --> Actions[GitHub Actions]
Actions -->|OIDC temporary credentials| IAM[IAM deploy role]
Actions --> ECR[Amazon ECR]
Internet[Hiring manager] --> ALB[Application Load Balancer]
ALB --> ECS[Amazon ECS service]
ECR --> ECS
ECS --> Fargate[AWS Fargate task]
Fargate --> Logs[CloudWatch Logs]
ECS --> Metrics[CloudWatch dashboard and alarm]
```
## Skills demonstrated
- Linux operations in AWS CloudShell
- Git and GitHub repository workflow
- VPC, subnet, route table, internet gateway, and security-group design
- AWS CloudFormation infrastructure as code
- Docker image creation and Amazon ECR storage
- Amazon ECS orchestration on AWS Fargate
- GitHub Actions CI/CD with GitHub OIDC and IAM
- CloudWatch logs, metrics, dashboard, and alarm response
## Security decisions
- The GitHub workflow uses short-lived OIDC credentials rather than long-lived AWS access keys.
- The trust policy binds the role to the immutable owner ID, repository ID, and `main` branch subject.
- The ECS task accepts HTTP only from the load balancer security group.
- The workflow role is limited to ECR image upload, ECS service deployment, and stack output inspection.
## Why ECS on Fargate
Amazon ECS exposes AWS networking, IAM, service deployment, and monitoring concepts without requiring a first-time learner to operate Kubernetes. AWS App Runner was simpler but would hide much of the requested network and orchestration layer. Amazon EKS was deferred because one static container does not justify its Kubernetes control plane and operational scope.
## Evidence
- Add a screenshot of the successful GitHub Actions run.
- Add a screenshot of the live application.
- Add a screenshot of the CloudWatch dashboard and healthy alarm.
- Add the short outage and recovery note from the Secret Mission.
## Cleanup
Delete the stack after collecting evidence:
```text
aws cloudformation delete-stack --stack-name cloud-engineer-capstone
```
The public GitHub repository remains available as the permanent portfolio artifact.
How does the README support the project?
The Mermaid diagram shows the delivery path from a Git push to the running Fargate task. The evidence section reserves space for the live results you capture later.
- Confirm the final source file contains data by running this command:
ls -l README.md
You should see README.md with a size greater than zero. All five project files are now stored in the CloudShell home directory.
File missing or still empty?
Confirm that your terminal prompt is inside aws-cloud-engineer-capstone. Check that you used the editor's save control before closing each tab.
Make sure the workflow path includes both hidden directories. Help me check my CloudShell project files.
Build and run the container
A container image packages the page with the web server that serves it. Building locally proves that the source and Dockerfile work together before the image enters AWS.
Why test the container locally?
A local build isolates application packaging from networking and deployment. A successful run proves that Docker can assemble the NGINX image and publish its web port.
- Build the local image from Dockerfile by running this command:
docker build -t some-content-nginx .
What does this command do?
- The build reads Dockerfile from the current directory.
- The -t option names the resulting image some-content-nginx.
- The final period makes the current folder the build context.
Docker prints the completed build stages and records the image under some-content-nginx.
Build not completing?
Confirm that the terminal is inside aws-cloud-engineer-capstone. Check that Dockerfile starts with the pinned NGINX image line.
Check that index.html is saved beside Dockerfile. Help me diagnose this Docker build.
Before you start the container, predict what Docker will print when the detached process begins.
- Start the portfolio container on port 8080 by running this command:
docker run --name some-nginx -d -p 8080:80 some-content-nginx
What does this command do?
- The --name option assigns the container the name some-nginx.
- The -d option keeps the container running in the background.
- The -p 8080:80 option maps CloudShell port 8080 to NGINX port 80.
Docker prints a container ID. That identifier confirms your portfolio page is running from some-content-nginx on port 8080.
That is the first runnable version complete. Your custom page now has a repeatable Linux container environment.
Container not starting?
Check that the image build completed before you ran the container command. A failed build leaves no some-content-nginx image to start.
If Docker reports that the container name already exists, remove the previous test container before retrying. Help me troubleshoot this local container.
Stop the local test container
The local container has completed its packaging test. Removing that temporary process frees port 8080 while preserving the image and project files for the deployment work.
Before you clean up, predict which artifact will remain available after the named container is removed.
- Stop the running test container by running this command:
docker stop some-nginx
Docker prints some-nginx after the container process stops.
- Remove the stopped test container by running this command:
docker rm some-nginx
Docker prints some-nginx again after removing its container metadata.
- Verify that the built image remains available by running this command:
docker images some-content-nginx
What should remain?
The output still lists some-content-nginx with its image ID and size. Removing the container leaves the reusable image untouched.
You have completed the local container loop. The source files and built image remain in CloudShell while no test container occupies port 8080.
Cleanup command not working?
Confirm that you used the container name some-nginx. The image name some-content-nginx identifies a different Docker object.
If the image list is empty, return to the build command before continuing. Help me verify my Docker cleanup state.
Your portfolio now runs as a tested container image. Next, you will turn its saved infrastructure definition into a public AWS platform.
Deploy the AWS Platform
Your portfolio container already works inside the AWS CloudShell Linux environment. Its current address exists only inside one shell session.
AWS CloudFormation turns the existing infra.yml template into a reproducible public platform. An Application Load Balancer gives internet traffic a route into that platform.
In this step, get ready to:
- Capture the immutable GitHub OIDC subject for your repository.
- Deploy the CloudFormation stack with named IAM resources.
- Test the public load balancer's first response.
Record the immutable repository identity
The deployment role needs a subject that identifies one repository branch. Immutable owner IDs plus repository IDs keep that identity tied to the same repository if its visible names change.
- Record your GitHub username from the existing repository URL here: YOUR_USERNAME.
- Open the repository identity response by entering this address in your browser:
https://api.github.com/repos/[[GITHUB_USERNAME="YOUR_USERNAME"]]/aws-cloud-engineer-capstone
What Does This Address Return?
- The repository endpoint returns a JSON document for the public repository.
- The top-level id value identifies the repository.
- The top-level name value contains the repository name.
- The owner object contains the owner login plus the owner ID.
You should see the repository document in your browser. The four identity values appear near the beginning of the response.
Repository Response Not Loading?
Confirm that the address contains your GitHub username. Check that the repository name is aws-cloud-engineer-capstone.
A not-found response can mean the username or repository name does not match the public repository. Help me check this repository API address.
- Record the value of owner.login here: your owner login.
- Record the value of owner.id here: 12345678.
- Record the top-level name value here: aws-cloud-engineer-capstone.
- Record the top-level id value here: 123456789.
How Do the Values Fit Together?
- The first OWNER position uses owner.login.
- The OWNER-ID position uses owner.id.
- The REPO position uses the top-level name value.
- The REPO-ID position uses the top-level id value.
- Construct the subject using repo:OWNER@OWNER-ID/REPO@REPO-ID:ref:refs/heads/main as the exact format.
- Record the completed subject here: repo:owner@123/repository@456:ref:refs/heads/main.
- Confirm that the completed subject ends with :ref:refs/heads/main.
Deploy the CloudFormation stack
This deployment starts AWS resources that can accrue charges. Your project stays within its planned ceiling when you delete the stack during cleanup.
Provisioning can take several minutes while CloudFormation connects the networking plus managed services. A quiet terminal during this wait does not mean the deployment has stalled.
- Deploy the existing infra.yml template from the CloudShell project directory by running this command:
aws cloudformation deploy \
--template-file infra.yml \
--stack-name cloud-engineer-capstone \
--parameter-overrides GitHubSubject='[[GITHUB_SUBJECT="repo:owner@123/repository@456:ref:refs/heads/main"]]' \
--capabilities CAPABILITY_NAMED_IAM
What Does This Deployment Create?
- The --template-file option selects the existing infra.yml template.
- The --parameter-overrides option supplies your immutable subject to GitHubSubject.
- The --capabilities option acknowledges the custom-named roles through CAPABILITY_NAMED_IAM.
- Amazon VPC provides the network with two public subnets.
- Amazon Elastic Container Registry (ECR) provides the private image repository.
- Amazon Elastic Container Service (ECS) defines the managed container service.
- AWS Fargate provides the task runtime without an EC2 cluster.
- AWS Identity and Access Management (IAM) provides the task execution role plus the scoped deployment role.
- Amazon CloudWatch provides the log group plus the dashboard plus the healthy-host alarm.
CloudShell confirms when the stack deployment finishes successfully. That is the largest infrastructure jump complete: your AWS platform now exists as one reproducible stack.
Stack Deployment Not Completing?
Confirm that CloudShell is still in the project directory containing infra.yml. Check that your subject follows the immutable repository format exactly.
A permissions failure means the signed-in AWS identity cannot create a resource from the template. Help me troubleshoot this CloudFormation deployment.
Inspect the stack and test the public URL
A completed stack exposes its status plus reusable values through outputs. These outputs identify the public endpoint plus the resources needed by later delivery work.
Before you run this check, which stack status do you expect to prove that provisioning finished?
- Inspect the deployed stack by running this command:
aws cloudformation describe-stacks --stack-name cloud-engineer-capstone
What Does This Command Show?
- The stack description reports the current CloudFormation status.
- The Outputs section exposes values produced by infra.yml.
- The application URL identifies the public load balancer.
- The GitHub role ARN identifies the role used for OIDC deployment access.
- The repository URI identifies the private ECR destination.
- The dashboard name identifies the monitoring dashboard.
You should see CREATE_COMPLETE as the stack status. The outputs should include ApplicationUrl, GitHubRoleArn, RepositoryUri, plus DashboardName.
Finding four values inside the full stack response can be fiddly. Look inside the Outputs section near the end of the response.
- Record the ApplicationUrl output here: http://your-load-balancer-address.
- Record the GitHubRoleArn output here: arn:aws:iam::123456789012:role/cloud-engineer-capstone-github-deploy.
- Record the RepositoryUri output here: 123456789012.dkr.ecr.us-east-1.amazonaws.com/cloud-engineer-capstone.
- Record the DashboardName output here: cloud-engineer-capstone.
Before you open the application URL, do you think the load balancer can return the portfolio page?
- Open a new browser tab.
- Paste http://your-load-balancer-address into the address bar.
- Press Enter to request the portfolio page.
You should see HTTP 503 instead of the portfolio page. This shortfall is the intended result of the first public test.
Why Is This Response Expected?
The load balancer plus the public network are available. The ECS service has DesiredCount: 0, so the target group has no healthy task.
The response proves that requests can reach the platform edge. The delivery workflow must publish the image before it starts the service.
Seeing a Different Browser Result?
Confirm that you copied the value beside ApplicationUrl. Make sure the stack reached CREATE_COMPLETE before testing it.
A browser connection failure can point to an incorrect URL or an incomplete load balancer deployment. Help me diagnose this public endpoint check.
Your public AWS platform is provisioned with its missing workload exposed. Next, GitHub Actions publishes the container so the same URL can display your portfolio page.
Deliver with GitHub Actions
Your public load balancer still returns 503 because the service has no container running behind it.
GitHub Actions closes that delivery gap. A push to main uses OIDC to request short-lived AWS credentials. The workflow then publishes the image to Amazon ECR before starting the Amazon ECS service.
In this step, get ready to:
- Store the deployment role ARN as a GitHub repository variable.
- Publish the project files to main through SSH.
- Watch the workflow start one healthy Fargate task.
Store the deployment role variable
The workflow reads AWS_ROLE_ARN to identify the IAM role it can request. The ARN already exists in your AWS CloudFormation stack outputs.
- Switch back to the cloud-engineer-capstone stack outputs from earlier.
- Record the value beside GitHubRoleArn here: arn:aws:iam::123456789012:role/cloud-engineer-capstone-github-deploy.
- Return to the aws-cloud-engineer-capstone repository from earlier.
- Click Settings below the repository name.
- Select Secrets and variables in the Security section.
- Select Actions.
- Click the Variables tab.
- Click New repository variable.
The repository variable form is ready for the role details.
- Enter AWS_ROLE_ARN in the Name field.
- Paste arn:aws:iam::123456789012:role/cloud-engineer-capstone-github-deploy into the Value field.
- Click Add variable.
Good progress. The workflow now knows which deployment role it may request from AWS.
Why use a repository variable?
A role ARN identifies an AWS resource. It does not grant access by itself.
The workflow reads it through ${{ vars.AWS_ROLE_ARN }}. OIDC supplies temporary credentials only after AWS validates the immutable repository subject.
Variable not listed?
Check that you created a repository variable under the Variables tab. A variable created under another scope will not appear here.
Confirm the name is exactly AWS_ROLE_ARN. Help me troubleshoot why my GitHub Actions repository variable is unavailable to the workflow.
Publish the project to main
A Git commit gives the workflow a precise version of your application. The SSH remote connects that history to the empty repository you prepared earlier.
- Initialize main in the CloudShell project directory from earlier. Set the commit identity before creating the first commit by running these commands:
git init -b main
git config user.name "[[GIT_AUTHOR_NAME="your Git author name"]]"
git config user.email "[[GIT_AUTHOR_EMAIL="your Git commit email"]]"
git add .
git commit -m "First commit"
What do these commands do?
- The first command creates a repository with main as its initial branch.
- The next two commands attach your author name and email to commits in this repository.
- The git add . command stages the project files.
- The final command records those staged files with the message First commit.
- Confirm the terminal reports a new commit on main with the project files included.
Commit not created?
Check that your CloudShell prompt is inside the directory containing index.html and infra.yml.
Confirm both author values contain your information. Help me diagnose why the first Git commit failed in AWS CloudShell.
- Return to the empty repository page in GitHub.
- Select SSH in the Quick setup area.
- Record the displayed repository URL here: git@github.com:your-username/aws-cloud-engineer-capstone.git.
- Connect the local repository to origin by running these commands:
git remote add origin [[YOUR_REPO_URL="git@github.com:your-username/aws-cloud-engineer-capstone.git"]]
git remote -v
What do these commands do?
- The first command associates the name origin with your repository's SSH URL.
- The second command displays the fetch and push destinations for that remote.
- Confirm the terminal lists your SSH URL once for fetching and once for pushing.
Remote not configured?
Check that the URL begins with git@github.com: and ends with aws-cloud-engineer-capstone.git.
Confirm you copied the SSH URL from your own repository. Help me fix the origin remote for my GitHub repository.
The deployment workflow already exists in .github/workflows/deploy.yml. Use this comparison before you publish the commit.
✔️ Awesome, I've got everything!
Great. Confirm that .github/workflows/deploy.yml is saved with the rest of the project.
ⓧ I'd like to double check the full code
name: Deploy to Amazon ECS
on:
push:
branches:
- main
workflow_dispatch:
permissions:
id-token: write
contents: read
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: cloud-engineer-capstone
ECS_CLUSTER: cloud-engineer-capstone
ECS_SERVICE: cloud-engineer-capstone
IMAGE_TAG: latest
jobs:
deploy:
runs-on: ubuntu-24.04
steps:
- name: Check out repository
uses: actions/checkout@v7.0.1
- name: Configure short-lived AWS credentials
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
role-session-name: ${{ github.run_id }}
aws-region: ${{ env.AWS_REGION }}
- name: Build and push image
shell: bash
run: |
set -euo pipefail
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGISTRY="$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$REGISTRY"
docker build -t "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
docker push "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: Deploy image to ECS
shell: bash
run: |
set -euo pipefail
aws ecs update-service \
--cluster "$ECS_CLUSTER" \
--service "$ECS_SERVICE" \
--desired-count 1 \
--force-new-deployment
aws ecs wait services-stable \
--cluster "$ECS_CLUSTER" \
--services "$ECS_SERVICE"
aws cloudformation describe-stacks \
--stack-name cloud-engineer-capstone \
--query 'Stacks[0].Outputs[?OutputKey==`ApplicationUrl`].OutputValue' \
--output text
How does this workflow close the gap?
- The permissions allow the workflow to request an OIDC token and read the repository.
- The credentials action exchanges that token for short-lived AWS credentials tied to AWS_ROLE_ARN.
- The build step pushes the latest image to the cloud-engineer-capstone ECR repository.
- The deployment step changes the service's desired count to one.
- The stability waiter keeps the run active until ECS reaches a stable deployment.
Before you push, what do you expect the load balancer to show when the workflow finishes? Your prediction is the benchmark for the final check.
- Publish main through the configured SSH remote by running this command:
git push -u origin main
What does this command do?
The command publishes your local main branch to origin. The push event starts the deployment workflow.
The terminal confirms that main now tracks its remote branch. Your repository page now contains the five project files.
Push not completing?
Confirm the terminal is using the SSH remote shown by git remote -v.
If your SSH key has a passphrase, enter it when the terminal prompts you. Help me troubleshoot why main is not pushing from CloudShell.
Verify the live deployment
The push starts the workflow that replaces the empty service with a running container. Expect the run to take a few minutes while it builds the image and waits for ECS stability.
- Return to your aws-cloud-engineer-capstone repository.
- Click Actions in the repository navigation.
- Select the workflow run created by the First commit push.
- Select the deploy job.
- Wait for the job to show a green success status.
That is the delivery chain working. GitHub obtained temporary credentials before it built the image and started the AWS service.
What just deployed?
- GitHub validated the repository identity through the immutable OIDC subject.
- AWS issued short-lived credentials for the scoped deployment role.
- The runner built the image from Dockerfile and index.html.
- The workflow pushed the image to ECR with the latest tag.
- ECS changed DesiredCount from zero to one before forcing a fresh deployment.
Before you check ECS, do you expect the service to return immediately or keep waiting? The command tests whether the deployment has reached a stable state.
- Switch back to the AWS CloudShell session from earlier.
- Wait for the ECS service to become stable by running this command:
aws ecs wait services-stable --cluster cloud-engineer-capstone --services cloud-engineer-capstone
What does this command prove?
The waiter checks the cloud-engineer-capstone service until its running task count matches its desired count. It also checks that only one deployment remains active.
The command returns control to the CloudShell prompt without an error. ECS now has one stable Fargate task.
Before you refresh, do you expect the previous 503 response or your custom portfolio page? The public endpoint gives you the answer.
- Return to the ApplicationUrl from the stack outputs you used earlier.
- Refresh the page in your browser.
You will see the AWS Cloud Engineer Capstone page with Service status: healthy at the top. The load balancer now has a healthy container target instead of an empty service.
Still seeing the 503 response?
Confirm the GitHub Actions run completed successfully. A failed image push or ECS deployment leaves the service without a healthy target.
Wait for the ECS stability command to return before refreshing the page again. Help me diagnose why my ApplicationUrl still returns 503 after the GitHub Actions deployment.
Your push now travels from GitHub to ECR before reaching a healthy ECS task. Next, you will connect that live service to logs, metrics, an alarm, and portfolio evidence.
Monitor the Service and Capture Evidence
Your container platform now serves the portfolio page from a healthy Fargate task. The next goal is to prove what happens behind each request.
A live endpoint alone does not prove operational skill. You need evidence that connects deployment behavior to service health.
Amazon CloudWatch records requests handled by Amazon ECS. Its dashboard connects that activity to Application Load Balancer health.
In this step, get ready to:
- Trace browser requests into NGINX access logs.
- Confirm service health through the CloudWatch dashboard.
- Publish monitoring evidence in README.md.
Generate requests and inspect the logs
The NGINX container writes an access record when it handles a request. Refreshing the portfolio page gives you fresh records to trace.
- Return to the browser tab showing the ApplicationUrl from earlier.
- Refresh the portfolio page several times.
- Switch back to the AWS CloudShell session from earlier.
Before you inspect the logs, predict whether each browser refresh produced a separate access record.
- Display recent container access records by running this command:
aws logs tail /ecs/cloud-engineer-capstone --since 10m --format short
What does this command show?
- The /ecs/cloud-engineer-capstone log group receives output from the running ECS task.
- The --since 10m option limits the result to recent activity.
- The --format short option keeps each record compact enough to scan.
You will see recent NGINX access records for the requests you generated. Each record proves that the running task handled traffic from the load balancer.
No recent access records?
- Refresh the live ApplicationUrl again.
- Confirm that the command targets /ecs/cloud-engineer-capstone.
If the log group stays empty, help me trace an Application Load Balancer request into my ECS NGINX logs.
Read the dashboard and alarm
A monitoring dashboard brings service activity into one view. The alarm converts healthy-target data into a clear operational state.
- Return to the AWS Management Console from earlier.
- Use the console search bar to open CloudWatch.
- Select Dashboards in the CloudWatch navigation pane.
- Open the cloud-engineer-capstone dashboard.
- Locate the chart showing ECS CPU utilization.
- Locate the line showing ECS memory utilization.
- Locate the chart showing Application Load Balancer request count.
- Locate the line showing healthy-host count.
The request-count chart connects your browser refreshes to load balancer traffic. The healthy-host line confirms that one target is available behind it.
- Copy the dashboard page URL into your CloudWatch dashboard URL.
- Select All alarms in the CloudWatch navigation pane.
- Open the cloud-engineer-capstone-health alarm.
This check can feel slow. The alarm evaluates one-minute periods, so allow a full period before refreshing its state.
- Refresh the alarm view after one evaluation period.
- Confirm that its current state is OK.
Good work. Your monitoring now shows both sides of a healthy service: incoming requests and an available target.
Alarm still missing a healthy state?
- Confirm that the portfolio page still loads through the ApplicationUrl.
- Check that the dashboard reports at least one healthy host.
- Refresh the alarm after another evaluation period.
If the state does not recover, help me compare my healthy-host metric with the cloud-engineer-capstone-health alarm.
Document and publish the evidence
The repository already explains the architecture and its trade-offs. Your evidence links now connect those claims to a working deployment.
- Return to the browser tab showing the live portfolio page.
- Copy its address into your ApplicationUrl.
- Return to the public GitHub repository from earlier.
- Open the Actions tab.
- Select the successful Deploy to Amazon ECS run from the previous step.
- Copy the run page address into your successful workflow URL.
The three URLs point to the live application, the successful delivery run, and the monitoring dashboard. Together they form an evidence trail from source control to operations.
- Switch back to the AWS CloudShell session from earlier.
- Open README.md from the aws-cloud-engineer-capstone folder in the CloudShell file editor.
- Replace the live demonstration placeholder with your ApplicationUrl.
- Replace the successful workflow placeholder with your successful workflow URL.
- Replace the live application evidence placeholder with your ApplicationUrl.
- Replace the CloudWatch dashboard placeholder with your CloudWatch dashboard URL.
- Keep the existing ECS-on-Fargate trade-off explanation under Why ECS on Fargate.
- Save README.md.
✔️ Awesome, I've got everything!
Your README.md now connects the architecture description to deployment and monitoring evidence.
ⓧ I'd like to double check the full code
Compare the updated README.md against this final evidence checklist.
- The live demonstration section contains the real ApplicationUrl.
- The evidence section links to the successful GitHub Actions run.
- The evidence section links to the live application.
- The evidence section links to the CloudWatch dashboard.
- The ECS-on-Fargate explanation still compares ECS with EKS and App Runner.
- The outage note placeholder remains available for the optional incident drill.
- Publish the README evidence to main by running these commands:
git add .
git commit -m "Update README"
git push -u origin main
What do these commands do?
- The git add . command stages the saved evidence update.
- The git commit -m "Update README" command records the updated portfolio documentation.
- The git push -u origin main command publishes the commit to the repository's main branch.
The push starts another deployment workflow because the repository runs it for changes to main.
- Return to the repository's Actions tab.
- Open the newest Deploy to Amazon ECS run.
- Wait for the run to show a successful result.
- Open the repository's rendered README.md.
You will see the live application link and the two evidence links in the rendered README. The infrastructure explanation remains directly below the security decisions.
Evidence push not completing?
- Confirm that README.md was saved before you staged the change.
- Check that the terminal is inside the aws-cloud-engineer-capstone folder.
- Confirm that the SSH remote still points to your public repository.
If the push or workflow fails, help me troubleshoot my README evidence update and GitHub Actions run.
Before the final check, predict whether a new page request will appear in the logs while the healthy-host alarm remains OK.
- Return to your ApplicationUrl.
- Refresh the portfolio page once.
- Switch back to the AWS CloudShell session.
- Check the latest access records by running this command:
aws logs tail /ecs/cloud-engineer-capstone --since 10m --format short
What proves the request was handled?
A recent NGINX access record shows that the running ECS task handled the request you just sent through the load balancer.
You will see a recent access record from the final refresh. This confirms that logs continue to capture live traffic.
- Return to the cloud-engineer-capstone-health alarm in CloudWatch.
- Confirm that the alarm state remains OK.
- Return to the rendered README.md in GitHub.
- Confirm that it shows the real application URL.
- Confirm that it includes the successful workflow evidence.
- Confirm that it includes the CloudWatch dashboard evidence.
You have turned a running endpoint into documented operational evidence. Your portfolio now proves delivery, observability, and service health from one public repository.
Secret mission
Run a Controlled Service Outage
Create a controlled outage in the live container platform. Diagnose it through the public endpoint and monitoring signals. Recover the service before documenting the incident in README.md.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your live platform available, pause its compute, or delete its resources completely. This project uses paid AWS resources, so leaving the stack running can keep accruing charges.
Cost warning
The AWS Paid plan can bill beyond credits or a configured limit depending on your account settings.
The Application Load Balancer can accrue charges while the stack exists. Public IPv4 addresses can also accrue hourly charges.
The AWS Fargate task accrues usage charges while it runs. Amazon CloudWatch resources can incur usage-based charges.
Choose the pause option to stop the Fargate task. Choose the delete option to remove the cost-bearing AWS stack resources.
Resources you used:
- The AWS CloudFormation stack named cloud-engineer-capstone.
- The networking layer containing an Amazon VPC, an internet gateway, two public subnets, routing resources, and two security groups.
- The container delivery layer containing the load balancer, target group, listener, Amazon ECR repository, Amazon ECS cluster, task definition, service, and Fargate task.
- The identity layer containing the GitHub OIDC provider plus two IAM roles.
- The monitoring layer containing the /ecs/cloud-engineer-capstone log group, the cloud-engineer-capstone dashboard, and the cloud-engineer-capstone-health alarm.
- The AWS CloudShell source folder at ~/aws-cloud-engineer-capstone.
Keep everything running
No action is needed while you want the hiring-manager URL to stay live. Choose this option only if that availability is worth the continuing AWS charges.
- Leave the cloud-engineer-capstone CloudFormation stack running.
- Keep the ECS service desired count at 1.
- Review your AWS billing regularly while the public application remains available.
- Keep the public aws-cloud-engineer-capstone GitHub repository as your permanent portfolio evidence.
Pause - I'll come back to this later
Pausing stops the Fargate task while preserving the stack for your return. The load balancer plus public IPv4 addresses can still accrue charges.
- Return to the Amazon ECS service named cloud-engineer-capstone in us-east-1.
- Use the service update control to set the desired count to 0.
- Wait until the service reports no running task.
The public application now returns HTTP 503 because the load balancer has no healthy target. Your infrastructure configuration remains available for a later restart.
- Leave the cloud-engineer-capstone CloudFormation stack in place.
- Keep the ~/aws-cloud-engineer-capstone source folder in CloudShell.
- Keep the public GitHub repository as your portfolio artifact.
Delete - I don't want to use this again
Complete removal deletes the AWS resources created by the stack. Your public GitHub repository stays available as portfolio evidence.
Deleting the stack is the irreversible part, so make sure your screenshots plus README evidence are already in GitHub. The repository itself is unaffected by this command.
- Delete the cloud-engineer-capstone stack from the CloudShell session in us-east-1 by running this command:
aws cloudformation delete-stack --stack-name cloud-engineer-capstone
This starts deletion of the complete stack. Its EmptyOnDelete: true setting also removes the stored ECR image.
Stack Deletion Not Starting?
- Confirm that your CloudShell session is still using us-east-1.
- Confirm that cloud-engineer-capstone still appears in your CloudFormation stack list.
- Help me diagnose the failed stack deletion.
Stack deletion can take several minutes while CloudFormation removes dependent resources.
- Wait for CloudFormation to finish deleting the stack.
- Return to the CloudFormation stack list in us-east-1 to confirm that cloud-engineer-capstone is no longer listed.
That is the cost-critical cleanup done. The deleted AWS resources can no longer accrue charges.
Your public GitHub repository already holds the project files. Removing the CloudShell copy leaves that portfolio evidence available.
- Remove the local source folder from CloudShell by running this command:
rm -rf ~/aws-cloud-engineer-capstone
This removes only the project folder from your CloudShell home directory. The public GitHub repository remains available.
Nice Work!
Nice Work!
You did it! Your portfolio page now runs publicly on Amazon ECS with AWS Fargate behind an Application Load Balancer.
You've learned how to:
- Build a custom NGINX container in AWS CloudShell. Test the portfolio page locally. Deploy the reproducible platform with AWS CloudFormation.
- Create a GitHub Actions pipeline that uses GitHub OIDC to obtain short-lived AWS credentials. Push the latest image to Amazon ECR. Release it to the ECS service without storing AWS access keys in the repository.
- Trace live requests through Amazon CloudWatch logs. Verify platform health through the dashboard. Prove recovery through the healthy-host alarm. Preserve the evidence in README.md.
- Secret Mission: Run a controlled service outage. Use the HTTP 503 response to identify the public symptom. Use the CloudWatch alarm to identify the missing healthy target. Restore the ECS service to one stable task.
Ready to quiz yourself?