I used CodePipeline for months without really understanding it: push code, stages turn green, app updates. That was fine until something broke and the console didn't explain why, and I got tired of just guessing. So I opened a terminal and traced my own pipeline step by step, using the AWS CLI — every file, every stage, every "why does this even exist." It turned out to be a lot more interesting than the console picture makes it look.
Here's what I found, with the real commands and the real output (with private details removed). If you've ever wondered what actually happens between "code pushed" and "app updated," this is it.
How the pipeline actually wakes up
First question: does CodePipeline keep checking CodeCommit for changes, or does something tell it the moment a change happens? I checked the pipeline settings to find out:
aws codepipeline get-pipeline --name my-app --profile my-profile --region us-west-2
"triggerType": "CloudWatchEvent",
"PollForSourceChanges": "false"
That answers it:
-
PollForSourceChanges: falsemeans it's not checking on a timer. - A separate CloudWatch Events rule sits in the background, watching for pushes to that branch.
- The moment a push happens, that rule starts the pipeline right away — not "sometime in the next minute or two."
What "Source" actually produces
Here's something I hadn't thought about before: the Source stage doesn't give Build a live link to your repo. It packs up a snapshot instead.
"configuration": {
"BranchName": "qa",
"OutputArtifactFormat": "CODE_ZIP",
"RepositoryName": "my-app"
}
-
CODE_ZIPmeans CodePipeline takes the whole repo at that commit and turns it into one plain zip file. - No
.gitfolder, no history — just the files as they were at that exact moment. - There's another option,
CODEBUILD_CLONE_REF, that does a real git clone instead, in case your build ever needs actual git history.
Where that zip actually goes
Source, Build, and Deploy each run in their own separate, temporary space — they don't share files directly. So the zip needs one shared place to sit, and that's an S3 bucket that CodePipeline manages for the whole pipeline:
"artifactStore": {
"type": "S3",
"location": "codepipeline-us-west-2-<ACCOUNT_ID>"
}
This one bucket holds files for every stage of the pipeline — it's not tied to one specific file. The exact file name (the "key") is different every time the pipeline runs.
Proving the zip is real
I didn't want to just take this on faith, so I looked at the real file:
aws s3api head-object --bucket codepipeline-us-west-2-<ACCOUNT_ID> --key "my-app/SourceArti/<key>" --profile my-profile --region us-west-2
{
"ContentLength": 98230361,
"ContentType": "application/zip",
"ServerSideEncryption": "aws:kms"
}
What this confirms:
- A real zip file, about 98 MB — the whole repo, nothing left out.
- Locked with KMS encryption while it sits in storage.
- These files delete themselves after a set amount of time — they're not meant to stay forever, just handed off between stages and cleaned up afterward.
Finding the right artifact, not an old one
S3 doesn't know which file is "the latest one in this folder" — only CodePipeline actually keeps track of that. So if something's broken, don't just guess by browsing S3 folders. Ask the pipeline first:
aws codepipeline get-pipeline-state --name my-app --profile my-profile --region us-west-2
"currentRevision": { "revisionId": "<commit-hash>" },
"latestExecution": { "status": "Succeeded" },
"pipelineExecutionId": "<execution-id>"
currentRevision.revisionId is the real commit hash that was pulled. Copy that value, then use list-action-executions with the matching pipelineExecutionId to find the exact S3 file for that specific run.
Build: how it knows what to work on
Now, the Build stage. It doesn't just magically know where the code is — the pipeline settings tell it directly:
"configuration": {
"ProjectName": "my-app"
},
"inputArtifacts": [{ "name": "SourceArtifact" }]
SourceArtifact is just a name tag. CodePipeline connects that name to the real bucket and file it just created in the Source stage — Build never has to know the real path itself.
What it actually runs on
"environment": {
"type": "LINUX_CONTAINER",
"image": "aws/codebuild/amazonlinux-x86_64-standard:6.0",
"computeType": "BUILD_GENERAL1_MEDIUM"
},
"environmentVariables": [
{ "name": "PROFILE_NAME", "value": "qa" },
{ "name": "HOST_ENTRY", "value": "<internal-host>:<internal-ip>" },
{ "name": "TAG_VERSION", "value": "latest_qa" }
]
What happens every single build:
- A brand-new Amazon Linux container is created — no leftover cache, nothing saved from the last run.
- These environment variables (
PROFILE_NAME,HOST_ENTRY,TAG_VERSION) are already set inside it forbuildspec.ymlto use. - Before any of your commands even run, CodeBuild automatically downloads and unzips
SourceArtifactinto the container's working folder.
That last point is why your pom.xml, source files, Dockerfile, and everything else are just "there" the moment the build starts — nobody has to unzip anything by hand.
Walking through the buildspec, step by step
install phase — sets up the runtime:
runtime-versions:
java: corretto17
pre_build phase — logging in, before anything gets built:
- aws ecr get-login-password | docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-west-2.amazonaws.com
- export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token --domain my-codeartifact-domain ...)
- Line 1 logs Docker into ECR using a password that only works for a short time. This is what makes the later
docker pushcommand actually work. - Line 2 gets a token so Maven can download private dependencies later, using
settings.xml. - One tip: don't print
envin a real pipeline. It's handy for debugging on your own computer, but it prints every environment variable — including ones you didn't mean to share — straight into build logs your whole team can read.
build phase — the actual compile:
- mvn -s settings.xml package -DskipTests=true
Compiles the Spring Boot project and packs it into a .jar file.
post_build phase — this is where it gets interesting:
- docker build -t my-app:${TAG_VERSION} .
- docker tag my-app:${TAG_VERSION} <account-id>.dkr.ecr.us-west-2.amazonaws.com/my-app:${TAG_VERSION}
- docker push <account-id>.dkr.ecr.us-west-2.amazonaws.com/my-app:${TAG_VERSION}
- Line 1 builds the image inside the CodeBuild container.
- Line 2 gives it a second label pointing at the ECR address.
- Line 3 uploads it — this is the exact moment the image stops being "a temporary file that's about to disappear" and becomes a real, permanent file that sticks around.
Proof it landed:
aws ecr describe-images --repository-name my-app --profile my-profile --region us-west-2
{
"imageDigest": "sha256:<digest>",
"imagePushedAt": "2026-07-13T18:24:55+05:30"
}
The part that actually surprised me
Here's the one thing that made this whole exercise worth doing: Build creates two completely separate outputs, and they don't go to the same place.
- The Docker image — the actual application, ~290 MB in my case — goes to ECR.
- A tiny zip — just deploy scripts and a small
deployment-env-varsfile, about 2.35 KB total — goes to S3, in a folder CodePipeline labelsBuildArtifact.
Laid out side by side, the size gap is what makes it click:
| What | Goes where | Size |
|---|---|---|
| Docker image | Container registry (ECR) | ~290 MB |
| Deploy scripts + tag reference | S3 (BuildArtifact) |
~2-3 KB |
These aren't two copies of the same thing — they're not even close in size. The small S3 zip only exists so CodePipeline can hand Deploy one simple instruction: "here's the image tag to go get from ECR." That's because CodePipeline can only pass files forward through S3, never directly through ECR. Your actual application never touches that small handoff file at all.
If you want to see this yourself: open S3, find your pipeline's artifact bucket, look inside the BuildArtifact folder, download the (tiny) zip, and unzip it locally. You'll find things like appspec.yml, a couple of shell scripts, and an imagedefinitions.json — nothing that looks like your actual application code, because it was never meant to be in there.
Stage 3 — Deploy
This is the part that made the whole "small zip vs. big image" split actually make sense to me. Deploy is different from Source and Build in one big way: it doesn't create a temporary container somewhere in AWS. It runs directly on your actual EC2 server, using the CodeDeploy agent that's already installed and running there.
The first thing the agent does is read appspec.yml — the file that was inside that tiny zip the whole time — to find out which script to run, and when:
version: 0.0
os: linux
hooks:
ApplicationStop:
- location: stop_container.sh
timeout: 300
AfterInstall:
- location: start_container.sh
timeout: 300
CodeDeploy always follows the same set order of steps on the server, and appspec.yml just connects your scripts to the steps you care about:
-
ApplicationStop→stop_container.sh— runs before anything new gets installed. Its job is simple: stop and remove whatever container is already running under this app's name, so the port is free and there's no clash when the new one starts. -
AfterInstall→start_container.sh— this is where most of the real work happens. It's also where thatdeployment-env-varsfile — the one that's been sitting quietly in the small zip since the Build stage — finally gets used.
Roughly, here's what start_container.sh does:
# read the image tag that Build decided on
source deployment-env-vars
# pull that exact image from ECR
docker pull <account-id>.dkr.ecr.us-west-2.amazonaws.com/my-app:${TAG_VERSION}
# start it with the right port mapping, env vars, and volumes
docker run -d \
--name my-app \
-p 8080:8080 \
--env-file app.env \
-v /opt/my-app/logs:/app/logs \
<account-id>.dkr.ecr.us-west-2.amazonaws.com/my-app:${TAG_VERSION}
And that's the moment everything comes together. Remember that "just instructions" file from the Build stage — the one that seemed too small to matter next to a 290 MB image? This is what it was for, the whole time:
- It never carried any real weight itself.
- It just told this script which tag to go get.
- The image had already been sitting in ECR since the Build stage finished.
- Deploy's only job is to tell the server to go grab it and run it.
It's a small thing, but it changed how I think about the whole pipeline: Source and Build are about creating files and putting them in the right place. Deploy isn't really "installing" anything in the usual sense — it's just giving the server one clear instruction and stepping out of the way.
The short version, if you just want the summary
- Source starts the moment you push (no waiting, no timer), packs the exact commit into a zip with no git history, and drops it into a shared S3 bucket under a brand-new name every time.
- Build picks that zip up on its own, compiles the code, and builds a Docker image.
- Build's output splits in two: the real app goes to ECR, and a tiny instruction file goes to S3 just to tell Deploy which image tag to grab.
- Deploy runs right on the EC2 server itself, reads that small file to find the right tag, stops whatever's currently running, pulls the new image from ECR, and starts it back up.
Have you ever traced your own pipeline's files like this, or do you just trust the console screen for it? I expected this to be boring, and instead I came out understanding what I'm actually running, much better than before.
Top comments (0)