Module 8 — CI/CD
Automate build→test→deploy: CI/CD concepts, GitHub Actions on the lab runner, Bitbucket Pipelines and Jenkins at a glance, and GitOps with Argo CD.
- Lesson: Continuous Integration & Delivery
- Lesson: Pipelines with GitHub Actions
- Lesson: Bitbucket Pipelines & Jenkins
- Lesson: Pull-Based Deployment with Argo CD
- Assignment 1: Build-and-push pipeline on the lab runner
- Assignment 2: GitOps-deploy your app with Argo CD
Lesson: Continuous Integration & Delivery
What you'll learn
- The difference between Continuous Integration (CI), Continuous Delivery, and Continuous Deployment — three terms that share the letters "CD" but mean different things.
- The standard stages of a pipeline (build → test → package → deploy) and what an artifact is.
- The two models for getting software onto a server: push deployment versus pull deployment.
- Why teams automate all of this instead of running commands by hand.
By the end you can read a pipeline diagram and explain, in plain words, what each stage does and where your code goes after you push it.
The lesson
You just wrote some code and ran it on your laptop. It works. Now what? In a real team, that code has to be combined with everyone else's code, tested, turned into something runnable, and put on a server — over and over, many times a day. Doing that by hand is slow and error-prone. CI/CD is the practice of automating that path from "I pushed code" to "it's running."
1. Continuous Integration (CI)
Integration means combining your changes with the shared codebase (usually the main branch in Git). Continuous Integration means everyone merges small changes frequently — and every merge automatically triggers a build and a test run.
The point is to catch problems early. If ten developers each work alone for two weeks and then try to combine everything (a "big bang merge"), the conflicts and bugs are painful. If instead everyone integrates several times a day, each change is small and breakage is caught within minutes.
A CI system watches your Git repository. When you push, it:
- Checks out your code.
- Builds it (compiles, or assembles a runnable package).
- Runs automated tests.
- Reports pass/fail back to you, usually as a green check or red X on your commit.
That fast pass/fail signal is the heart of CI.
2. Continuous Delivery vs Continuous Deployment
Both are abbreviated "CD", which is confusing. Here is the distinction:
- Continuous Delivery — every change that passes CI is automatically prepared and ready to release, but a human clicks the button to actually push it to production. There is a manual approval gate.
- Continuous Deployment — there is no button. Every change that passes all tests goes to production automatically.
Continuous Delivery:
commit -> build -> test -> [READY] --(human approves)--> production
Continuous Deployment:
commit -> build -> test -> --------(automatic)---------> production
Continuous Delivery is the common, safer choice for most teams: automate everything up to the release, but keep a human in the loop for the final go. Continuous Deployment requires very strong automated testing and monitoring, because nobody is checking before users get the change.
3. Pipeline stages and artifacts
A pipeline is an ordered series of automated steps. A typical one looks like this:
+---------+ +-------+ +---------+ +----------+ +----------+
| BUILD |-->| TEST |-->| PACKAGE |-->| PUSH |-->| DEPLOY |
| compile | | unit | | make | | upload | | run on |
| code | | tests | | image | | artifact | | server |
+---------+ +-------+ +---------+ +----------+ +----------+
| | | | |
if this fails, the pipeline stops here and reports red
Each box is a stage. Stages run in order; if one fails, later ones are skipped. This "fail fast" behavior means you never deploy code that didn't pass its tests.
An artifact is the concrete output a stage produces and hands to the next stage — for example a compiled binary, a .zip, or (most common today) a container image. A container image is a self-contained, runnable package of your app plus everything it needs to run.
In our lab, the package stage builds a container image and the push stage uploads it to the lab's private container registry at 10.100.100.6. A registry is just a storage server for images. Later stages (or another tool entirely) pull the image from there and run it.
4. Push vs Pull deployment
There are two ways the deploy stage actually gets software onto the target server.
-
Push deployment: the pipeline reaches out to the server and pushes the change — e.g. it SSHes in, or runs
kubectl applyagainst a cluster. The CI server needs credentials to the production environment, and it initiates the change. -
Pull deployment: a tool running inside the target environment watches a Git repository and pulls changes in when it sees them. The CI pipeline only updates Git; it never touches production directly. This is the basis of GitOps, which you'll meet in the Argo CD chapter.
PUSH model: PULL model:
+----------+ +----------+ +-----------+
| CI server|---kubectl apply-------> | cluster | | CI server |---> Git repo
| (has prod| | | +-----------+ ^
| creds) | +----------+ | watches
+----------+ +----------+ | & pulls
| agent in |----------+
| cluster |
+----------+
Push is simpler to set up. Pull is safer at scale: production credentials never leave the cluster, and Git becomes the single source of truth for "what should be running."
5. Why automate at all?
Automating the build→test→deploy path gives you:
- Speed — minutes, not a manual afternoon.
- Consistency — the pipeline runs the exact same steps every time; no "I forgot to run the tests."
- A safety net — broken code is blocked before it reaches users.
- An audit trail — every deploy is tied to a commit, so you know exactly what is running and who changed it.
- Confidence to ship often — small, frequent releases are far less risky than big rare ones.
In the lab, you'll wire all of this up for real: a Gitea Actions runner on the build VM (10.100.100.11) builds your image and pushes it to the registry (10.100.100.6), and Argo CD pulls it onto the live Kubernetes cluster (master 10.100.100.7).
Dig deeper
- GitHub Actions — Understanding GitHub Actions
- Argo CD — What Is GitOps?
- Atlassian — Continuous integration vs delivery vs deployment
- Martin Fowler — Continuous Integration
Search terms
continuous integration vs continuous delivery vs deploymentwhat is a CI/CD pipeline explainedwhat is a build artifactpush vs pull deployment gitopswhy automate software deployment
Check yourself
- In one sentence each, define Continuous Integration, Continuous Delivery, and Continuous Deployment.
- What is the one difference between Continuous Delivery and Continuous Deployment?
- Name the typical stages of a pipeline in order. What happens to later stages if one fails?
- What is an "artifact"? Give an example used in our lab.
- In a pull-based deployment, which side initiates the change — the CI server or something inside the target environment? Why is that safer?
Lesson: Pipelines with GitHub Actions
What you'll learn
- The building blocks of GitHub Actions: workflows, jobs, steps, and runners — and how they nest.
- How triggers (the
on:key) decide when a workflow runs. - How to read and write a real workflow YAML file.
- How to build a container image and push it to the lab's private registry using the Gitea Actions runner.
By the end you can write a working build-and-push-image pipeline and run it on the lab.
The lesson
GitHub Actions is a CI/CD system built into GitHub. You describe your pipeline in a YAML file inside your repo, and GitHub runs it for you on triggers like "someone pushed code." The big reason we teach it: its workflow syntax is widely copied. The lab's Gitea server has an Actions runner that understands the same syntax, so everything you learn here runs for real on the lab — no GitHub account needed.
1. The four building blocks
GitHub Actions has a clear hierarchy. Define these terms once and the rest is easy:
- Workflow — the whole automated process, defined in one YAML file in
.github/workflows/(Gitea uses.gitea/workflows/). A repo can have many. - Job — a named unit of work inside a workflow. Jobs run on a fresh machine and, by default, run in parallel with each other.
- Step — a single command or action inside a job. Steps run in order on the same machine and share its filesystem.
- Runner — the machine that actually executes a job. In the lab, the runner lives on the build VM (
10.100.100.11).
Workflow (build-and-push.yml)
|
+-- Job: build
| |
| +-- Step 1: checkout code
| +-- Step 2: log in to registry
| +-- Step 3: build image
| +-- Step 4: push image
|
+-- Job: notify (could run in parallel)
A step is either a shell command (run:) or a reusable pre-built action (uses:). An action is a packaged piece of automation someone else wrote — for example actions/checkout clones your repo.
2. Triggers — the on: key
The on: key says when the workflow runs. Common triggers:
on:
push:
branches: [ main ] # run when code is pushed to main
pull_request: # run on every PR (great for CI tests)
workflow_dispatch: # add a manual "Run" button
schedule:
- cron: '0 2 * * *' # run nightly at 02:00
You can combine them. A common pattern is: run tests on every pull_request, but only build-and-push an image on push to main.
3. A minimal CI workflow
Here is a complete, readable workflow that runs tests on every push. Read it top to bottom:
name: CI # shown in the UI
on:
push:
pull_request:
jobs:
test: # job id
runs-on: ubuntu-latest # which runner to use
steps:
- name: Check out the code
uses: actions/checkout@v4
- name: Run the tests
run: |
echo "Running tests..."
./run-tests.sh
runs-on: picks the runner. On GitHub this would be a GitHub-hosted machine; on the lab it selects the Gitea runner on 10.100.100.11. The | after run: lets you write a multi-line script.
4. Secrets — never hardcode credentials
To push to a registry you need a password, but you must never write passwords in YAML. Instead store them as secrets (in repo settings) and reference them with ${{ secrets.NAME }}. They are encrypted and masked in logs:
- name: Log in to the registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login 10.100.100.6 \
-u "${{ secrets.REGISTRY_USER }}" --password-stdin
If you ever see a real password in a YAML file, that is a bug — flag it.
5. Build-and-push image workflow on the lab
This is the workflow you will actually build in Assignment 1. It builds a container image and pushes it to the lab's private registry at 10.100.100.6. The runner on 10.100.100.11 uses Docker-out-of-Docker (DooD) — meaning the job talks to the host's Docker daemon directly, so plain docker build works.
name: build-and-push
on:
push:
branches: [ main ]
jobs:
image:
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v4
- name: Log in to the private registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login 10.100.100.6 \
-u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build the image
run: docker build -t 10.100.100.6/myapp:${{ github.sha }} .
- name: Push the image
run: docker push 10.100.100.6/myapp:${{ github.sha }}
Two things to notice:
${{ github.sha }}is the commit's unique ID. Tagging the image with the commit SHA means every build produces a uniquely named image — you always know exactly which commit an image came from. This matches the lab convention of sha-based tags. (Avoid the:latesttag for deploys; it's ambiguous.)- The whole job runs on
10.100.100.11. The.indocker buildis the build context — the directory containing yourDockerfile.
6. Expressions, variables, and the job lifecycle
A few more pieces you'll see:
${{ ... }}expressions read context data —github.sha,github.ref(which branch/tag),secrets.*, and outputs from earlier steps.env:sets environment variables at workflow, job, or step level.needs:makes one job wait for another, so you can chaintest -> build -> deploy:
jobs:
test:
runs-on: ubuntu-latest
steps: [ { run: ./run-tests.sh } ]
build:
needs: test # only runs if 'test' succeeded
runs-on: ubuntu-latest
steps: [ { run: docker build -t myapp . } ]
Each job starts on a clean machine, so anything one job produces and another needs (like a built file) must be passed explicitly — using artifacts (actions/upload-artifact / download-artifact) or, for images, by pushing to the registry. That registry image is the handoff point to the deploy step you'll build with Argo CD in the next chapters.
Dig deeper
- GitHub Actions — Workflow syntax reference
- GitHub Actions — Events that trigger workflows
- GitHub Actions — Using secrets in a workflow
- Gitea — Act Runner documentation
- actions/checkout on GitHub
Search terms
github actions workflow jobs steps explainedgithub actions on push pull_request triggergithub actions build and push docker imagegitea act runner setup tutorialgithub actions secrets best practicesgithub actions needs depend on another job
Check yourself
- Put these in order from largest to smallest: step, workflow, job. What does a runner do?
- Which key decides when a workflow runs, and how would you make it run only on pushes to
main? - What is the difference between a
run:step and auses:step? - Why tag the image with
${{ github.sha }}instead oflatest? - How should you provide the registry password to the workflow, and why must it not appear in the YAML?
Lesson: Bitbucket Pipelines & Jenkins
What you'll learn
- What Bitbucket Pipelines is and how its YAML compares to GitHub Actions.
- The Jenkins server/agent model and what a "Jenkinsfile" pipeline looks like.
- When you'd reach for each tool, and the practical limitations and trade-offs of each versus GitHub Actions.
- That CI/CD concepts transfer between tools — once you understand the model, switching tools is mostly learning new syntax.
By the end you can compare three CI engines and pick a sensible one for a given situation.
The lesson
GitHub Actions is not the only CI/CD engine, and in your career you will meet others. The good news: the concepts from the previous chapters — triggers, stages, steps, runners, artifacts — apply everywhere. Only the syntax and the operating model change. This chapter gives you a working mental model of two common alternatives: Bitbucket Pipelines and Jenkins. We keep these at overview depth — enough to read one and know when to choose it.
1. The big picture: hosted vs self-managed
CI engines fall on a spectrum:
More managed for you <---------------------------------> More you manage
+-------------------+ +----------------------+ +-------------------+
| GitHub Actions / | | Self-hosted runners | | Jenkins |
| Bitbucket Pipelines| | (Gitea runner, etc.) | | (you run the |
| cloud runners | | | | whole server) |
+-------------------+ +----------------------+ +-------------------+
GitHub Actions and Bitbucket Pipelines are cloud-hosted: the vendor gives you runners and you just write YAML. Jenkins is a server you install and operate yourself — maximum control, maximum responsibility. The lab's Gitea runner sits in the middle: cloud-style YAML, but on a machine you own (10.100.100.11).
2. Bitbucket Pipelines
Bitbucket is Atlassian's Git hosting (often used alongside Jira). Bitbucket Pipelines is its built-in CI/CD, configured in a single file at the repo root: bitbucket-pipelines.yml.
The model is similar to GitHub Actions but simpler and more opinionated. Every step runs inside a Docker container (you pick the image), and steps are grouped under triggers like default, branches, and pull-requests:
image: node:20 # default container for all steps
pipelines:
default: # runs on every push (any branch)
- step:
name: Build and test
script:
- npm install
- npm test
branches:
main: # runs only on the main branch
- step:
name: Build image
script:
- docker build -t myapp .
services:
- docker # opt in to a Docker daemon
Notice the shape is the same idea as GitHub Actions: triggers at the top, then steps with a script: (the equivalent of run:). Key differences:
- The whole config is one file, not many workflow files.
- Each step is always a container; you set
image:rather thanruns-on:. - It is tightly tied to Bitbucket and Atlassian's ecosystem.
When to use it: your team already lives in Bitbucket/Jira. Limitations vs GitHub Actions: a far smaller marketplace of reusable actions, cloud build minutes are metered and can get costly, and it's less flexible for complex multi-job graphs.
3. Jenkins: the server/agent model
Jenkins is the veteran open-source automation server. Unlike the others, it is not bundled with a Git host — you install and run it yourself. Its defining feature is the controller/agent (historically "master/agent") architecture:
+---------------------+
| Jenkins controller | <- schedules jobs, stores config,
| (the web UI/brain) | shows the dashboard
+----------+----------+
|
+--------------+--------------+
| | |
+---------+ +---------+ +---------+
| Agent 1 | | Agent 2 | | Agent 3 | <- where builds actually run
| (Linux) | | (Win) | | (ARM) |
+---------+ +---------+ +---------+
The controller is the brain: it stores configuration, schedules work, and serves the dashboard. Agents are worker machines that actually execute builds — you can have agents on different operating systems and architectures. This is powerful: one Jenkins can drive builds across a whole fleet.
Modern Jenkins pipelines are defined in code in a Jenkinsfile committed to your repo, written in a Groovy-based syntax:
pipeline {
agent any // run on any available agent
stages {
stage('Build') {
steps { sh 'docker build -t myapp .' }
}
stage('Test') {
steps { sh './run-tests.sh' }
}
stage('Push') {
steps { sh 'docker push myapp' }
}
}
}
Same concepts again — stages and steps — just different syntax (sh instead of run:/script:).
When to use it: you need to self-host for compliance or air-gapped reasons, you have unusual build needs (exotic OSes, hardware), or you have a huge existing Jenkins estate. Its enormous plugin ecosystem (1,800+ plugins) can integrate with almost anything.
Limitations / trade-offs vs GitHub Actions: you own the uptime, upgrades, security patching, and backups of the whole server — that's real operational work. The plugin ecosystem is powerful but plugins vary in quality and can break on upgrades. Groovy pipeline syntax is more complex than YAML. It does not come pre-integrated with your Git host's UI the way Actions does.
4. Choosing between them
A quick decision guide:
Already on GitHub? -> GitHub Actions
Already on Bitbucket/Jira? -> Bitbucket Pipelines
Need full self-host control,
exotic platforms, or have
a big existing install? -> Jenkins
Want Actions syntax but on
your own infrastructure? -> self-hosted runner (the lab's Gitea runner)
The honest truth: the concepts dominate. Triggers, stages, runners/agents, secrets, artifacts, and push-vs-pull deployment exist in all three. If you deeply understand the GitHub Actions chapter, you can read a bitbucket-pipelines.yml or a Jenkinsfile and roughly follow it on day one. Pick the tool that fits where your code already lives and how much you want to operate yourself.
5. What they share (and what you should standardize on)
Whatever the engine, good pipelines look the same:
- Triggered automatically on push / pull request.
- Fast feedback: tests early, fail fast.
- Secrets stored in the platform, never in the repo.
- A versioned artifact (usually a container image tagged by commit) as the handoff to deployment.
- The pipeline definition lives in the repo as code (
workflow.yml,bitbucket-pipelines.yml,Jenkinsfile) so it's reviewed and versioned like everything else.
That last point — pipeline-as-code — is the throughline, and it sets up the next chapter, where Git becomes the source of truth for deployment too.
Dig deeper
- Bitbucket Pipelines — Get started
- Bitbucket Pipelines — Configure bitbucket-pipelines.yml
- Jenkins — Pipeline documentation
- Jenkins — Managing nodes (agents)
- Jenkins — Using a Jenkinsfile
Search terms
bitbucket pipelines yml tutorialjenkins controller agent architecture explainedjenkinsfile declarative pipeline examplejenkins vs github actions comparisonbitbucket pipelines vs github actionsself hosted ci cd vs cloud hosted
Check yourself
- What single file configures Bitbucket Pipelines, and what does every step always run inside?
- In Jenkins, what is the difference between the controller and an agent?
- Name one situation where Jenkins is the better choice than GitHub Actions, and one cost of choosing it.
- List two things that are true of a good pipeline regardless of which engine runs it.
- Why does the chapter say "the concepts dominate"? Give an example of a concept that appears in all three tools.
Lesson: Pull-Based Deployment with Argo CD
What you'll learn
- The GitOps principle: Git as the single source of truth for what runs in your cluster.
- What an Argo CD Application is, what sync means, and how drift detection works.
- How pull-based deployment contrasts with the push-based CI you've seen so far.
- How to deploy an app to the lab's live Kubernetes cluster with Argo CD.
By the end you can explain GitOps to a teammate and define an Argo CD Application that deploys to the lab cluster.
The lesson
In the concepts chapter you met pull deployment: instead of the CI pipeline pushing changes into production, a tool inside the environment watches Git and pulls changes in. GitOps is that idea made into a discipline, and Argo CD is the most popular tool for doing it on Kubernetes. This chapter shows how it works and has you deploy to the lab's real cluster (master 10.100.100.7).
1. The GitOps principle
GitOps rests on one rule: the desired state of your system is described declaratively in Git, and an automated agent continuously makes the cluster match Git.
"Declaratively" means you describe what you want (e.g. "3 replicas of myapp version abc123"), not how to get there. Kubernetes manifests (YAML) are already declarative, which is why GitOps and Kubernetes fit so well.
The four GitOps principles, in plain words:
- Declarative — the whole system is described as data, not scripts.
- Versioned in Git — that description lives in Git, so it has history, review, and rollback.
- Pulled automatically — an agent applies the Git state; no human runs
kubectl apply. - Continuously reconciled — the agent constantly checks that reality matches Git and corrects any difference.
The practical payoff: Git is the single source of truth. Want to know what's running? Read the repo. Want to roll back? git revert. Want an audit trail? It's the commit history.
2. Push CI vs pull GitOps
Compare the two models for the deploy step:
PUSH (classic CI deploy):
+-----------+ builds image +-----------+ kubectl apply +-----------+
| CI runner |----------------> | registry | | cluster |
| (Gitea, | |10.100.100.6|<-- CI also reaches|10.100.100.7|
| .11) |---- has cluster credentials --- and pushes ----->| |
+-----------+ +-----------+ +-----------+
The CI runner needs production credentials. It initiates the change.
PULL (GitOps with Argo CD):
+-----------+ builds image +-----------+
| CI runner |----------------> | registry |
| (.11) | |10.100.100.6|
+-----+-----+ +-----------+
| updates a manifest in Git (image tag = new sha)
v
+-----------+ Argo CD watches & pulls +-----------+
| Git repo |<------------------------------| Argo CD |
| (manifests)| | (in cluster|
+-----------+ | .7) |
+-----------+
CI only touches Git. Argo CD, inside the cluster, pulls and applies.
The crucial difference: in the pull model, production credentials never leave the cluster. Your CI runner only needs write access to a Git repo and the image registry — not to the cluster. That shrinks the blast radius if your CI is ever compromised, and it means every change to production is a Git commit you can see and revert.
3. The Argo CD Application
Argo CD runs inside your Kubernetes cluster. You tell it what to manage by creating an Application — itself a Kubernetes resource. An Application answers three questions: where is the desired state (source)?, where should it go (destination)?, and how should it sync?
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
spec:
project: default
source:
repoURL: https://gitea.example.com/intern/myapp-deploy.git
targetRevision: main # which branch/tag to track
path: k8s # folder of manifests within the repo
destination:
server: https://kubernetes.default.svc # the cluster Argo CD lives in
namespace: myapp
syncPolicy:
automated:
prune: true # delete resources removed from Git
selfHeal: true # undo manual changes that drift from Git
Read it as a sentence: "Watch the k8s/ folder on the main branch of this repo; keep namespace myapp in this cluster matching it; do it automatically."
4. Sync, drift, and self-heal
Two concepts make Argo CD powerful:
-
Sync = the act of making the cluster match Git. Argo CD compares the manifests in Git (desired) with what's actually running (live). If they differ, the Application is OutOfSync; running a sync applies the Git version. With
syncPolicy.automated, this happens on its own whenever Git changes. -
Drift detection = Argo CD continuously notices when the live cluster diverges from Git — for example if someone manually runs
kubectl editand bumps the replica count. The Application goes OutOfSync and Argo CD shows you exactly what differs. WithselfHeal: true, it automatically reverts the cluster back to what Git says. Git wins — always.
Git says: replicas=3 Live cluster: replicas=5 (someone edited it)
| |
+-----> Argo CD compares <-+
|
status = OutOfSync, diff shown
|
selfHeal -> sets replicas back to 3
prune: true extends this to deletions: if you remove a resource from Git, Argo CD removes it from the cluster too. This is what "Git is the source of truth" really means in practice — the cluster has no state that isn't in Git.
5. How a deploy actually flows in the lab
Putting the whole module together, here's the end-to-end flow you'll build across the assignments:
- You push app code to Gitea. The Actions runner on
10.100.100.11builds a container image tagged with the commit SHA and pushes it to the registry at10.100.100.6(Module chapter 2). - The image tag in your deployment manifest (in a Git repo Argo CD watches) is updated to the new SHA — either by hand at first, or automated later.
- Argo CD, running in the cluster (
10.100.100.7), sees the manifest changed, marks the Application OutOfSync, and syncs — pulling the new image and rolling out the update. - If anyone manually messes with the running app, drift detection flags it and
selfHealputs it back.
You never run kubectl apply against production yourself. You commit to Git; Argo CD does the rest.
6. When push is still fine
GitOps isn't mandatory for everything. For a tiny project, a push-based kubectl apply step in your CI is simpler and perfectly acceptable. GitOps shines when you have multiple environments or clusters, want strong audit/rollback, and want to keep cluster credentials out of CI. As the number of things you deploy grows, pull-based GitOps scales better and is safer.
Dig deeper
- Argo CD — Getting Started
- Argo CD — Application specification reference
- Argo CD — Automated sync policy (prune & self-heal)
- OpenGitOps — GitOps Principles
- Argo CD — Tracking and deployment strategies
Search terms
what is gitops explained simplyargo cd application crd tutorialargo cd sync drift self healpush vs pull deployment kubernetes argo cdargo cd automated sync prune selfhealgitops single source of truth git
Check yourself
- State the one rule that GitOps rests on.
- In the pull model, why don't your CI runner need credentials to the cluster?
- What three questions does an Argo CD
Applicationanswer? - What does it mean for an Application to be OutOfSync, and what does
selfHeal: truedo about it? - Walk through, in order, what happens in the lab from "you push app code" to "the new version is running" without you ever typing
kubectl apply.
Assignment 1: Build-and-push pipeline on the lab runner
Goal: Write a GitHub-Actions-style workflow that builds a container image from your app and pushes it to the lab's private registry, running for real on the lab's Gitea Actions runner.
Where: A repository on the lab's Gitea server. The workflow executes on the Actions runner on the build VM 10.100.100.11 (Docker-out-of-Docker) and pushes to the private registry at 10.100.100.6.
Tasks
- Create (or reuse) a small app repo in Gitea with a working
Dockerfileat its root. Keep the app trivial — a "hello" web server is plenty. Confirmdocker build .works locally first. - In the repo, create the workflow file
.gitea/workflows/build-and-push.yml. - Make the workflow trigger
on: pushto themainbranch. - Add a job with
runs-on: ubuntu-latestand these steps, in order:- Check out the code with
actions/checkout@v4. - Log in to the registry
10.100.100.6usingdocker loginwith credentials read from secrets (--password-stdin). Do not type the password in the file. - Build the image, tagging it
10.100.100.6/<yourname>-app:${{ github.sha }}. - Push that image to the registry.
- Check out the code with
- In the repo settings, add the registry credentials as Actions secrets (e.g.
REGISTRY_USER,REGISTRY_PASSWORD). Use the values your mentor provides; record real secret values nowhere in the repo. - Push a commit to
mainand watch the workflow run in the Gitea Actions tab. Read the logs. - If it fails, fix it and push again until it goes green. (This is normal — read the error, adjust, repeat.)
- Verify the image actually landed in the registry (ask your mentor for the registry catalog URL or use
docker pullof your SHA-tagged image from a lab host).
Deliverable
A link to your Gitea repo containing .gitea/workflows/build-and-push.yml, a green workflow run on main, and a SHA-tagged image present in the registry at 10.100.100.6. Paste the workflow file and the URL of the successful run into your submission.
Acceptance criteria — you're done when:
- The repo has a working
Dockerfilethat builds locally. -
.gitea/workflows/build-and-push.ymlexists and triggers on push tomain. - The workflow has steps for checkout, registry login, build, and push, in that order.
- Registry credentials come from Actions secrets; no password or token appears anywhere in the repo (grep your files to be sure).
- The image is tagged with
${{ github.sha }}, notlatest. - At least one workflow run on
maincompleted green. - The SHA-tagged image is confirmed present in the registry at
10.100.100.6.
Hints
- Re-read chapter 2, section 5 — your workflow is almost exactly that example; change the image name to yours.
- Run
docker build .on your machine before touching the workflow. Fix Dockerfile problems locally where the loop is faster. - "denied" or "unauthorized" on push almost always means the login step failed or the secret name is misspelled — check the login step's logs (the password will be masked, that's expected).
- Pipe the password into login:
echo "$PASS" | docker login ... --password-stdin. Avoid-pon the command line; it leaks into logs. - The
.at the end ofdocker buildis the build context (the current directory). Forgetting it is a common error. - Use
${{ github.sha }}exactly — the curly braces andgithub.prefix matter. - Blocked for >~30 min after re-reading the lessons? Bring what you've tried to your mentor.
Assignment 2: GitOps-deploy your app with Argo CD
Goal: Use Argo CD to deploy the image you built in Assignment 1 onto the lab's live Kubernetes cluster, the pull-based GitOps way — without ever running kubectl apply against production yourself.
Where: A Git repo on the lab's Gitea server holds your Kubernetes manifests. Argo CD runs inside the lab cluster (master 10.100.100.7) and pulls from that repo. Your image lives in the registry at 10.100.100.6.
Tasks
- Create a new Gitea repo (e.g.
<yourname>-deploy) to hold deployment manifests. This is your GitOps source of truth — keep it separate from your app code. - Add a
k8s/folder with Kubernetes manifests for your app: at minimum aDeploymentand aService. Point the Deployment's containerimage:at your SHA-tagged image from Assignment 1:10.100.100.6/<yourname>-app:<sha>. - Commit and push the manifests to
main. - Create an Argo CD
Application(a YAML manifest, as in chapter 4 section 3) that:- sets
source.repoURLto your deploy repo,targetRevision: main,path: k8s; - sets
destinationto the in-cluster server and a namespace named after you; - enables
syncPolicy.automatedwithprune: trueandselfHeal: true.
- sets
- Apply the Application to Argo CD (your mentor will tell you how to reach Argo CD — its UI or
argocd app create). Confirm the Application appears and reaches Synced and Healthy. - Verify your app is actually running in the cluster (check the pods are Running and the Service responds).
- Demonstrate drift detection: manually change the live Deployment (e.g. scale replicas with
kubectl scale). Watch Argo CD report OutOfSync and thenselfHealrevert it. Capture what you observed. - Demonstrate a GitOps update: change the image tag in your manifest to a newer SHA (rebuild via Assignment 1 if needed), commit, and watch Argo CD sync the new version automatically.
Deliverable
A link to your deploy repo (with k8s/ manifests and the Argo CD Application YAML), plus a short note describing what you saw during the drift-detection test (steps 7) and the GitOps update (step 8). Include the Argo CD Application status showing Synced/Healthy.
Acceptance criteria — you're done when:
- A separate deploy repo exists with
k8s/manifests (Deployment + Service) referencing your SHA-tagged image at10.100.100.6. - An Argo CD
Applicationmanifest exists with the correctsource(your repo,path: k8s,main) anddestination(your namespace in the cluster). -
syncPolicy.automatedis set withprune: trueandselfHeal: true. - The Application shows Synced and Healthy in Argo CD.
- Your app's pods are Running in the cluster and the Service responds.
- You triggered drift (a manual
kubectlchange) and observed Argo CD mark it OutOfSync and self-heal it back. - You changed the image tag in Git, committed, and Argo CD synced the new version automatically — you never ran
kubectl applyto deploy.
Hints
- Re-read chapter 4 sections 3–5; your Application is the example with your repo URL and namespace swapped in.
- Two repos is intentional: app code (Assignment 1) and deploy manifests (here). Mixing them is a common beginner mistake and muddles the "source of truth."
- If the pod won't start with
ImagePullBackOff, the cluster can't pull from10.100.100.6— check the image name/tag is exactly right and ask your mentor about registry pull access from the cluster. - "Synced but not Healthy" usually means the manifest applied but the app is crashing — check the pod logs, not Argo CD.
- For the drift test,
kubectl scale deployment/<name> --replicas=5is an easy way to create drift; Argo CD should pull it back to whatever your manifest says. - Remember: with GitOps, the only way you change production is a Git commit. If you're tempted to
kubectl applyto fix something, change the manifest in Git instead. - Blocked for >~30 min after re-reading the lessons? Bring what you've tried to your mentor.