Prerequisites #
SSH Key Pairs #
Helm Repo Pipeline Key #
Create a private SSH key pair, that is used by the GitLab code repostory pipeline to access the GitLab Helm Chart repository:
# Create SSH key pair
ssh-keygen -t rsa -b 2048 -C "helm_repo_key" -f ~/.ssh/helm_repo_key
-
helm_repo_keyPrivate SSH key, added as encoded CI/CD variable to the GitLab code repository -
helm_repo_key.pubPublic SSH key, add to GitLab Helm repository as deploy key / pipeline key
# encode the pipeline key: Test version
base64 -w0 ~/.ssh/helm_repo_key > helm_repo_key.b64
# List encoded key
cat helm_repo_key.b64
# Shell output:
LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUJGd0FBQUFkemMyZ3RjbgpOaEFBQUFBd0VBQVFBQUFRRUF3TElHOFVRVU8wYzdXZFp0d3B2OFlycDJWaU1KZVhDbnpXWW5iamh4SjNIa0lUd09QalFMCk5NaGx0T29lQVdNMEt6cGErdU5IQWl3V1VGWUl4TzdFOVo0NjJqTjRSTU9BWHRlSU1sZjJRekJwTHZsK1QyUnpkelRnQ0sKU3NING9IMjFmYlFBd28rdlg0aXE1UWROK1BHY2kzZ2hGcGRISE9rZXdpN1FwbDdxRjdlcHVDRkVoRXVQcDlyTXNHQVJlSQp6eGxtV1JKMmF4YTh4aTIrbktmRTdzMWlmQnQvQzFpZDhiVElUY3NlVGJIcmx4RkcvcEJTblRyUmZMUVNKemw5L2tqQ2hoCmY2OEE2V1I2Mi8xaXNZdHZvR1lvV1JrT2Nqejc1VVJDQkl4NXF2UWJOVHV2dEwrZjV4YkJ3bTZ6Ty9pSHZPVzV0dElhVm4KdVVHelJJUm9jUUFBQThqRlluZWV4V0ozbmdBQUFBZHpjMmd0Y25OaEFBQUJBUURBc2dieFJCUTdSenRaMW0zQ20veGl1bgpaV0l3bDVjS2ZOWmlkdU9IRW5jZVFoUEE0K05BczB5R1cwNmg0Qll6UXJPbHI2NDBjQ0xCWlFWZ2pFN3NUMW5qcmFNM2hFCnc0QmUxNGd5Vi9aRE1Ha3UrWDVQWkhOM05PQUlwS3dmaWdmYlY5dEFEQ2o2OWZpS3JsQjAzNDhaeUxlQ0VXbDBjYzZSN0MKTHRDbVh1b1h0Nm00SVVTRVM0K24yc3l3WUJGNGpQR1daWkVuWnJGcnpHTGI2Y3A4VHV6V0o4RzM4TFdKM3h0TWhOeXg1TgpzZXVYRVViK2tGS2RPdEY4dEJJbk9YMytTTUtHRi9yd0RwWkhyYi9XS3hpMitnWmloWkdRNXlQUHZsUkVJRWpIbXE5QnMxCk82KzB2NS9uRnNIQ2JyTTcrSWU4NWJtMjBocFdlNVFiTkVoR2h4QUFBQUF3RUFBUUFBQVFBUGdUT2NUYU5JR3NUc2gxdXAKRFVhTFJpQVg4YlZGTTNxWkJSZDJuNjJCSkVHY0ovb29hS1VmTCtHNUl4NWJjN2xDb2kwb1kwbTNJOGQrMnRGVlVUWUJZbQpRdll2VlNLUjNtNDdaR2dMdmtyNXhEQnBteWR5MXdRYzJsL0dRS0hKL2h0d25COTBpRDJ1OU83MVF6ZHpxTE0xS0tzVzU0CkMyRTQ3aFJvV000WFczeXkxSjJJYjhDZHdaNlZibHdjbWRRZWM4TE5QOXhqK1JhTWNTbXhuV3oxS1F1dFovY2NtWGk0ckoKWEhIdXpKeERJZVhrR0ZZcFZZWmhMcVZYRXFkdHdqbEM1YVNodjhDajdwazBreTRpWHlkVytWL2krdEh5SVpuc29ocG5kRgpVVXZGRXdtcFZSbnpCY0l4ZEtWV09sU1FZcENiNnR4dm92Zko0aWVoT0hTTkFBQUFnUURDbHlEd21qUDc2aWRGcHdsdUdxCncyaXVxcTlFRlJsVHRhckhsV01PUXRlN3BCUGxvMG9yQnFvOGVQYjlxbFhMWG1rMlRTZnhrUjQ4SWJsZTlLVy9OWmluVnEKd0tOU0NpZHozUlRXZ0xFdnU0c1dCNXE0ZmVjUVZGeW81SGlZM0FmQ3VldjF4ZEhtdHArSWU1dXpta3hMTXduc0tBam12ego0cXFUR3B6M1J4TXdBQUFJRUE1UktKVnovSkhQQkRoeCtMVVcvcStYcXJHQ2RmSlZDeng1VlhqQTZBWXhaNGM5Qlh3UDA0CkVzMnFXUXJyVFpXRGpFUEJDYmNTOStNR2h5Q0dlNmVTYjl6Rzl0eUFKYXdiVVgxZ25nK0xOcWVIY04vMWwzVzYyajNGbGEKTGdEczhhWDNCTzZyUU1nNTQvZkwrK2tuZ01VMm1GNUNPdlFMRnZQamdwZWdaQ1R1MEFBQUNCQU5kWXpNT1VsVDNmV2JNNgpGQjdLSlpJU094bXp5VURTMm8xU3ZJaXAvVnlBTHJ2WVBTSVNJTkc0amg3VlVYNEk1ejZkSmJHQmY0Y2YxMVVJWVlKQ1ZpCml0U0VWU2pPSnpaK1NSR25qWlF3aDkvcDJNU0drZU5FeUxKTityUWtza2ZsMmNqdW5zNlRYWmxPRWZiZlpnQkxXUytDWDcKQlp0dHh0a0NNaHhRdHNzVkFBQUFEV2hsYkcxZmNtVndiMTlyWlhrQkFnTUVCUT09Ci0tLS0tRU5EIE9QRU5TU0ggUFJJVkFURSBLRVktLS0tLQo=
Argo CD Repository Key #
Create a private SSH key pair, that is used by Argo CD to access the GitLab Helm Chart repository:
# Create SSH key pair
ssh-keygen -t rsa -b 2048 -C "deploy_key" -f ~/.ssh/deploy_key
-
deploy_keyPrivate SSH key, used by Argo CD -
deploy_key.pubPublic SSH key, add to GitLab Helm repository as deploy key
Harbor Project and Credentials #
I’m using the following Harbor project and credentials:
Harbor Project name: argocd-applicationset-1
User: robot$argocd-applicationset-1+applicationset-admin
Password: 1VH306D0DUjDLO89sHKt2hnCuoj3MPgq
GitLab Code Repository: Main (Dev) #
File and Folder Structure #
# applicationset-1-code
├── Dockerfiles
│ └── Dockerfile
├── .gitlab-build.yml
├── .gitlab-ci.yml
├── .gitlab-dev.yml
├── .gitlab-prod.yml
├── .gitlab-update-helm.yml
├── README.md
└── website
└── index.html
CI/CD Variables #
Harbor Credentials #
Add the credentials for the Harbor Project as GitLab CI/CD Variables:
# Key
HARBOR_USERNAME
# Value
robot$argocd-applicationset-1+applicationset-admin
# Key
HARBOR_PASSWORD
# Value
1VH306D0DUjDLO89sHKt2hnCuoj3MPgq
Variable options:
-
Enable “Visibility” x “Masked”
-
Disable Flag “Protect variable”
Encoded SSH Key #
Add the encoded private SSH (Helm Repo Pipeline Key) as GitLab CI/CD Variable:
# Key
HELM_REPO_KEY
# Value
LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUJGd0FBQUFkemMyZ3RjbgpOaEFBQUFBd0VBQVFBQUFRRUF3TElHOFVRVU8wYzdXZFp0d3B2OFlycDJWaU1KZVhDbnpXWW5iamh4SjNIa0lUd09QalFMCk5NaGx0T29lQVdNMEt6cGErdU5IQWl3V1VGWUl4TzdFOVo0NjJqTjRSTU9BWHRlSU1sZjJRekJwTHZsK1QyUnpkelRnQ0sKU3NING9IMjFmYlFBd28rdlg0aXE1UWROK1BHY2kzZ2hGcGRISE9rZXdpN1FwbDdxRjdlcHVDRkVoRXVQcDlyTXNHQVJlSQp6eGxtV1JKMmF4YTh4aTIrbktmRTdzMWlmQnQvQzFpZDhiVElUY3NlVGJIcmx4RkcvcEJTblRyUmZMUVNKemw5L2tqQ2hoCmY2OEE2V1I2Mi8xaXNZdHZvR1lvV1JrT2Nqejc1VVJDQkl4NXF2UWJOVHV2dEwrZjV4YkJ3bTZ6Ty9pSHZPVzV0dElhVm4KdVVHelJJUm9jUUFBQThqRlluZWV4V0ozbmdBQUFBZHpjMmd0Y25OaEFBQUJBUURBc2dieFJCUTdSenRaMW0zQ20veGl1bgpaV0l3bDVjS2ZOWmlkdU9IRW5jZVFoUEE0K05BczB5R1cwNmg0Qll6UXJPbHI2NDBjQ0xCWlFWZ2pFN3NUMW5qcmFNM2hFCnc0QmUxNGd5Vi9aRE1Ha3UrWDVQWkhOM05PQUlwS3dmaWdmYlY5dEFEQ2o2OWZpS3JsQjAzNDhaeUxlQ0VXbDBjYzZSN0MKTHRDbVh1b1h0Nm00SVVTRVM0K24yc3l3WUJGNGpQR1daWkVuWnJGcnpHTGI2Y3A4VHV6V0o4RzM4TFdKM3h0TWhOeXg1TgpzZXVYRVViK2tGS2RPdEY4dEJJbk9YMytTTUtHRi9yd0RwWkhyYi9XS3hpMitnWmloWkdRNXlQUHZsUkVJRWpIbXE5QnMxCk82KzB2NS9uRnNIQ2JyTTcrSWU4NWJtMjBocFdlNVFiTkVoR2h4QUFBQUF3RUFBUUFBQVFBUGdUT2NUYU5JR3NUc2gxdXAKRFVhTFJpQVg4YlZGTTNxWkJSZDJuNjJCSkVHY0ovb29hS1VmTCtHNUl4NWJjN2xDb2kwb1kwbTNJOGQrMnRGVlVUWUJZbQpRdll2VlNLUjNtNDdaR2dMdmtyNXhEQnBteWR5MXdRYzJsL0dRS0hKL2h0d25COTBpRDJ1OU83MVF6ZHpxTE0xS0tzVzU0CkMyRTQ3aFJvV000WFczeXkxSjJJYjhDZHdaNlZibHdjbWRRZWM4TE5QOXhqK1JhTWNTbXhuV3oxS1F1dFovY2NtWGk0ckoKWEhIdXpKeERJZVhrR0ZZcFZZWmhMcVZYRXFkdHdqbEM1YVNodjhDajdwazBreTRpWHlkVytWL2krdEh5SVpuc29ocG5kRgpVVXZGRXdtcFZSbnpCY0l4ZEtWV09sU1FZcENiNnR4dm92Zko0aWVoT0hTTkFBQUFnUURDbHlEd21qUDc2aWRGcHdsdUdxCncyaXVxcTlFRlJsVHRhckhsV01PUXRlN3BCUGxvMG9yQnFvOGVQYjlxbFhMWG1rMlRTZnhrUjQ4SWJsZTlLVy9OWmluVnEKd0tOU0NpZHozUlRXZ0xFdnU0c1dCNXE0ZmVjUVZGeW81SGlZM0FmQ3VldjF4ZEhtdHArSWU1dXpta3hMTXduc0tBam12ego0cXFUR3B6M1J4TXdBQUFJRUE1UktKVnovSkhQQkRoeCtMVVcvcStYcXJHQ2RmSlZDeng1VlhqQTZBWXhaNGM5Qlh3UDA0CkVzMnFXUXJyVFpXRGpFUEJDYmNTOStNR2h5Q0dlNmVTYjl6Rzl0eUFKYXdiVVgxZ25nK0xOcWVIY04vMWwzVzYyajNGbGEKTGdEczhhWDNCTzZyUU1nNTQvZkwrK2tuZ01VMm1GNUNPdlFMRnZQamdwZWdaQ1R1MEFBQUNCQU5kWXpNT1VsVDNmV2JNNgpGQjdLSlpJU094bXp5VURTMm8xU3ZJaXAvVnlBTHJ2WVBTSVNJTkc0amg3VlVYNEk1ejZkSmJHQmY0Y2YxMVVJWVlKQ1ZpCml0U0VWU2pPSnpaK1NSR25qWlF3aDkvcDJNU0drZU5FeUxKTityUWtza2ZsMmNqdW5zNlRYWmxPRWZiZlpnQkxXUytDWDcKQlp0dHh0a0NNaHhRdHNzVkFBQUFEV2hsYkcxZmNtVndiMTlyWlhrQkFnTUVCUT09Ci0tLS0tRU5EIE9QRU5TU0ggUFJJVkFURSBLRVktLS0tLQo=
Variable options:
-
Enable “Visibility” x “Masked”
-
Disable Flag “Protect variable”
Verify Variables #
Pipeline Manifests #
.gitlab-ci.yml #
- .gitlab-ci.yml
variables:
GITLAB_URL: "gitlab.jklug.work"
HELM_REPOSITORY: "git@gitlab.jklug.work:production/argocd-applicationsets/applicationset-1-helm.git" # Helm Chart Repository
HARBOR_CONTAINER_REGISTRY: "harbor.jklug.work"
HARBOR_PROJECT: "argocd-applicationset-1"
HARBOR_REPOSITORY: "${HARBOR_CONTAINER_REGISTRY}/${HARBOR_PROJECT}/${DEPLOYMENT_ENVIRONMENT}/example-app"
default:
interruptible: true
stages:
- build
- update-helm
workflow:
name: '${PIPELINE_NAME} -Commit: ${CI_COMMIT_TITLE}'
rules:
- if: $CI_COMMIT_REF_NAME == "main"
variables:
PIPELINE_NAME: 'Main branch build'
DEPLOYMENT_ENVIRONMENT: dev
IMAGE_VERSION: "${CI_COMMIT_REF_SLUG}_${CI_PIPELINE_IID}"
HELM_CHART_VERSION: $CI_PIPELINE_IID
HELM_REPO_BRANCH: "main"
- if: $CI_COMMIT_TAG
variables:
PIPELINE_NAME: "Tag $CI_COMMIT_TAG build"
DEPLOYMENT_ENVIRONMENT: prod
IMAGE_VERSION: "$CI_COMMIT_TAG"
HELM_CHART_VERSION: $CI_COMMIT_TAG
HELM_REPO_BRANCH: "prod"
include:
- local: .gitlab-dev.yml
- local: .gitlab-prod.yml
- local: .gitlab-build.yml
- local: .gitlab-update-helm.yml
.gitlab-build.yml #
- .gitlab-build.yml
.build:
stage: build
image: docker:28.4
services:
- name: docker:28.4-dind
alias: docker
command: ["--tls=false"]
variables:
DOCKER_TLS_CERTDIR: ""
before_script:
# Login to Harbor registry via robot user
- docker login -u "$HARBOR_USERNAME" -p "$HARBOR_PASSWORD" "$HARBOR_CONTAINER_REGISTRY"
script:
# Define image tag name
- IMAGE_TAG_COMPLETE="${HARBOR_REPOSITORY}:${IMAGE_VERSION}"
# Build and tag image
- docker build -f Dockerfiles/Dockerfile --pull -t "${IMAGE_TAG_COMPLETE}" .
# Push image to Harbor
- docker push "${IMAGE_TAG_COMPLETE}"
.gitlab-update-helm.yml #
- gitlab-update-helm.yml
.update-helm:
stage: update-helm
image: debian:12.12-slim
script:
# Install required tools
- apt-get update
- apt-get install -y --no-install-recommends git openssh-client ca-certificates curl
- curl -L "https://github.com/mikefarah/yq/releases/download/v4.47.2/yq_linux_amd64" -o /usr/local/bin/yq
- chmod +x /usr/local/bin/yq
# Configure SSH for Git
- mkdir -p ~/.ssh
- echo "$HELM_REPO_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H ${GITLAB_URL} >> ~/.ssh/known_hosts
# Clone the external repository into helm-chart-repo directory
- git clone --branch $HELM_REPO_BRANCH $HELM_REPOSITORY helm-chart-repo
- cd helm-chart-repo
# Verify the Helmchart does exist
- ls -R
- ls -R helm-chart
# Update values.yaml with new container images
- yq e ".example_app.repository = strenv(HARBOR_REPOSITORY) | .example_app.tag = strenv(IMAGE_VERSION)" -i helm-chart/values.yaml
# Update Chart.yaml with new version
- yq e ".version = \"$HELM_CHART_VERSION\" | .appVersion = \"$HELM_CHART_VERSION\"" -i helm-chart/Chart.yaml
# Update Commit Message
- yq e ".message = \"$CI_COMMIT_MESSAGE\"" -i helm-chart/Chart.yaml
# Add and commit the changes
- git config --global user.email "juergen@jklug.work"
- git config --global user.name "GitLab CI"
- git add helm-chart/values.yaml # Add values.yaml
- git add helm-chart/Chart.yaml # Add Chart.yaml
- git commit -m "Update Helm chart with image $VERSION"
# Push the changes back to the external repository
- git push origin $HELM_REPO_BRANCH
.gitlab-dev.yml #
- .gitlab-dev.yml
build-dev:
extends:
- .build
rules:
- if: $CI_COMMIT_REF_NAME == "main"
update-helm-dev:
extends:
- .update-helm
rules:
- if: $CI_COMMIT_REF_NAME == "main"
needs:
- job: build-dev # Require build job
.gitlab-prod.yml #
- .gitlab-prod.yml
build-prod:
extends:
- .build
rules:
- if: $CI_COMMIT_TAG
update-helm-prod:
extends:
- .update-helm
rules:
- if: $CI_COMMIT_TAG
needs:
- job: build-prod # Require build job
Dockerfile #
- Dockerfiles/Dockerfile
FROM nginx:1.18
COPY website/index.html /usr/share/nginx/html
Example Website #
- website/index.html
<!DOCTYPE html>
<html>
<head>
<title>K8s ApplicationSet Deployment</title>
</head>
<body>
<h1>Argo CD ApplicationSet</h1>
<p>Example Deployment</p>
</body>
</html>
GitLab Code Repository: Prod #
Create Prod Branch #
# Create a new "prod" branch and switch to it
git checkout -b prod
# Push local branch and establish tracking relationship between local branch and it's remote counterpart
git push -u origin prod
Create and Push Tag #
The tag push triggers the GitLab pipeline for the prod environment:
# Create local tag
git tag 1.0.1
# Push tag to remote repository
git push origin 1.0.1
GitLab Helm Repository #
File and Folder Structure #
# applicationset-1-helm
├── helm-chart
│ ├── Chart.yaml
│ ├── templates
│ │ ├── deployment.yaml
│ │ ├── ingress.yaml
│ │ └── service.yaml
│ └── values.yaml
└── README.md
# Create file and folder structure
mkdir -p helm-chart/templates &&
touch helm-chart/{Chart.yaml,values.yaml} &&
touch helm-chart/templates/deployment.yaml &&
touch helm-chart/templates/service.yaml &&
touch helm-chart/templates/ingress.yaml
Deploy Keys #
Add Pipeline Key #
Add the previously created public SSH key, so that GitLab code repository pipeline can access the Helm chart repository:
-
Go to: (Settings) > “Repository” > “Deploy keys”
-
Click “Add new key”
-
Define a title like “helm_repo_key”
-
Paste the public SSH key
helm_repo_key.pubinto the “Key” section -
Enable “Grant write permissions to this key”
-
Click “Add key”
Add Deploy Key #
Add the previously created public SSH key, so that Argo CD can access the Helm chart repository:
-
Go to: (Settings) > “Repository” > “Deploy keys”
-
Click “Add new key”
-
Define a title like “argocd_deploy”
-
Paste the public SSH key into the “Key” section
-
Disable “Grant write permissions to this key”
-
Click “Add key”
Verify keys #
Helm Chart: Dev Branch #
values.yaml #
- helm-chart/values.yaml
# Define the image details
example_app:
repository: "" # Registry will be updated by the pipeline
tag: "" # Tag will be updated by the pipeline
container_target_port: 80
ingress_domain: example-app-dev.jklug.work
imagePullSecrets:
- harbor-registry-secret # The Kubernetes secret name
Chart.yaml #
apiVersion: v2
name: Example App
description: Helm chart for Example App
type: application
version: ""
appVersion: ""
message: |
deployment.yaml #
- helm-chart/templates/deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-app
labels:
app: example-app
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
imagePullSecrets:
- name: "{{ .Values.imagePullSecrets | first }}"
containers:
- name: example-app
image: "{{ .Values.example_app.repository }}:{{ .Values.example_app.tag }}"
ports:
- containerPort: {{ .Values.example_app.container_target_port | int }}
imagePullPolicy: IfNotPresent
service.yaml #
- helm-chart/templates/service.yaml
---
apiVersion: v1
kind: Service
metadata:
name: example-app-service
spec:
type: ClusterIP
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 900
selector:
app: example-app
ports:
- name: http
protocol: TCP
port: {{ .Values.example_app.container_target_port | int }}
targetPort: {{ .Values.example_app.container_target_port | int }}
ingress.yaml #
- helm-chart/templates/ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-app-ingress
annotations:
#cert-manager.io/cluster-issuer: cluster-issuer-dns01
nginx.ingress.kubernetes.io/ssl-redirect: "false" # Set to true in prod
nginx.ingress.kubernetes.io/proxy-body-size: "0"
spec:
ingressClassName: nginx-metallb
#tls:
#- hosts:
#- "{{ .Values.example_app.ingress_domain }}"
#secretName: "example-app-tls-secret"
rules:
- host: "{{ .Values.example_app.ingress_domain }}"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: example-app-service
port:
number: {{ .Values.example_app.container_target_port | int }}
Helm Chart: Prod Branch #
Create Branch #
# Create a new "prod" branch and switch to it
git switch --create prod
values.yaml #
Adapt the Kubernetes Ingress URL of the values.yaml file:
- helm-chart/values.yaml
# Define the image details
example_app:
repository: "" # Registry will be updated by the pipeline
tag: "" # Tag will be updated by the pipeline
container_target_port: 80
ingress_domain: example-app-prod.jklug.work
imagePullSecrets:
- harbor-registry-secret # The Kubernetes secret name
Push Branch #
# Push local branch and establish tracking relationship between local branch and it's remote counterpart
git push -u origin prod
Ansible Playbook #
Apply Argo CD ApplicationSet #
---
# Deploy Argo CD Example ApplicationSet
- name: Argo CD ApplicationSet
hosts: localhost
connection: local
gather_facts: false
vars:
# Vault Secrets
argocd_repository_keys: "{{ lookup('community.hashi_vault.vault_kv2_get', 'services/argocd/gitlab_deploykey', engine_mount_point='homelab_prod').secret }}"
# Vault Variables
argocd_repository_private_key: "{{ argocd_repository_keys.private_key }}" # Key to access GitLab Helm chart repository
# GitLab Helm Chart Repository
gitlab_helm_repository: "git@gitlab.jklug.work:production/argocd-applicationsets/applicationset-1-helm.git" # Define GitLab Helm chart repository
gitlab_repository_name: "applicationset-1-helm" # Just a unique name for the GitLab repository in ArgoCD
# Harbor Credentials
harbor_registry: "harbor.jklug.work"
harbor_user: "robot$argocd-applicationset-1+applicationset-admin" # Harbor credentials for image pull
harbor_pw: "1VH306D0DUjDLO89sHKt2hnCuoj3MPgq" # Harbor credentials for image pull
# Deployment Variables
app_namespace_dev: example-app-dev # Define Kubernetes namespace
app_namespace_prod: example-app-prod # Define Kubernetes namespace
# ArgoCD Variables
argocd_namespace: "argocd" # ArgoCD default namespace
tasks:
# Kubernetes Namespaces
- name: Ensure Kubernetes Dev Namespace exists
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ app_namespace_dev }}"
- name: Ensure Kubernetes Prod Namespace exists
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ app_namespace_prod }}"
# Harbor Registry Secrets
- name: Create Image Pull Secret for Harbor Registry
vars:
dockerconfig: "{{ {'auths': {harbor_registry: {'username': harbor_user, 'password': harbor_pw, 'auth': (harbor_user + ':' + harbor_pw) | b64encode}}} }}"
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: harbor-registry-secret
namespace: "{{ app_namespace_dev }}"
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: "{{ dockerconfig | to_json | b64encode }}"
- name: Create Image Pull Secret for Harbor Registry
vars:
dockerconfig: "{{ {'auths': {harbor_registry: {'username': harbor_user, 'password': harbor_pw, 'auth': (harbor_user + ':' + harbor_pw) | b64encode}}} }}"
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: harbor-registry-secret
namespace: "{{ app_namespace_prod }}"
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: "{{ dockerconfig | to_json | b64encode }}"
# Argo CD GitLab Repository
- name: Add GitLab Repository via Argo CD Repository Secret
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: "{{ gitlab_repository_name }}"
namespace: "{{ argocd_namespace }}"
labels:
argocd.argoproj.io/secret-type: repository
type: Opaque
stringData:
url: "{{ gitlab_helm_repository }}"
sshPrivateKey: "{{ argocd_repository_private_key }}"
# Argo CD ApplicationSet
- name: Create Argo CD ApplicationSet
kubernetes.core.k8s:
state: present
apply: true
wait: true
definition:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: "{{ gitlab_repository_name }}"
namespace: "{{ argocd_namespace }}"
spec:
generators:
- list:
elements:
- branch: main
namespace: "{{ app_namespace_dev }}"
- branch: prod
namespace: "{{ app_namespace_prod }}"
template:
metadata:
name: '{{ gitlab_repository_name }}-{% raw %}{{branch}}{% endraw %}'
spec:
project: default
source:
repoURL: "{{ gitlab_helm_repository }}"
targetRevision: '{% raw %}{{branch}}{% endraw %}'
path: helm-chart
helm:
valueFiles:
- values.yaml
destination:
server: https://kubernetes.default.svc
namespace: '{% raw %}{{namespace}}{% endraw %}'
syncPolicy:
automated:
prune: true
selfHeal: true
# Run Ansible playbook:
ansible-playbook playbooks/argocd_applicationset.yml -i inventory
Argo CD #
Verify Applications #
Argo CD Applications:
Main / Dev Application:
Prod Application:
Argo CD CLI: Verify Resources #
CLI Login #
# Login to Argo CD instance
argocd login argocd.jklug.work --grpc-web
# Default user
admin
List GitLab Repositories #
The GitLab Helm chart repository is connected via the applicationset-1-helm Kubernetes secret, to remove the GitLab repository, delete the Kubernetes secret.
# List GitLab repositories
argocd repo list
# Shell output:
TYPE NAME REPO INSECURE OCI LFS CREDS STATUS MESSAGE PROJECT
git git@gitlab.jklug.work:production/argocd-applicationsets/applicationset-1-helm.git false false false false Successful
List ApplicationSet #
# List Argo CD ApplicationSets
kubectl -n argocd get applicationsets
# Shell output:
NAME AGE
applicationset-1-helm 32s
# Delete Argo CD ApplicationSet
kubectl -n argocd delete applicationset applicationset-1-helm
List Application #
# Verify Argo CD Applications
argocd app list
# Shell output:
NAME CLUSTER NAMESPACE PROJECT STATUS HEALTH SYNCPOLICY CONDITIONS REPO PATH TARGET
argocd/applicationset-1-helm-main https://kubernetes.default.svc example-app-dev default Synced Healthy Auto-Prune <none> git@gitlab.jklug.work:production/argocd-applicationsets/applicationset-1-helm.git helm-chart main
argocd/applicationset-1-helm-prod https://kubernetes.default.svc example-app-prod default Synced Healthy Auto-Prune <none> git@gitlab.jklug.work:production/argocd-applicationsets/applicationset-1-helm.git helm-chart prod
# List application details: Main / Dev
argocd app get argocd/applicationset-1-helm-main
# Shell output:
Name: argocd/applicationset-1-helm-main
Project: default
Server: https://kubernetes.default.svc
Namespace: example-app-dev
URL: https://argocd.jklug.work/applications/applicationset-1-helm-main
Source:
- Repo: git@gitlab.jklug.work:production/argocd-applicationsets/applicationset-1-helm.git
Target: main
Path: helm-chart
Helm Values: values.yaml
SyncWindow: Sync Allowed
Sync Policy: Automated (Prune)
Sync Status: Synced to main (e523084)
Health Status: Healthy
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service example-app-dev example-app-service Synced Healthy service/example-app-service created
apps Deployment example-app-dev example-app Synced Healthy deployment.apps/example-app created
networking.k8s.io Ingress example-app-dev example-app-ingress Synced Healthy ingress.networking.k8s.io/example-app-ingress created
# List application details: Prod
argocd app get argocd/applicationset-1-helm-prod
# Shell output:
Name: argocd/applicationset-1-helm-prod
Project: default
Server: https://kubernetes.default.svc
Namespace: example-app-prod
URL: https://argocd.jklug.work/applications/applicationset-1-helm-prod
Source:
- Repo: git@gitlab.jklug.work:production/argocd-applicationsets/applicationset-1-helm.git
Target: prod
Path: helm-chart
Helm Values: values.yaml
SyncWindow: Sync Allowed
Sync Policy: Automated (Prune)
Sync Status: Synced to prod (bc75e12)
Health Status: Healthy
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service example-app-prod example-app-service Synced Healthy service/example-app-service created
apps Deployment example-app-prod example-app Synced Healthy deployment.apps/example-app created
networking.k8s.io Ingress example-app-prod example-app-ingress Synced Healthy ingress.networking.k8s.io/example-app-ingress created
Kubectl #
Verify Kubernetes Resources #
Dev #
# List default resources
kubectl get all -n example-app-dev
# Shell output:
NAME READY STATUS RESTARTS AGE
pod/example-app-65567f4c96-m9zrc 1/1 Running 0 3m38s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/example-app-service ClusterIP 10.201.53.247 <none> 80/TCP 3m38s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/example-app 1/1 1 1 3m38s
NAME DESIRED CURRENT READY AGE
replicaset.apps/example-app-65567f4c96 1 1 1 3m38s
# List Ingress resource
kubectl get ingress -n example-app-dev
# Shell output:
NAME CLASS HOSTS ADDRESS PORTS AGE
example-app-ingress nginx-metallb example-app-dev.jklug.work 192.168.70.110 80 3m48s
Prod #
# List default resources
kubectl get all -n example-app-prod
# Shell output:
NAME READY STATUS RESTARTS AGE
pod/example-app-6cf8956d48-klwgm 1/1 Running 0 5m20s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/example-app-service ClusterIP 10.201.91.3 <none> 80/TCP 5m20s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/example-app 1/1 1 1 5m20s
NAME DESIRED CURRENT READY AGE
replicaset.apps/example-app-6cf8956d48 1 1 1 5m20s
# List Ingress resource
kubectl get ingress -n example-app-prod
# Shell output:
NAME CLASS HOSTS ADDRESS PORTS AGE
example-app-ingress nginx-metallb example-app-prod.jklug.work 192.168.70.110 80 5m30s