CodePipeline: Wiring Source, Build, Deploy Into a Chain

K
Kai··5 min read

The first four Parts built each piece separately: code on CodeCommit, build with CodeBuild, deploy with CodeDeploy. But we still trigger each piece by hand — type start-build, type create-deployment. That isn't CI/CD yet; those are loose steps. CodePipeline is the conductor that joins them into one automated chain: a new commit kicks off the whole Source → Build → Deploy chain automatically. This article builds the first pipeline.

💰 Cost

The pipeline deploys to one t3.micro EC2 (in-place target). CodePipeline bills per active pipeline (there's a free-tier tier). Keep the pipeline + target for Articles 13–14, clean up at the end of Part V.

Goal

Understand the CodePipeline structure (stages, actions, artifacts), build a pipeline joining the three services you've learned, and see how artifacts flow between stages.

Stages, actions, artifacts

CodePipeline models the workflow as stages that run sequentially, each stage containing actions. Each action produces an output artifact (a bundle of files) stored in an artifact store (an S3 bucket), and the next stage takes the previous stage's output as its input artifact. That's how data flows: Source produces source code, Build takes the source code and produces a built bundle, Deploy takes the built bundle and puts it on the server.

We use pipeline type V2 — the current version, which supports variables, filtered triggers, and per-run billing.

Service role and artifact store

The pipeline needs a service role (trusting codepipeline.amazonaws.com) with permissions to: read/write the artifact bucket, fetch code from CodeCommit, call CodeBuild, call CodeDeploy. This is the orchestration role — it calls the services, while each service still runs under its own role (CodeBuild under the Article 2 role, CodeDeploy under the Article 8 role). The artifact store is the S3 bucket created in Article 2 (versioning already enabled — precisely for the pipeline).

Pipeline definition

The pipeline is declared in JSON, three stages chained together:

{
  "pipeline": {
    "name": "awscicd-pipeline",
    "roleArn": "arn:aws:iam::111122223333:role/awscicd-codepipeline-role",
    "artifactStore": {"type": "S3", "location": "awscicd-artifacts-111122223333-ap-southeast-1"},
    "pipelineType": "V2",
    "stages": [
      {"name": "Source", "actions": [{
        "name": "Source",
        "actionTypeId": {"category": "Source", "owner": "AWS", "provider": "CodeCommit", "version": "1"},
        "configuration": {"RepositoryName": "awscicd-demo-app", "BranchName": "main"},
        "outputArtifacts": [{"name": "SourceOutput"}]
      }]},
      {"name": "Build", "actions": [{
        "name": "Build",
        "actionTypeId": {"category": "Build", "owner": "AWS", "provider": "CodeBuild", "version": "1"},
        "configuration": {"ProjectName": "awscicd-demo-build"},
        "inputArtifacts": [{"name": "SourceOutput"}],
        "outputArtifacts": [{"name": "BuildOutput"}]
      }]},
      {"name": "Deploy", "actions": [{
        "name": "Deploy",
        "actionTypeId": {"category": "Deploy", "owner": "AWS", "provider": "CodeDeploy", "version": "1"},
        "configuration": {"ApplicationName": "awscicd-demo", "DeploymentGroupName": "awscicd-demo-dg"},
        "inputArtifacts": [{"name": "BuildOutput"}]
      }]}
    ]
  }
}

Notice the wiring: Source outputArtifacts: SourceOutput → Build inputArtifacts: SourceOutput, then Build outputArtifacts: BuildOutput → Deploy inputArtifacts: BuildOutput. The artifact names are exactly how stages connect to each other.

$ aws codepipeline create-pipeline --cli-input-json file://pipeline.json \
    --query 'pipeline.[name,pipelineType]' --output text
awscicd-pipeline    V2

The pipeline runs itself

Once created, the pipeline starts itself on the first run. Poll each stage's status:

$ aws codepipeline get-pipeline-state --name awscicd-pipeline \
    --query 'stageStates[].[stageName,latestExecution.status]' --output text
Source:InProgress  Build:None        Deploy:None
Source:Succeeded   Build:InProgress  Deploy:None
Source:Succeeded   Build:Succeeded   Deploy:InProgress
Source:Succeeded   Build:Succeeded   Deploy:Succeeded

All three stages run in sequence and Succeeded. The whole chain — pull code, build, test, deploy to EC2 — runs without typing a single command after creating the pipeline. Verify the app is on the server:

$ curl http://<ec2-public-ip>/
... awscicd demo app — v2 ...

Artifacts flow between stages

This is the mechanism worth dissecting. Each stage does not re-fetch the code from scratch — it takes the previous stage's output via the artifact store. Specifically:

  • Source pulls code from CodeCommit, bundles it into SourceOutput (a zip), uploads it to the artifact bucket.
  • Build takes SourceOutput as its input (no longer cloning CodeCommit itself — CodeBuild inside a pipeline uses the input artifact instead of the source declared in the project), runs buildspec.yml, produces BuildOutput containing index.html + appspec.yml + scripts/.
  • Deploy takes BuildOutput as the revision; because this bundle has appspec.yml at its root, CodeDeploy knows how to deploy it.
   commit → CodeCommit (main)
        │  trigger
   ┌──────────────── CodePipeline awscicd-pipeline (V2) ────────────────┐
   │  Source              Build                Deploy                    │
   │  CodeCommit  ──────▶ CodeBuild  ────────▶ CodeDeploy                │
   │  out:SourceOutput    in:SourceOutput      in:BuildOutput            │
   │                      out:BuildOutput      (in-place EC2)            │
   └──────┬──────────────────┬──────────────────────┬───────────────────┘
          └── S3 artifact store (each stage reads the previous stage's output) ──┘
                                                     ▼
                                       curl http://<ec2>/ → v2

The crux: the artifact is the contract between stages. Build doesn't need to know where Source got the code, Deploy doesn't need to know what Build did — each stage just receives a bundle, does its job, hands out a bundle. This makes each piece swappable: changing the source from CodeCommit to something else only touches the Source stage, the rest is untouched.

Trigger: running on commit

The first run was triggered by creating the pipeline. From then on, the pipeline runs itself every time the main branch of CodeCommit gets a new commit — CodePipeline sets up an EventBridge rule to catch that event (the trigger type is currently either PollForSourceChanges or via EventBridge depending on configuration). That means from now on, a developer just needs to git push, and the chain handles the rest until the code is running on the server.

🧹 Cleanup

The pipeline and EC2 target are used again in Articles 13–14, so keep them until the end of Part V. When cleaning up:

$ aws codepipeline delete-pipeline --name awscicd-pipeline
$ aws ec2 terminate-instances --instance-ids <target-iid>

Wrap-up

CodePipeline joins the stages into a chain: stages run sequentially, each action produces an artifact, and the next stage takes the previous stage's artifact via the S3 artifact store. We built a three-stage V2 pipeline — Source (CodeCommit) → Build (CodeBuild) → Deploy (CodeDeploy) — running under one orchestration service role, and it ran end-to-end the moment it was created, putting the app on EC2 with no manual action. The artifact is the contract between stages (SourceOutputBuildOutput), so each piece is independently swappable. From here, git push is enough to kick off the whole chain.

The next article makes the pipeline more practical for production: add a manual approval gate before deploy, run multiple actions in parallel, and filter triggers by branch — the things that turn a pipeline that "works" into one that's "usable at a company".