Last reviewed: May 2026
Build the AWS services on the DOP-C02 exam with plain Terraform — one block at a time, each tied back to an exam domain. The same code works on OpenTofu.
By the end of this lab you'll have provisioned, with plain Terraform, a complete code-to-production pipeline — a CodeCommit Git repo, a CodeBuild project that runs your test suite and packages artifacts, a CodeDeploy application that rolls out to a target, a CodePipeline pipeline that chains source → build → deploy, and a CloudWatch Synthetics canary that pokes the deployed endpoint and alarms when it stops responding. This is the DOP-C02 reference architecture in five blocks.
Every resource is plain Terraform. Drop the snippets into a single main.tf, run terraform init, then terraform apply step-by-step.
>= 1.5 or OpenTofu >= 1.6.us-east-1.https://example.com; in production this would be your application's health-check endpoint.aws_s3_bucket as the pipeline source (CodePipeline supports S3 source).Mostly pay-per-use, no significant idle billing:
BUILD_GENERAL1_SMALL; a lab build costs cents.Whole stack is under $5/month running. Destroy when done.
Standard opener. The Code* services are regional — pick the region your source repos and target environments share. Defaulting to us-east-1 for the lab.
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Project = "certlabpro-dop-c02"
ManagedBy = "terraform"
}
}
}
resource "aws_s3_bucket" "artifacts" {
bucket_prefix = "certlabpro-dop-c02-artifacts-"
}
resource "aws_s3_bucket_public_access_block" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}CodeCommit is the AWS Git host. DOP-C02 tests the Code* suite end-to-end as a tightly-integrated story — the most common exam questions ask which AWS-native service combination achieves a given outcome, and the answer almost always starts with CodeCommit.
The default_branch = "main" matches modern Git defaults (CodeCommit historically created master). The repo we create is empty after apply; in real use you'd push your first commit with a buildspec.yml and appspec.yml at the root — those are the files CodeBuild (Step 3) and CodeDeploy (Step 4) read on every pipeline run.
resource "aws_codecommit_repository" "app" {
repository_name = "certlabpro-dop-c02-app"
description = "Application source for the DOP-C02 lab pipeline."
default_branch = "main"
}CodeBuild runs the build and test steps from your buildspec.yml. DOP-C02 tests the managed-image-vs-custom-image trade-off here — managed images are zero-maintenance but slower to start; custom images give you faster cold-start and reproducible toolchains. The lab uses the managed aws/codebuild/standard:7.0 image, which covers Node, Python, Java, Go, .NET, Ruby out of the box.
The IAM role gives CodeBuild permission to write to CloudWatch Logs (for build output), read the source from CodeCommit, and write artifacts to the S3 bucket from Step 1. LINUX_CONTAINER + BUILD_GENERAL1_SMALL is the cheapest tier at $0.005/build-minute.
resource "aws_iam_role" "codebuild" {
name = "certlabpro-dop-c02-codebuild"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "codebuild.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "codebuild" {
name = "build-permissions"
role = aws_iam_role.codebuild.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
Resource = "*"
},
{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"]
Resource = [aws_s3_bucket.artifacts.arn, "${aws_s3_bucket.artifacts.arn}/*"]
},
{
Effect = "Allow"
Action = "codecommit:GitPull"
Resource = aws_codecommit_repository.app.arn
},
]
})
}
resource "aws_codebuild_project" "build" {
name = "certlabpro-dop-c02-build"
service_role = aws_iam_role.codebuild.arn
source {
type = "CODEPIPELINE"
buildspec = "buildspec.yml"
}
artifacts {
type = "CODEPIPELINE"
}
environment {
type = "LINUX_CONTAINER"
image = "aws/codebuild/standard:7.0"
compute_type = "BUILD_GENERAL1_SMALL"
privileged_mode = false
}
}CodeDeploy handles the actual rollout — onto EC2, Lambda, or ECS. DOP-C02 tests the deployment-strategy axis repeatedly: AllAtOnce (cheapest, scariest), HalfAtATime, OneAtATime, CodeDeployDefault.LambdaCanary10Percent5Minutes (the Lambda canary pattern), and CodeDeployDefault.ECSAllAtOnce. We pick the Lambda compute platform for the lab because it doesn't require EC2 instances to actually exist — keeps the lab cheap and tightly-scoped.
The deployment configuration CodeDeployDefault.LambdaAllAtOnce is the simplest of the Lambda strategies. In production, LambdaCanary10Percent5Minutes is the most-frequently-recommended DOP-C02 answer for risk-mitigated deployments: it shifts 10% of traffic to the new version, waits 5 minutes for CloudWatch alarms, then shifts the remainder if nothing trips.
resource "aws_iam_role" "codedeploy" {
name = "certlabpro-dop-c02-codedeploy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "codedeploy.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "codedeploy_lambda" {
role = aws_iam_role.codedeploy.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda"
}
resource "aws_codedeploy_app" "app" {
name = "certlabpro-dop-c02-app"
compute_platform = "Lambda"
}
resource "aws_codedeploy_deployment_group" "lambda" {
app_name = aws_codedeploy_app.app.name
deployment_group_name = "default"
service_role_arn = aws_iam_role.codedeploy.arn
deployment_config_name = "CodeDeployDefault.LambdaAllAtOnce"
deployment_style {
deployment_option = "WITH_TRAFFIC_CONTROL"
deployment_type = "BLUE_GREEN"
}
auto_rollback_configuration {
enabled = true
events = ["DEPLOYMENT_FAILURE"]
}
}CodePipeline is the orchestrator that ties the prior three steps into a single graph: source from CodeCommit, build with CodeBuild, deploy with CodeDeploy. Every transition is a hook point — you can add manual approvals, parallel test stages, and notifications between any two stages. The DOP-C02 Configuration Management and IaC domain tests this stage-and-action structure repeatedly.
Once the pipeline exists, you have CI/CD. The last piece — CloudWatch Synthetics — closes the loop on deployment observability: a canary is a managed headless-browser script (Puppeteer-based) that runs on a schedule and reports success/failure to CloudWatch. DOP-C02's Monitoring and Logging domain leans hard on this pattern: deploy a change → canary catches the regression within minutes → CodeDeploy auto-rollback (which we wired up in Step 4) triggers from the canary's CloudWatch alarm.
The canary script below polls https://example.com every 5 minutes; in production you'd point it at your application's health endpoint, with assertions on response body and headers, not just HTTP 200.
# ── CodePipeline ──────────────────────────────────────────────
resource "aws_iam_role" "codepipeline" {
name = "certlabpro-dop-c02-codepipeline"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "codepipeline.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "codepipeline" {
name = "pipeline-permissions"
role = aws_iam_role.codepipeline.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"codecommit:*",
"codebuild:*",
"codedeploy:*",
"s3:*",
"iam:PassRole",
]
Resource = "*"
}]
})
}
resource "aws_codepipeline" "main" {
name = "certlabpro-dop-c02"
role_arn = aws_iam_role.codepipeline.arn
artifact_store {
location = aws_s3_bucket.artifacts.bucket
type = "S3"
}
stage {
name = "Source"
action {
name = "Source"
category = "Source"
owner = "AWS"
provider = "CodeCommit"
version = "1"
output_artifacts = ["source"]
configuration = {
RepositoryName = aws_codecommit_repository.app.repository_name
BranchName = "main"
}
}
}
stage {
name = "Build"
action {
name = "Build"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
version = "1"
input_artifacts = ["source"]
output_artifacts = ["build"]
configuration = {
ProjectName = aws_codebuild_project.build.name
}
}
}
}
# ── Synthetics canary ─────────────────────────────────────────
resource "aws_iam_role" "canary" {
name = "certlabpro-dop-c02-canary"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "canary_logs" {
role = aws_iam_role.canary.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
data "archive_file" "canary_src" {
type = "zip"
output_path = "${path.module}/build/canary.zip"
source {
filename = "nodejs/node_modules/index.js"
content = <<-EOT
const synthetics = require("Synthetics");
exports.handler = async () => {
const page = await synthetics.getPage();
const res = await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
if (!res.ok()) throw new Error("Status code " + res.status());
};
EOT
}
}
resource "aws_synthetics_canary" "endpoint" {
name = "certlabpro-dop-c02-endpoint"
artifact_s3_location = "s3://${aws_s3_bucket.artifacts.bucket}/canary/"
execution_role_arn = aws_iam_role.canary.arn
handler = "index.handler"
zip_file = data.archive_file.canary_src.output_path
runtime_version = "syn-nodejs-puppeteer-9.0"
start_canary = true
schedule {
expression = "rate(5 minutes)"
}
}terraform destroy tears down everything in this lab. Two notes:
aws s3 rm s3://<bucket> --recursive) before destroy or use force_destroy = true on the bucket resource and re-apply once before destroying.start_canary = true — it's running continuously at 5-minute intervals from the moment of apply. Each run is ~$0.0012; a forgotten lab canary running for a week is ~$0.25. Cheap, but real. Destroy when done.DOP-C02 covers a broad professional surface — CloudFormation StackSets for multi-account / multi-region IaC, Service Catalog, AWS Config rules + remediation, Systems Manager OpsCenter + Patch Manager, AWS Health Dashboard automation, Trusted Advisor checks, AWS Backup, EventBridge Pipes, Step Functions for cross-service orchestration, and X-Ray for distributed tracing.
We stick to the end-to-end CI/CD pipeline because it's the single most-tested architecture on the exam — every other DOP-C02 pattern (Config rules feeding remediation, Synthetics paging via SNS, StackSets rolling out via CodePipeline) builds on this base. Get the source-to-deploy chain in your hands first; layer on multi-account StackSets and cross-region orchestration once the basics feel mechanical.
For the surfaces not provisioned here, the Browse, Playbook, and Editorial sections of this cert page have conceptual coverage. A follow-up lab that adds CloudFormation StackSets + Config Rules + Systems Manager Patch Manager would round out the managing operations at scale domain.