Hands-On Lab Guide

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.

Console region: eu-west-1 AWS CLI v2 syntax throughout 5 exercises, chained
5
Exercises
eu-west-1
Target region
12+
Services touched
30+
CLI commands

Before You Start — Prerequisites

Local tooling
IAM permissions

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.

Why this guide exists for the exam

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.

Lab Map

ExerciseWhat you buildCore servicesDeep-dive guide
1Containerized web app on Fargate behind an ALBECR, ECS, Fargate, ALB, IAM01 — Containers
2CI/CD pipeline with blue/green deploys to the Exercise 1 serviceCodePipeline, CodeBuild, CodeDeploy02 — CI/CD
3Serverless API + Lambda + DynamoDB via SAMSAM, CloudFormation, Lambda, API Gateway, DynamoDB03 — IaC
4HTTP API + Lambda protected by a Cognito JWT authorizerAPI Gateway (HTTP API), Lambda, Cognito06 — API Gateway & 07 — Cognito
5X-Ray Active Tracing on the Exercise 4 Lambda, read the service mapX-Ray, Lambda, IAM05 — X-Ray
How to use this guide

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.

Exercise 1 — Docker → ECR → ECS Fargate → ALB

Containers

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.

Part A — Build and Push the Image

  1. In a new project folder, create a minimal 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.
  2. Create the ECR repository. Console: ECR → Repositories → Create repository → Private → name it dva-lab-web, region eu-west-1. CLI equivalent:
    bash
    aws ecr create-repository \
      --repository-name dva-lab-web \
      --image-scanning-configuration scanOnPush=true \
      --region eu-west-1
    Response includes a repositoryUri like <account-id>.dkr.ecr.eu-west-1.amazonaws.com/dva-lab-web.
  3. Authenticate Docker to ECR using a short-lived token — never store long-term credentials in docker login.
    bash
    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.
  4. Build the image locally.
    bash
    docker build -t dva-lab-web:latest .
    Image appears in docker images.
  5. Tag the image with the full ECR repository URI.
    bash
    docker tag dva-lab-web:latest \
      <account-id>.dkr.ecr.eu-west-1.amazonaws.com/dva-lab-web:latest
  6. Push the image to ECR.
    bash
    docker push <account-id>.dkr.ecr.eu-west-1.amazonaws.com/dva-lab-web:latest
    Push completes; the image tag latest is now visible under Images in the ECR console.
Why this matters for the exam

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.

Part B — IAM Roles for the Task

  1. Create two IAM roles: an ECS task execution role (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.
Task role vs. task execution role — the classic exam trap

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.

Part C — Register the Task Definition

task-definition.json
{
  "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"
        }
      }
    }
  ]
}
  1. Create the CloudWatch Logs group referenced above, then register the task definition.
    bash
    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-1
    Response returns a taskDefinitionArn ending in dva-lab-task:1.
Why 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.

Part D — Cluster, ALB, and Service

  1. Create the ECS cluster. Console: ECS → Clusters → Create cluster → name dva-lab-cluster, infrastructure AWS Fargate. Cluster shows status ACTIVE with 0 running tasks.
  2. Create an Application Load Balancer. Console: EC2 → Load Balancers → Create → Application Load Balancer, scheme internet-facing, at least two public subnets across two Availability Zones in eu-west-1. ALB state Active, with a public DNS name.
  3. Create a target group of type IP (not Instance), protocol HTTP, port 80, health check path /, 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.
  4. Create a security group for the tasks that allows inbound TCP 80 only from the ALB's security group (not from 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.
  5. Create the ECS service, linking the task definition, cluster, target group, and networking. Console: ECS → Clusters → dva-lab-cluster → Create service → Launch type Fargate → task definition dva-lab-task → desired tasks 2 → attach to the ALB target group. CLI equivalent:
    bash
    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.
  6. Browse to the ALB's DNS name in a browser or via curl.
    bash
    curl -I http://dva-lab-alb-1234567890.eu-west-1.elb.amazonaws.com
    HTTP/1.1 200 OK served by your container.
Why 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.

Exercise 2 — CodePipeline with CodeBuild and CodeDeploy Blue/Green

CI/CD

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.

CodeCommit is not the source stage to reach for anymore

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.

Part A — Source Connection and Repository

  1. Push the Exercise 1 Dockerfile and app code to a new GitHub repository. Repository exists with at least a main branch containing the Dockerfile.
  2. Create a connection. Console: Developer Tools → Settings → Connections → Create connection → GitHub → authorize the AWS Connector for GitHub app. Connection status Available, with a connection ARN you'll reference in the pipeline's source stage.

Part B — buildspec.yml and the CodeBuild Project

buildspec.yml
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
  1. Create the CodeBuild project. Console: CodeBuild → Create build project → source: the CodePipeline source artifact → environment image aws/codebuild/amazonlinux2-x86_64-standard:5.0enable "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.
  2. Attach an IAM service role to the project with 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.
Why "Privileged" mode is the step people forget

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.

Part C — appspec.yml and the CodeDeploy Application

appspec.yml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "dva-lab-web"
          ContainerPort: 80
  1. Create a second, "green" target group (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.
  2. Create the CodeDeploy application with compute platform ECS. Console: CodeDeploy → Applications → Create application → name dva-lab-app → compute platform Amazon ECS. CLI equivalent:
    bash
    aws deploy create-application \
      --application-name dva-lab-app \
      --compute-platform ECS \
      --region eu-west-1
  3. Create the deployment group referencing the ECS service, the two target groups, and the ALB listeners.
    bash
    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.
What blue/green actually does at deploy time

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.

Part D — Assemble the Pipeline

  1. Create the pipeline. Console: CodePipeline → Create pipeline → Source stage: GitHub (via CodeConnections), your repo and branch → Build stage: CodeBuild project from Part B → Deploy stage: Amazon ECS (Blue/Green), pointing at the CodeDeploy application and deployment group from Part C. Pipeline diagram shows three stages: Source → Build → Deploy.
  2. CLI alternative for scripting pipeline creation from a definition file:
    bash
    aws codepipeline create-pipeline \
      --cli-input-json file://pipeline.json \
      --region eu-west-1
  3. Push a code change to the 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.
Why the ECS appspec.yml looks different from the EC2/Lambda versions

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.

Exercise 3 — Serverless App with AWS SAM

Serverless / IaC

Scaffold, build, and deploy a minimal API Gateway + Lambda + DynamoDB application using the SAM CLI, entirely from a declarative template.yaml.

Part A — Initialize the Project

  1. Scaffold a new SAM application.
    bash
    sam init \
      --runtime python3.12 \
      --dependency-manager pip \
      --app-template hello-world \
      --name dva-lab-sam
    A new dva-lab-sam/ directory containing template.yaml, a hello_world/ function folder, and events/ test payloads.
  2. Replace the generated 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.

Part B — template.yaml Walkthrough

template.yaml
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"
What the SAM transform is doing for you

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.

Part C — Build and Deploy

  1. Build the application — SAM resolves dependencies and prepares deployment artifacts locally.
    bash
    sam build
    A .aws-sam/build/ directory containing the packaged function and a transformed template.
  2. Deploy interactively for the first time so SAM prompts for and saves your deployment configuration.
    bash
    sam deploy --guided
    Prompts 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.
  3. Note the ApiUrl output printed at the end of the deploy, or retrieve it later.
    bash
    aws cloudformation describe-stacks \
      --stack-name dva-lab-sam \
      --query "Stacks[0].Outputs" \
      --region eu-west-1
  4. Test the deployed API.
    bash
    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.
Why "Allow SAM CLI IAM role creation" is a step to actually understand

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.

Exercise 4 — HTTP API + Lambda + Cognito User Pool Authorizer

API + Identity

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.

Part A — Create the Cognito User Pool

  1. Create the user pool. Console: Cognito → User pools → Create user pool → sign-in options: email → default password policy → no MFA (lab simplicity) → region eu-west-1 → name dva-lab-pool. CLI equivalent:
    bash
    aws cognito-idp create-user-pool \
      --pool-name dva-lab-pool \
      --auto-verified-attributes email \
      --region eu-west-1
    Response includes a UserPool.Id like eu-west-1_XXXXXXXXX.
  2. Create an app client with no client secret (required for the USER_PASSWORD_AUTH flow used from the CLI/curl below) and explicit auth flows enabled.
    bash
    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-1
    Response includes a ClientId you'll use as the JWT audience and for sign-in.
  3. Create a test user and set a permanent password (skipping the forced first-login password reset for lab purposes).
    bash
    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-1
    User status CONFIRMED in the console's Users tab.

Part B — Lambda and the HTTP API

  1. Create a simple Lambda function 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.
  2. Create the HTTP API. Console: API Gateway → Create API → HTTP API → Add integration: Lambda → select 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.

Part C — Attach the JWT Authorizer

  1. Create a JWT authorizer on the API. Console: your HTTP API → Authorization → Manage authorizers → Create → type JWT → Identity source $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:
    bash
    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-1
    Response returns an AuthorizerId.
  2. Attach the authorizer to the GET /hello route.
    bash
    aws apigatewayv2 update-route \
      --api-id abc123xyz \
      --route-id def456ghi \
      --authorization-type JWT \
      --authorizer-id jkl789mno \
      --region eu-west-1
    The route now shows Authorization: JWT in the console.
  3. Call the API without a token to confirm the authorizer is enforcing.
    bash
    curl -i https://abc123xyz.execute-api.eu-west-1.amazonaws.com/hello
    HTTP/1.1 401 Unauthorized — the Lambda is never invoked.

Part D — Obtain and Use a JWT

  1. Authenticate as the test user to receive tokens.
    bash
    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
    Response JSON includes AuthenticationResult.IdToken, AccessToken, and RefreshToken.
  2. Call the protected route with the ID token as a bearer token.
    bash
    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.
ID token vs. access token — the field that decides which one works

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.

Exercise 5 — X-Ray Tracing on the Exercise 4 Lambda

Observability

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.

Part A — Enable Active Tracing

  1. Enable Active Tracing on the function. Console: Lambda → dva-lab-hello → Configuration → Monitoring and operations tools → Edit → toggle AWS X-Ray Active tracing on. CLI equivalent:
    bash
    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.
  2. Attach trace-write permissions to the function's execution role — the AWS managed policy AWSXRayDaemonWriteAccess covers xray:PutTraceSegments and xray:PutTelemetryRecords.
    bash
    aws iam attach-role-policy \
      --role-name dva-lab-hello-role \
      --policy-arn arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess
    Policy appears under the role's permissions in IAM.
The Active Tracing toggle silently does nothing without this IAM permission

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.

Part B — Instrument Downstream Calls (X-Ray SDK)

  1. If the function makes any downstream AWS SDK calls (e.g. to DynamoDB, as in Exercise 3's pattern), wrap the SDK client with the X-Ray SDK so those calls appear as their own subsegments rather than being invisible inside one opaque Lambda segment.
    python
    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")
    node.js
    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.
What Active Tracing captures automatically vs. what needs the SDK

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.

Part C — Generate Traffic and Read the Service Map

  1. Call the Exercise 4 API a handful of times to generate sampled traces.
    bash
    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.
  2. Open the X-Ray console in eu-west-1 → Traces → Service map, over a time range covering your test calls. A graph with nodes for the client, the API Gateway HTTP API, the dva-lab-hello Lambda function, and (if instrumented) DynamoDB — connected by edges showing request counts and average latency, colored green for healthy responses.
  3. Click into an individual trace from the Traces list. A timeline view showing the segment and any subsegments stacked with their durations, letting you see at a glance whether latency came from the Lambda's own code, a cold start, or a downstream call like DynamoDB.
Why there's no separate "enable X-Ray on the API" toggle here

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.