This repository contains a preconfigured environment through GitHub Codespaces that is used to run a workshop to explore GitOps, GitHub Actions, and a Harbor Container Registry.
This workshop builds on the information provided in a previous workshop who's content can be found at this link https://github.com/NicholasCote/k8s-argo-codespace.
For more information about the CIRRUS platform, see NCAR HPC Documentation - CIRRUS
GitOps treats a Git repository as the single source of truth for infrastructure and applications. When you commit your desired state to Git, an automated tool ensures your running application matches the repository code. This approach brings all the benefits of Git to deployments.
- Fork this repository into your own GitHub account.
a. Owner will be your GitHub username - Select the button labeled
<> Codein the upper right - Select the Codespaces tab
- Use the
Create codespace on mainbutton to launch a new codespace
A container registry is a central location that hosts container images. There are a number of free public container registries, ghcr.io, quay.io, and hub.docker.com for example. The CIRRUS platform run by CISL contains its own container registry, https://hub.k8s.ucar.edu/, available to all UCAR staff. In order to push images to the CISL Container Registry, built on Harbor, a CIRRUS admin has to create a project and add you to it.
At this point in the workshop we will add everyone to the gitops-workshop project with admin privileges.
- Login to https://hub.k8s.ucar.edu/ via the
LOGIN WITH Entra IDbutton - Once logged in I can add you as an admin to the gitops-workshop project
Note: If you prefer to use a public repository like Docker Hub you can do this and still follow along with the workshop content. You will not use a robot account in Docker Hub, just your user id and password/API token.
Now that you have access to the gitops-workshop project, we need to create a robot account for GitHub Actions to authenticate with Harbor. Robot accounts are strongly recommended over personal accounts when logging in programmatically.
To create a robot account:
-
Log in to the Harbor Web UI at https://hub.k8s.ucar.edu/ using your CIT credentials
-
Navigate to the
gitops-workshopproject (you should have Project Admin privileges) -
Click the Robot Accounts tab
-
In the popup window, provide the following details:
- Name: Choose a descriptive name (this will result in
robot$gitops-workshop+{your_name}) - Expiration time: Set a reasonable expiration date (e.g., 1 month from now) - avoid using "never expire"
- Description: Optional description for the robot account
- Permissions: Ensure "Push" and "Pull" permissions are selected

- Name: Choose a descriptive name (this will result in
-
Click Finish to create the robot account
-
Important: Copy and store the one-time secret securely - it will not be shown again and should not be exposed in plain text publicly
Note: If you need to generate a new secret later, select the robot account, open the ACTIONS dropdown, and click REFRESH SECRET.
GitHub Actions needs secure access to your container registry credentials:
- In your forked repository, go to Settings → Secrets and variables → Actions
- Click "New repository secret"

- Name:
HARBOR_ROBOT_PW(orDOCKER_HUB_TOKENif using Docker Hub) - Secret: Paste the robot account token you copied earlier
- Click "Add secret"
- Click "New repository secret"
- Name:
HARBOR_ROBOT_USER(orDOCKER_HUB_USERif using Docker Hub) - Secret: Paste the robot account username
- Click "Add secret"
Your workflow will now be able to authenticate with the container registry securely.
When running GitHub Actions workflows, it's important to be aware of security settings that can be set at the repository level. For more details on common best practices, see CIRRUS - GitHub Actions Best Practices
When the codespace starts, it looks to .devcontainer/devcontainer.json for configuration including ports to forward, scripts to run automatically, customizations, and host resource requirements.
Container Creation (runs once):
.devcontainer/startup_script.shinstalls Kubernetes management tools (kubectx, kubens, k9s, ArgoCD CLI) and configures kubectl aliases
Post-Attach (runs each time you connect):
.devcontainer/post_attach.shsets up the workshop environment:- Installs minikube
- Waits for minikube to be ready
- Installs Argo CD
- Updates Argo CD for HTTP access
- Restarts Argo CD to apply changes
- Deploys a Flask application to Argo CD
- Provides access information
It takes a minute for these to complete. You will see output in the terminal at the bottom of the screen and this on successful completion.
Argo CD is a continuous delivery tool that specializes in GitOps. An Argo CD application is defined by the git URL, Helm chart directory location, and git branch that contains the Helm chart. Argo CD monitors the directory every 3 minutes and can be configured to automatically deploy any changes.
Workshop Flow:
This workshop is preconfigured with an Argo CD Application using the flask-helm directory in your fork. Currently it uses an existing container image. We'll add a GitHub Actions workflow to:
- Build a new container when changes are made to
flask-app/ - Push the new image to the container registry
- Update the application's
values.yamlfile with new image details - Push changes back to the repository automatically
This repository contains a pre-built Flask application with a Dockerfile to create a container image and a Helm chart directory, flask-helm/. The Helm chart follows standard conventions:
templates/directory with Kubernetes object definitionsvalues.yamlfile to provide configurable values for the templatesChart.yamlfile that provides metadata about the Helm chart
Kubernetes deploys different object types. These objects are defined in YAML files within the templates/ directory. For this simple web application, we only need two objects:
templates/deployment.yaml- Defines the containers to run, replicas, and resource requirementstemplates/service.yaml- Exposes the deployment containers on the Kubernetes network
The templates use Helm's templating syntax (e.g., {{ .Values.webapp.container.image }}) to substitute values from values.yaml, making the chart reusable across different environments.
The values.yaml file contains default configuration values that can be overridden during deployment. This includes settings like:
- Container image name and tag
- Service port configuration
- Resource limits and requests
- Replica count
Let's forward the container application and look at it live. In the terminal run
kubectl port-forward svc/flask-demo -n argocd 8001:5000
The application is now available at http://127.0.0.1:8001. There will be a pop up in the bottom right corner with an Open in Browser button for quick access.
We have confirmed the web application is up and running.
Now let's create the automation workflow that will build, push, and deploy your application changes.
-
In your codespace, create the GitHub Actions directory structure:
mkdir -p .github/workflows
-
Create a new workflow file:
touch .github/workflows/flask-app-cicd.yaml
-
Open the file and add the following workflow configuration: Note: If you used Docker Hub make sure to replace the HARBOR secrets to match the ones used for Docker Hub.
name: Flask App CI/CD Pipeline
on:
workflow_dispatch:
push:
paths:
- flask-app/**
branches:
- main
permissions:
contents: write
pull-requests: write
env:
GITHUB_BRANCH: ${{ github.ref_name }}
REGISTRY: hub.k8s.ucar.edu
PROJECT: gitops-workshop
USERNAME: {GitHub Username Lowercase}
jobs:
image-build-push:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d.%H.%M')" >> $GITHUB_OUTPUT
- name: Registry login
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.HARBOR_ROBOT_NAME }}
password: ${{ secrets.HARBOR_ROBOT_PW }}
- name: Build container image
run: |
docker buildx build -t ${{ env.REGISTRY }}/${{ env.PROJECT }}/flask-demo-${{ env.USERNAME }}:${{ steps.date.outputs.date }} .
- name: Push container image
run: |
docker push ${{ env.REGISTRY }}/${{ env.PROJECT }}/flask-demo-${{ env.USERNAME }}:${{ steps.date.outputs.date }}
- name: Update Helm values.yaml
run: |
sed -i "s|image: .*|image: ${{ env.REGISTRY }}/${{ env.PROJECT }}/flask-demo-${{ env.USERNAME }}:${{ steps.date.outputs.date }}|" flask-helm/values.yaml
- name: Update Helm Chart.yaml appVersion
run: |
sed -i "s|appVersion: .*|appVersion: ${{ steps.date.outputs.date }}|" flask-helm/Chart.yaml
- name: Commit and push changes
run: |
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
git config --global user.name "${{ github.actor }}"
git add flask-helm/values.yaml flask-helm/Chart.yaml
git commit -m "Update Helm chart with new image: ${{ steps.date.outputs.date }}"
git pushTriggers:
workflow_dispatch: Allows manual triggering from the GitHub Actions tabpushwithpaths: Automatically runs when changes are made to theflask-app/directory on the main branch
runs-on: This specifies what operating system your GitHub Action will run on.
- GitHub Provided Systems:
ubuntu-latest- Latest Ubuntu systemwindows-latest- Latest Windows systemmacos-latest- Latest macOS system
Note: If you need workflows that require more computational resources, access to Glade, or specialized hardware like GPUs, see CIRRUS GitHub Actions Runners.
Key Steps:
- Checkout: Downloads your repository code
- Date Generation: Creates a timestamp for image tagging
- Registry Login: Authenticates with Harbor using your robot account
- Build: Creates a new container image from your Flask app
- Push: Uploads the image to the container registry
- Update Helm Files: Modifies both
values.yamlandChart.yamlwith the new image reference - Commit Changes: Pushes the updated Helm chart back to your repository
Let's test our automation by making a change to the Flask application:
-
Edit the style.css file:
flask-app/app/static/style.css -
Make a visible change, such as updating the body background color on line 9:
background-color: #A8C700 -
Commit and push the change:
git add flask-app/app/static/style.css git commit -m "Update Flask app background color" git push -
Watch the workflow run:
- Go to your GitHub repository
- Click the "Actions" tab
- You should see your workflow running automatically
- Click on the workflow run to see detailed logs
Once the workflow completes successfully:
- Check the updated files: Your
flask-helm/values.yamlandflask-helm/Chart.yamlshould now reference the new container image - Wait for Argo CD: Argo CD checks for changes every 3 minutes, or you can manually sync in the Argo CD UI
- See the updated application: Run
kubectl port-forward svc/flask-demo -n argocd 8001:5000and see the new site with a green background
Note: If port-forward was running when the image was updated it will hit an error and stop. Rerunning the port-forward command will connect the new container
You've now experienced the complete GitOps cycle:
- Code Change → Automated Build → Registry Push → Chart Update → Automated Deployment
This approach ensures your running application always matches what's defined in your Git repository, with full traceability and automated deployments.



