Skip to main content

Argo CD with GitLab CI, Part 2: GitLab Repositories (Source Code and Pipeline Repository, Helm Chart Repository), Deploy Key for CI Pipeline

1109 words·
Argo CD Argo CD CLI GitLab GitLab CI CI Pipeline Kubernetes CoreDNS Python Flask
ArgoCD - This article is part of a series.
Part 2: This Article



GitLab Repository: Flask App - Code & Pipeline
#

File & Folder Structure
#

flask-app-code
├── Dockerfiles
│   └── Dockerfile
├── flask-app  # Python Flask web application
│   ├── app.py
│   └── requirements.txt
├── .gitlab-ci.yml  # GitLab CI Pipeline manifest
└── README.md

CI Pipeline Manifest
#

  • .gitlab-ci.yml
### Variables
variables:
  # Define the image name, tagging it with the GitLab CI registry and the current commit SHA
  IMAGE_SHA: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
  # Repository URL of the "Flask Web Application Manifest" project
  GIT_REPO_URL: "git@gitlab.jklug.work:python/flask-app-helm.git"


### Stages
stages:
  - build
  - update_helm_chart


### Build Container Image
build_image:
  image: docker:stable
  stage: build
  services:
    - docker:dind
  variables:
    DOCKER_TLS_CERTDIR: ""
  before_script:
    # Login to GitLab Container Registry using predefined CI/CD variables
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    # Build the Docker image from the specified Dockerfile in the Dockerfiles directory
    - docker build --pull -t $IMAGE_SHA -f Dockerfiles/Dockerfile .
    # Push the built Docker image to the GitLab Container Registry
    - docker push $IMAGE_SHA
    # Print the image name
    - echo $IMAGE_SHA
  rules: 
    # Rule: Run this job only for the main branch and if the specified Dockerfile exists
    - if: $CI_COMMIT_BRANCH == "main"
      exists:
        - Dockerfiles/Dockerfile


### Update Helm Chart
update_helm_chart:
  image: alpine:latest
  stage: update_helm_chart
  needs:
    # Run this job only if the 'build_image' job succeeds
    - build_image
  variables:
    HELM_REPO_BRANCH: "main"  # Branch of the external repository to update
  script:
    # Install required tools
    - apk add --no-cache yq git openssh
    # Configure SSH for Git
    - mkdir -p ~/.ssh
    - echo "$pipeline_key" > ~/.ssh/id_rsa  # Use the correct variable name
    - chmod 600 ~/.ssh/id_rsa
    - ssh-keyscan -H gitlab.jklug.work >> ~/.ssh/known_hosts
    # Clone the external repository
    - git clone --branch $HELM_REPO_BRANCH $GIT_REPO_URL helm-chart-repo
    - cd helm-chart-repo
    # Debug repository contents
    - ls -R
    - ls -R helm-chart
    # Update the container image in the Helm chart's values.yaml
    - yq e ".image.repository = \"$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG\" | .image.tag = \"$CI_COMMIT_SHA\"" -i helm-chart/values.yaml
    # Add and commit the changes
    - git config --global user.email "ci@example.com"
    - git config --global user.name "GitLab CI"
    - git add helm-chart/values.yaml
    - git commit -m "Update Helm chart with image $IMAGE_SHA"
    # Push the changes back to the external repository
    - git push origin $HELM_REPO_BRANCH
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Dockerfile
#

  • Dockerfiles/Dockerfile
### Stage 1: Compile Python Application

# Use Python-slim image as the base image / Label image as "builder"
FROM python:3.13-slim AS builder

# Set the working directory inside the container to /app
WORKDIR /app

# Copy the contents of the "flask-app" directory to "/app" inside the container
ADD flask-app/. /app

# Install the dependencies listed in "requirements.txt"
RUN pip install -r requirements.txt

# Compile all Python files to bytecode and move the desired one to /app as app.pyc
RUN python -m compileall .



### Stage 2: Final Container with Bytecode

# Use a lightweight Python image for the final stage
FROM python:3.13-rc-alpine

# Copy requirements.txt from builder
COPY --from=builder /app/requirements.txt /tmp/   

# Install app requirements
RUN pip install -r /tmp/requirements.txt

# Create system user "appuser" / no PW
RUN adduser -S -D -H -h /app appuser

# Switch to "appuser"
USER appuser

# Copy compiled .pyc files and any necessary files from the builder stage
COPY --from=builder /app/__pycache__/* /app/

# Define working directory
WORKDIR /app

# Expose port 8080 for the application
EXPOSE 8080

# Execute Bytecode when the container starts
ENTRYPOINT ["python", "/app/app.cpython-313.pyc"]

Python Code
#

Flask Application: app.py
#

  • flask-app/app.py
# Import the Flask class from the Flask library
from flask import Flask

 # Create an instance of the Flask application
app = Flask(__name__)


# Define a route for the root URL ("/")
@app.route("/")
def main():   # Define a function that will execute when the root URL is accessed
    return "Hi there from Python Flask application\n"  # Return a simple string response to the client.


# Start the Flask app on all available network interfaces (0.0.0.0) and port 8080
app.run(host='0.0.0.0', port=8080)

Requirements: requirements.txt
#

  • flask-app/requirements.txt
# Flask web framework version 3.1.0
Flask==3.1.0



GitLab Repository: Flask App - Manifest
#

File & Folder Structure
#

flask-app-helm
├── helm-chart  # Helm chart
│   ├── Chart.yaml
│   ├── templates
│   │   ├── deployment.yaml
│   │   └── service.yaml
│   └── values.yaml
└── README.md

Helm Chart
#

deployment.yaml
#

  • helm-chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-app
  labels:
    app: flask-app
spec:
  replicas: 2  # Number of replicas
  selector:
    matchLabels:
      app: flask-app
  template:
    metadata:
      labels:
        app: flask-app
    spec:
      imagePullSecrets:
      - name: "{{ .Values.imagePullSecrets | first }}"  # Correctly access the first item in the array
      containers:
      - name: flask-app
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        ports:
        - containerPort: 8080
        env:
        - name: FLASK_ENV
          value: "production"

service.yaml
#

  • helm-chart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: flask-app
spec:
  type: ClusterIP
  ports:
    - port: 8080
      targetPort: 8080
  selector:
    app: flask-app

values.yaml
#

  • helm-chart/values.yaml
# Define the image details
image:
  repository: "gitlab-registry.jklug.work/python/flask-app-code/main"  # GitLab Registry; will be updated by the pipeline
  tag: "latest"  # Tag placeholder; will be updated by the pipeline

imagePullSecrets:
  - gitlab-registry-secret  # The Kubernetes secret name

Chart.yaml
#

  • helm-chart/Chart.yaml
apiVersion: v2
name: flask-web-app
description: A Helm chart for deploying a Flask web application
type: application
version: 0.1.0
appVersion: 1.0.0



GitLab Deploy Key: For CI Pipeline
#

Create SSH Key Pair
#

Generate a SSH key pair named pipeline_key and pipeline_key.pub in the current directory:

# Create SSH RSA key pair: 4096 bit
ssh-keygen -t rsa -b 4096 -f pipeline_key
# Copy the public SSH key
cat pipeline_key.pub

Add Public Key to Helm Project
#

Add the public SSH key to the GitLab Helm chart project flask-app-helm:

  • Go to: (Project) “Settings” > “Repository”

  • Expand the “Deploy keys” section

  • Click “Add new key”

  • Paste the value of the public SSH key pipeline_key.pub

  • Define a title like “Pipeline-key”

  • Select “Grant write permissions to this key”

  • Click “Add key”


Add Private Key to Pipeline Project
#

Add the private SSh key as variable to the GitLab code and pipeline project `flask-app-code:

Go to: (Project) “Settings” > “CI/CD”

Expand the “Variables” section

Click “Add variable”

Select type: “Variable (default)”

Unflag “Protect variable”

Define a key name like pipeline_key

Paste the value of the private key pipeline_key

Click “Add variable”



Verify Image Name
#

After the pipeline jobs run through, verify the correct image name.

Job Logs
#

Verify the job logs of the “flask-app-code” project:

  • Go to: (Project) “Build” > “Jobs”

  • Select the build_image job

  • Copy the image name from the job logs:

# Copy the image name
$ echo $IMAGE_SHA
gitlab-registry.jklug.work/python/flask-app-code/main:9ae1cd23b7ba9ad472014389730096980cac3fa8

Helm Chart values.yaml
#

The values.yaml manifest in the “flask-app-helm” project should now look like this:

  • helm-chart/values.yaml
# Define the image details
image:
  repository: "gitlab-registry.jklug.work/python/flask-app-code/main" # Define GitLab Registry
  tag: "9ae1cd23b7ba9ad472014389730096980cac3fa8" # Placeholder; will be updated by the pipeline
imagePullSecrets:
  - name: gitlab-registry-secret
ArgoCD - This article is part of a series.
Part 2: This Article