Five console-plus-CLI exercises that walk through the exact build/deploy/observe patterns the DVA-C02 exam expects you to have handled yourself at least once. Each exercise chains into the next — the ECS service you stand up in Exercise 1 gets a CI/CD pipeline in Exercise 2; the Lambda function you build in Exercise 4 gets X-Ray tracing in Exercise 5 — so working through them in order mirrors how these services actually compose in a real account.
eu-west-1 (or pass --region eu-west-1 explicitly on every command, as this guide does).curl available, for Exercises 3, 4, and 5.Use an IAM identity (console user or CLI profile) with broad permissions across ECR, ECS, EC2 (for the ALB/VPC), IAM (role creation), CodePipeline, CodeBuild, CodeDeploy, CloudFormation, Lambda, API Gateway, DynamoDB, Cognito, and X-Ray. This lab is written for a sandbox/training account — in production you'd scope each of these down to least privilege, which is itself a recurring exam theme.
DVA-C02 tests scenario judgment — "which service, which flag, which IAM role" — and that judgment sharpens fastest when you've actually typed the commands and watched them fail for the reason the exam is testing (wrong network mode, wrong IAM role, wrong token type). Read the matching deep-dive guide for any exercise where a step surprises you.
| Exercise | What you build | Core services | Deep-dive guide |
|---|---|---|---|
| 1 | Containerized web app on Fargate behind an ALB | ECR, ECS, Fargate, ALB, IAM | 01 — Containers |
| 2 | CI/CD pipeline with blue/green deploys to the Exercise 1 service | CodePipeline, CodeBuild, CodeDeploy | 02 — CI/CD |
| 3 | Serverless API + Lambda + DynamoDB via SAM | SAM, CloudFormation, Lambda, API Gateway, DynamoDB | 03 — IaC |
| 4 | HTTP API + Lambda protected by a Cognito JWT authorizer | API Gateway (HTTP API), Lambda, Cognito | 06 — API Gateway & 07 — Cognito |
| 5 | X-Ray Active Tracing on the Exercise 4 Lambda, read the service map | X-Ray, Lambda, IAM | 05 — X-Ray |
Every step is numbered with its expected outcome so you can tell immediately if something went wrong before moving on. Placeholder values like <account-id>, subnet-0123abcd, and eu-west-1_XXXXXXXXX stand in for the real IDs your console will show you — copy those real values into the CLI commands as you go.
Build a container image, push it to a private ECR repository, and run it as an ECS service on Fargate behind an internet-facing Application Load Balancer. This is the foundation the CI/CD pipeline in Exercise 2 deploys into.
Dockerfile for a simple web app (e.g. an nginx-based static page or a small Node/Python HTTP server listening on port 80).
A working Dockerfile that builds cleanly and exposes port 80.
dva-lab-web, region eu-west-1. CLI equivalent:
aws ecr create-repository \ --repository-name dva-lab-web \ --image-scanning-configuration scanOnPush=true \ --region eu-west-1Response includes a
repositoryUri like <account-id>.dkr.ecr.eu-west-1.amazonaws.com/dva-lab-web.
docker login.
aws ecr get-login-password --region eu-west-1 \ | docker login --username AWS --password-stdin <account-id>.dkr.ecr.eu-west-1.amazonaws.com
Login Succeeded.
docker build -t dva-lab-web:latest .Image appears in
docker images.
docker tag dva-lab-web:latest \ <account-id>.dkr.ecr.eu-west-1.amazonaws.com/dva-lab-web:latest
docker push <account-id>.dkr.ecr.eu-west-1.amazonaws.com/dva-lab-web:latestPush completes; the image tag
latest is now visible under Images in the ECR console.
The get-login-password | docker login pattern is the only supported way to authenticate Docker to ECR — the exam will present distractors involving IAM access keys pasted into Docker config or long-lived passwords, both wrong. The token from get-login-password is valid for 12 hours, which itself sometimes shows up as a quiz distractor value.
dva-lab-ecsTaskExecutionRole) with the AWS managed policy AmazonECSTaskExecutionRolePolicy attached and a trust policy for ecs-tasks.amazonaws.com, and an ECS task role (dva-lab-ecsTaskRole) with the same trust policy but no permissions attached yet (add app-specific permissions here later, e.g. S3/DynamoDB access).
Two distinct role ARNs, both trusted by ecs-tasks.amazonaws.com.
The execution role is used by the ECS agent itself before your container code runs — to pull the image from ECR and write logs to CloudWatch Logs. The task role is assumed by your application code inside the running container to call other AWS services (S3, DynamoDB, etc.). Granting your app's DynamoDB permissions to the execution role instead of the task role is a textbook wrong-answer pattern on this exam.
{
"family": "dva-lab-task",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::<account-id>:role/dva-lab-ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::<account-id>:role/dva-lab-ecsTaskRole",
"containerDefinitions": [
{
"name": "dva-lab-web",
"image": "<account-id>.dkr.ecr.eu-west-1.amazonaws.com/dva-lab-web:latest",
"portMappings": [
{ "containerPort": 80, "protocol": "tcp" }
],
"essential": true,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/dva-lab-web",
"awslogs-region": "eu-west-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}
aws logs create-log-group --log-group-name /ecs/dva-lab-web --region eu-west-1 aws ecs register-task-definition \ --cli-input-json file://task-definition.json \ --region eu-west-1Response returns a
taskDefinitionArn ending in dva-lab-task:1.
awsvpc network mode is non-negotiable here
Fargate tasks only support awsvpc networking — each task gets its own elastic network interface, private IP, and security group, unlike the bridge mode available on EC2 launch type. This is also why the ALB target type must be ip rather than instance for Fargate services: there's no EC2 instance ID to register, only the task's ENI IP address.
dva-lab-cluster, infrastructure AWS Fargate.
Cluster shows status ACTIVE with 0 running tasks.
Active, with a public DNS name.
/, and attach it to a listener on the ALB (port 80).
Target group shows 0 registered targets — the service creation step below will populate it.
0.0.0.0/0) — the ALB's own security group allows inbound 80/443 from the internet.
Two security groups: one open to the internet on the ALB, one that only trusts the ALB on the tasks.
dva-lab-task → desired tasks 2 → attach to the ALB target group. CLI equivalent:
aws ecs create-service \
--cluster dva-lab-cluster \
--service-name dva-lab-service \
--task-definition dva-lab-task \
--desired-count 2 \
--launch-type FARGATE \
--platform-version LATEST \
--network-configuration "awsvpcConfiguration={subnets=[subnet-0123abcd,subnet-0456efgh],securityGroups=[sg-0789ijkl],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:eu-west-1:<account-id>:targetgroup/dva-lab-tg/abc123,containerName=dva-lab-web,containerPort=80" \
--region eu-west-1
Service reaches steadyState with 2/2 tasks RUNNING, and both show healthy in the target group.
curl.
curl -I http://dva-lab-alb-1234567890.eu-west-1.elb.amazonaws.com
HTTP/1.1 200 OK served by your container.
assignPublicIp=DISABLED here
Tasks sit in private subnets and only need to be reachable from the ALB, which lives in public subnets — this is the standard pattern the exam expects for "internet-facing app, private compute." If your subnets have no NAT gateway, the tasks still need outbound access to pull the image from ECR and reach the ECS APIs; that's normally satisfied via VPC endpoints for ECR/S3/CloudWatch Logs or a NAT gateway, another exam-favorite networking distinction.
Deep dive: 01 — Containers: ECS, ECR, Fargate, EKS Study Guide.
Wire a pipeline that builds a new image on every source change, pushes it to ECR, and shifts production traffic to it using CodeDeploy's blue/green deployment against the ECS service from Exercise 1.
AWS stopped onboarding new customers to CodeCommit in July 2024, and CodeStar was deprecated even earlier. The exam may still describe legacy environments that use them, but the current real-world (and increasingly exam-current) pattern is a third-party repository — GitHub, Bitbucket, or GitLab — connected via AWS CodeConnections (formerly CodeStar Connections) as the pipeline's source action. This exercise uses a GitHub repository through a CodeConnections connection.
Dockerfile and app code to a new GitHub repository.
Repository exists with at least a main branch containing the Dockerfile.
Available, with a connection ARN you'll reference in the pipeline's source stage.
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com
- IMAGE_TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION:0:7}
build:
commands:
- echo Build started on `date`
- docker build -t $REPOSITORY_URI:$IMAGE_TAG .
- docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:latest
post_build:
commands:
- echo Build completed on `date`
- docker push $REPOSITORY_URI:$IMAGE_TAG
- docker push $REPOSITORY_URI:latest
- echo Writing image definitions for CodeDeploy...
- printf '[{"name":"dva-lab-web","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
artifacts:
files:
- imageDetail.json
- appspec.yml
- taskdef.json
aws/codebuild/amazonlinux2-x86_64-standard:5.0 → enable "Privileged" mode (required to run docker build inside the build container) → environment variables AWS_ACCOUNT_ID and REPOSITORY_URI → buildspec: use the file above.
A CodeBuild project that, run standalone, successfully builds and pushes an image and produces imageDetail.json as an artifact.
ecr:GetAuthorizationToken, ecr:BatchCheckLayerAvailability, ecr:PutImage, ecr:InitiateLayerUpload, ecr:UploadLayerPart, and ecr:CompleteLayerUpload on the dva-lab-web repository, plus permissions to write to the pipeline's artifact S3 bucket and CloudWatch Logs.
A manual "Start build" from the console succeeds end-to-end.
CodeBuild's build environment runs as a container itself. Building a Docker image inside a container (Docker-in-Docker) requires elevated access to the build host's Docker daemon, which CodeBuild only grants when Privileged mode is enabled. Forgetting this is the single most common reason a working buildspec.yml fails only inside CodeBuild — a favorite "why did this fail" exam scenario.
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: "dva-lab-web"
ContainerPort: 80
dva-lab-tg-green) on the same ALB, and a second listener (e.g. port 8080) for CodeDeploy's test traffic — CodeDeploy blue/green for ECS needs two target groups and two listeners (production + test) even if you don't manually use the test one.
ALB now has target groups dva-lab-tg ("blue") and dva-lab-tg-green, and listeners on 80 and 8080.
dva-lab-app → compute platform Amazon ECS. CLI equivalent:
aws deploy create-application \ --application-name dva-lab-app \ --compute-platform ECS \ --region eu-west-1
aws deploy create-deployment-group \
--application-name dva-lab-app \
--deployment-group-name dva-lab-dg \
--deployment-config-name CodeDeployDefault.ECSAllAtOnce \
--service-role-arn arn:aws:iam::<account-id>:role/dva-lab-CodeDeployECSRole \
--ecs-services clusterName=dva-lab-cluster,serviceName=dva-lab-service \
--load-balancer-info targetGroupPairInfoList="[{targetGroups=[{name=dva-lab-tg},{name=dva-lab-tg-green}],prodTrafficRoute={listenerArns=[arn:aws:elasticloadbalancing:eu-west-1:<account-id>:listener/app/dva-lab-alb/abc/def]},testTrafficRoute={listenerArns=[arn:aws:elasticloadbalancing:eu-west-1:<account-id>:listener/app/dva-lab-alb/abc/ghi]}}]" \
--region eu-west-1
Deployment group created, status Ready.
CodeDeploy registers the new task set against the green target group, runs health checks, optionally routes a slice of traffic through the test listener, then shifts the ALB's production listener to point at green either all-at-once or on a linear/canary schedule you define in the deployment config — with automatic rollback to blue if a CloudWatch alarm you attach to the deployment group fires during the bake time.
aws codepipeline create-pipeline \ --cli-input-json file://pipeline.json \ --region eu-west-1
main branch of the GitHub repository.
The pipeline triggers automatically via the CodeConnections webhook (not polling), moves through Source → Build → Deploy, and the ALB begins serving the new image once the blue/green shift completes.
Every CodeDeploy compute platform uses an appspec.yml, but the shape is platform-specific: EC2/on-premises appspecs define hooks and file copy locations; Lambda appspecs point at a function version/alias; ECS appspecs (shown above) reference a task definition and the container/port that receives ALB traffic. Picking the EC2-shaped appspec for an ECS deployment is a common wrong-answer distractor.
Deep dive: 02 — CI/CD: CodePipeline, CodeBuild, CodeDeploy, CodeCommit Study Guide.
Scaffold, build, and deploy a minimal API Gateway + Lambda + DynamoDB application using the SAM CLI, entirely from a declarative template.yaml.
sam init \ --runtime python3.12 \ --dependency-manager pip \ --app-template hello-world \ --name dva-lab-samA new
dva-lab-sam/ directory containing template.yaml, a hello_world/ function folder, and events/ test payloads.
hello_world/ folder with a notes/ folder containing app.py and requirements.txt, implementing a handler that reads/writes items in a DynamoDB table using the table name from an environment variable.
A working handler with lambda_handler(event, context) that returns a JSON API Gateway proxy response.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: DVA-C02 lab - minimal serverless notes API
Globals:
Function:
Runtime: python3.12
Timeout: 10
MemorySize: 128
Resources:
NotesTable:
Type: AWS::Serverless::SimpleTable
Properties:
TableName: dva-lab-notes
PrimaryKey:
Name: noteId
Type: String
NotesFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: notes/
Handler: app.lambda_handler
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref NotesTable
Environment:
Variables:
TABLE_NAME: !Ref NotesTable
Events:
GetNote:
Type: Api
Properties:
Path: /notes/{noteId}
Method: get
PutNote:
Type: Api
Properties:
Path: /notes
Method: post
Outputs:
ApiUrl:
Description: "Invoke URL for the notes API"
Value: !Sub "https://${ServerlessRestApi}.execute-api.eu-west-1.amazonaws.com/Prod/notes"
Transform: AWS::Serverless-2016-10-31 tells CloudFormation to expand this short-hand template into full AWS::ApiGateway::RestApi, AWS::Lambda::Function, AWS::Lambda::Permission, and AWS::DynamoDB::Table resources at deploy time. AWS::Serverless::Function with an Api event source both creates the REST API resource/method and wires the Lambda invoke permission automatically — doing this in raw CloudFormation takes noticeably more resource declarations. DynamoDBCrudPolicy is a SAM policy template that generates a scoped IAM policy for exactly the table referenced, rather than you hand-writing an IAM policy document.
sam buildA
.aws-sam/build/ directory containing the packaged function and a transformed template.
sam deploy --guidedPrompts include:
Stack Name [sam-app]: → dva-lab-sam; AWS Region [us-east-1]: → eu-west-1; Confirm changes before deploy [y/N]: → y; Allow SAM CLI IAM role creation [Y/n]: → Y (SAM needs this to create the Lambda execution role); Save arguments to configuration file [Y/n]: → Y, written to samconfig.toml so future deploys can just run sam deploy.
ApiUrl output printed at the end of the deploy, or retrieve it later.
aws cloudformation describe-stacks \ --stack-name dva-lab-sam \ --query "Stacks[0].Outputs" \ --region eu-west-1
curl -X POST https://abc123xyz.execute-api.eu-west-1.amazonaws.com/Prod/notes \
-H "Content-Type: application/json" \
-d '{"noteId":"1","text":"hello from SAM"}'
curl https://abc123xyz.execute-api.eu-west-1.amazonaws.com/Prod/notes/1
POST returns 200/201; GET returns the note JSON, confirming the Lambda successfully wrote to and read from DynamoDB using the task-scoped IAM policy.
SAM deploys through CloudFormation using CAPABILITY_IAM — a capability acknowledgment CloudFormation requires before it will create IAM resources on your behalf. If a pipeline (rather than an interactive human) runs sam deploy, this becomes --capabilities CAPABILITY_IAM as an explicit flag rather than an interactive prompt. Forgetting the capability flag in automation is a classic "why did my deploy fail with an IAM capability error" exam and real-world scenario.
Deep dive: 03 — IaC: CloudFormation, SAM, CDK Study Guide.
Stand up a Cognito User Pool, front a Lambda function with an HTTP API, protect a route with a JWT authorizer backed by that user pool, then obtain and use a real JWT to call the API.
dva-lab-pool. CLI equivalent:
aws cognito-idp create-user-pool \ --pool-name dva-lab-pool \ --auto-verified-attributes email \ --region eu-west-1Response includes a
UserPool.Id like eu-west-1_XXXXXXXXX.
USER_PASSWORD_AUTH flow used from the CLI/curl below) and explicit auth flows enabled.
aws cognito-idp create-user-pool-client \ --user-pool-id eu-west-1_XXXXXXXXX \ --client-name dva-lab-client \ --no-generate-secret \ --explicit-auth-flows ALLOW_USER_PASSWORD_AUTH ALLOW_REFRESH_TOKEN_AUTH \ --region eu-west-1Response includes a
ClientId you'll use as the JWT audience and for sign-in.
aws cognito-idp admin-create-user \ --user-pool-id eu-west-1_XXXXXXXXX \ --username testuser@example.com \ --user-attributes Name=email,Value=testuser@example.com Name=email_verified,Value=true \ --message-action SUPPRESS \ --region eu-west-1 aws cognito-idp admin-set-user-password \ --user-pool-id eu-west-1_XXXXXXXXX \ --username testuser@example.com \ --password 'TempPass123!' \ --permanent \ --region eu-west-1User status
CONFIRMED in the console's Users tab.
dva-lab-hello (Python or Node) that returns a 200 response with a JSON body echoing the caller's identity claims from the event.
Function invokes successfully with a test event in the console.
dva-lab-hello → configure route GET /hello → deploy to a stage (e.g. $default).
An invoke URL like https://abc123xyz.execute-api.eu-west-1.amazonaws.com/hello returns 200 with no authorization applied yet.
$request.header.Authorization → Issuer URL https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_XXXXXXXXX → Audience: the app client ID from Part A. CLI equivalent:
aws apigatewayv2 create-authorizer \ --api-id abc123xyz \ --authorizer-type JWT \ --identity-source '$request.header.Authorization' \ --name dva-lab-cognito-authorizer \ --jwt-configuration Audience=1a2b3c4d5e6f7g8h9i0j,Issuer=https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_XXXXXXXXX \ --region eu-west-1Response returns an
AuthorizerId.
GET /hello route.
aws apigatewayv2 update-route \ --api-id abc123xyz \ --route-id def456ghi \ --authorization-type JWT \ --authorizer-id jkl789mno \ --region eu-west-1The route now shows
Authorization: JWT in the console.
curl -i https://abc123xyz.execute-api.eu-west-1.amazonaws.com/hello
HTTP/1.1 401 Unauthorized — the Lambda is never invoked.
aws cognito-idp initiate-auth \ --auth-flow USER_PASSWORD_AUTH \ --client-id 1a2b3c4d5e6f7g8h9i0j \ --auth-parameters USERNAME=testuser@example.com,PASSWORD='TempPass123!' \ --region eu-west-1Response JSON includes
AuthenticationResult.IdToken, AccessToken, and RefreshToken.
TOKEN=$(aws cognito-idp initiate-auth \ --auth-flow USER_PASSWORD_AUTH \ --client-id 1a2b3c4d5e6f7g8h9i0j \ --auth-parameters USERNAME=testuser@example.com,PASSWORD='TempPass123!' \ --region eu-west-1 \ --query 'AuthenticationResult.IdToken' --output text) curl -i https://abc123xyz.execute-api.eu-west-1.amazonaws.com/hello \ -H "Authorization: Bearer $TOKEN"
HTTP/1.1 200 OK with the Lambda's JSON response, including the JWT claims API Gateway forwarded in the event.
Because the authorizer above was configured with Audience set to the app client ID, it validates the ID token, whose aud claim contains the client ID. Cognito access tokens don't carry an aud claim at all — they carry client_id instead — so an access token will fail validation against an authorizer configured this way. Which token type an authorizer expects, and why, is one of the most commonly missed distinctions on this exam.
Deep dive: 06 — API Gateway Study Guide and 07 — Cognito Identity Study Guide.
Turn on Active Tracing for the dva-lab-hello function, grant the execution role permission to emit trace data, generate some traffic, and read the resulting service map and individual traces in the X-Ray console.
dva-lab-hello → Configuration → Monitoring and operations tools → Edit → toggle AWS X-Ray Active tracing on. CLI equivalent:
aws lambda update-function-configuration \ --function-name dva-lab-hello \ --tracing-config Mode=Active \ --region eu-west-1
TracingConfig.Mode in the response shows Active. The function's default sampling behavior (roughly the standard 1 request/second reservoir plus 5% of additional requests) now applies.
AWSXRayDaemonWriteAccess covers xray:PutTraceSegments and xray:PutTelemetryRecords.
aws iam attach-role-policy \ --role-name dva-lab-hello-role \ --policy-arn arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccessPolicy appears under the role's permissions in IAM.
Turning on Active Tracing changes how Lambda instruments the invocation, but the function still needs xray:PutTraceSegments/xray:PutTelemetryRecords permission to actually send that data to X-Ray. Without it, the toggle is on, the function runs fine, and traces simply never appear — no error is thrown, which makes this a favorite "why isn't X-Ray showing anything" exam and real-world scenario.
from aws_xray_sdk.core import patch_all, xray_recorder
patch_all() # instruments boto3, requests, and other supported libraries
import boto3
dynamodb = boto3.resource("dynamodb")
const AWSXRay = require('aws-xray-sdk-core');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
const dynamodb = new AWS.DynamoDB.DocumentClient();
Once traffic flows, each downstream call shows as a labeled subsegment (e.g. "DynamoDB") nested inside the Lambda segment, with its own duration.
The Lambda console toggle alone gives you the function's own segment — total invocation duration, cold start, and whether the invocation errored or faulted. It does not automatically break out time spent inside downstream calls (DynamoDB, S3, an outbound HTTP call) into separate subsegments; that granularity only appears once your code is instrumented with the X-Ray SDK as shown above. This split — "automatic segment" vs. "SDK-instrumented subsegments" — is exactly the kind of distinction DVA-C02 tests.
for i in {1..10}; do
curl -s -o /dev/null -w "%{http_code}\n" \
https://abc123xyz.execute-api.eu-west-1.amazonaws.com/hello \
-H "Authorization: Bearer $TOKEN"
done
A run of 200 status codes printed to the terminal.
dva-lab-hello Lambda function, and (if instrumented) DynamoDB — connected by edges showing request counts and average latency, colored green for healthy responses.
REST APIs (API Gateway v1) expose a stage-level Active Tracing setting (TracingEnabled) independent of any backend integration. HTTP APIs (v2), used in this exercise, do not expose that same separate stage toggle — the trace chain for an HTTP API + Lambda integration is driven by the Lambda function's own Active Tracing setting, which is exactly why enabling it on dva-lab-hello in Part A was the operative step. Knowing that REST APIs and HTTP APIs differ here is a good example of the REST-vs-HTTP-API feature gaps this exam likes to probe.
Deep dive: 05 — X-Ray Observability Study Guide.