Qwik / Helm / LKE / GitHub Kickstart Tutorial

    Written By: Catlin Cox

    Source Code Introduction

    In order to get started quickly, you can go to our GitHub repository and view the complete code. This article will not contain everything from the project and is advised to go look into the code itself to get a full understanding. Starter Repository

    It is expected to have an understanding of Kubernetes and a basic familiarity with Git and the Qwik framework. The project structure is outlined by the following screen shot.

    qwik-framework-lke-project-structure.png

    Let's go over the file structure and outline the purpose of each of the folders associated with the template. We will go from top to bottom of the project structure while explaining the purpose of each folder.

    1. .github - this folder contains all the workflow actions and dependabot configuration for the project.
    2. .husky - this folder contains all the hooks used by husky to perform code formatting along with test verification before allowing the code to be pushed to the remote repository.
    3. adapters - the adapter folder contains the associated code for the deployment adapter being used. Our projects use the express adapter, but more are available at the following. Qwik Deployment Adapters
    4. helm-chart - contains the definition of the Kubernetes manifest used and the templating values that are populated when executing the GitHub action workflow for deployment.
    5. public - contains all the public files, such as manifest, robots.txt and icons that are used by the project.
    6. src - this folder contains all the source code related to the project. You can see there are many other folders within the src folder.
      1. media - contains all the media that is not served by our content delivery system, this can include image, video, files, etc. Qwik optimized these images to be used as JSX for importing and optimization.
      2. module - contains all the separate module information, we use this as a way of sorting components on a per page or functionality basis. Example would be home-page containing a carousel only associated with the home page of the website.
      3. route-action - contains all the route actions that are used, primarily used for form management and form submission. Ultimately it is up to the developer to decide where to put global actions that are available everywhere. We export the actions from the specific layout or index page associated with the action in order to separate the action from the existing layout / index file.
      4. route-loader - similar to the route-action but is used to load the data for a specific route inside the application. These are exported as well from the specific route that is associated with the function.
      5. routes - contains all the actual available routes for the application. Here we define the layouts, pages, and paths that are available to the application.
      6. server-function - contains all the individual server functions defined, any component can call a server function and load data within the server environment to avoid leaking secure information to the client.
      7. shared - contains all the shared components, icons and other helpful utilities that are shared throughout many different paths, routes, components, etc.
      8. support - contains helpers for additional things, example would be a theme-tracker function that handles the changing of the theme server side by using an http cookie.
      9. tailwind.css - the standard stylesheet used to configure tailwind.
      10. entry.dev.tsx - the Vite dev server entry point for running the application locally.
      11. entry.express.tsx - the entry point for running the application in production with the express server adapter.
      12. entry.preview.tsx - the entry point for running the application in an optimized fashion locally.
      13. entry.ssr.tsx - the primary entry point used by every adapter and all optimizations are enabled.
    7. Dockerfile - contains the code to package the application as a docker container for the container registry.

    I think the rest of the files are pretty self-explanatory, we won't cover all of the remaining ones, but if you have question feel free to post it to the GitHub repository.

    The GitHub Workflows & Deployment

    Let's cover the configuration for the GitHub workflows and then lean into how to structure Helm in order to take full advantage of the deployment actions. We will explain the different environment variables that are used inside the workflows to make the actions work correctly.

    Dependabot Configuration

    We use dependabot to automatically create pull request and check for vulnerable libraries or upgrade our dependencies while ensuring that the code is still be executed through the test phase before being allowed to merge from the dependabot branch. Below is our simple configuration that allows dependabot to take action on the repository.

    version: 2
    updates:
      - package-ecosystem: npm
        directory: "/"
        schedule:
          interval: daily
    

    The configuration allows use to scan daily for updated packages that are currently used for the project. The benefit is that we have less overhead dealing with looking up updated packages manually or relying on always executing npm update --save.

    Issue Auto Assign

    We have an action that is associated with auto assigning an individual to pull request and issues. This action is very convenient when it comes to isolating assigning issues to a specific team or group within GitHub. It is also very simple to configure and executes nicely when things arise.

    name: Issue Auto Assign
    on:
      issues:
        types: ["opened"]
      pull_request:
        types: ["opened"]
    jobs:
      run:
        runs-on: ubuntu-latest
        permissions:
          issues: write
          pull-requests: write
        steps:
          - name: "Auto Assign Issue"
            uses: pozil/auto-assign-issue@v1
            with:
              repo-token: ${{ secrets.GITHUB_TOKEN }}
              assignees: catlincox # this should be replaced with your own user.
              numOfAssignee: 1
    

    Standard CI Testing Action

    The below action is executed on every branch and pull request that is opened for the repository. We believe we should always have a testing suite associated with a repository, no matter the language that is being used.

    name: Execute Overall Project Testing & Dependency Scanning
    on:
      push:
        branches: ["*"]
        tags: ["[0-9]+.[0-9]+.[0-9]+"]
      pull_request:
        branches: ["*", "release/*"]
    env:
      HUSKY: 0
    jobs:
      execute_test:
        runs-on: ubuntu-latest
        permissions: write-all
        steps:
          - name: Checkout Repository
            uses: actions/checkout@v4
          - name: Configure Node Runtime
            uses: actions/setup-node@v4
            with:
              node-version: "20.12.2"
              cache: "npm"
          - name: Install Dependencies
            run: npm install
          - name: Run Unit Test
            run: npm run test
    

    Auto Cleanup Workflow Execution

    Often, we find ourselves having to many executed workflows being saved in the pipelines. The following action allows us to clean up the workflows for a project and keep the history clean and tidy.

    name: Cleanup Old Workflow Executions
    on:
      schedule:
        - cron: "0 0 * * *"
    jobs:
      cleanup_workflow_executions:
        runs-on: ubuntu-latest
        permissions:
          actions: write
          contents: read
        steps:
          - name: Delete workflow executions
            uses: Mattraks/delete-workflow-runs@v2
            with:
              token: ${{ github.token }}
              repository: ${{ github.repository }}
              retain_days: 30
              keep_minimum_runs: 5
    

    Actual Deployment Release Action

    The following action is highly specific to how we currently do our deployments. It can be tweaked and changed to fit another need, but we will go over some of the actions that are associated with getting things to work with LKE. We will first show the full code then step through it a bit and explain each section.

    name: Create Image & Deploy Platform with Helm
    on:
      push:
        tags: ["[0-9]+.[0-9]+.[0-9]+"]
    
    env:
      REGISTRY: ghcr.io
      IMAGE_NAME: ${{ github.repository }}
      NAMESPACE: qwik-framework-lke # replace with the actual name of the namespace.
      HELM_RELEASE_NAME: qwik-framework-lke # replace with the name of the helm chart for your application.
      HUSKY: 0
    
    jobs:
      create_image_and_deploy_platform:
        runs-on: ubuntu-latest
    
        permissions:
          contents: read
          packages: write
          id-token: write
          actions: read
          attestations: write # remove this if you don't want to utilize attestation for docker image build.
    
        steps:
          - name: Checkout Repository
            uses: actions/checkout@v4
    
          - name: Container Registry Login
            uses: docker/login-action@v3
            with:
              registry: ${{ env.REGISTRY }}
              username: ${{ github.actor }}
              password: ${{ secrets.GITHUB_TOKEN }}
    
          - name: Extract Metadata (tags, labels) for Docker
            id: meta
            uses: docker/metadata-action@v5
            with:
              images: |
                ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
              tags: |
                type=ref,event=branch
                type=ref,event=pr
                type=semver,pattern={{version}}
                type=semver,pattern={{major}}.{{minor}}
    
          - name: Build Docker Image & Push
            id: push
            uses: docker/build-push-action@v5
            with:
              context: .
              push: ${{ github.event_name != 'pull_request' }}
              tags: ${{ steps.meta.outputs.tags }}
              labels: ${{ steps.meta.outputs.labels }}
    
          - name: Generate Artifact Attestation
            id: attest
            uses: actions/attest-build-provenance@v1
            with:
              subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
              subject-digest: ${{ steps.push.outputs.digest }}
              push-to-registry: ${{ github.event_name != 'pull_request' }}
    
          - name: Setup Kubectl For Deployment
            uses: azure/setup-kubectl@v4
    
          - name: Setup Helm
            uses: azure/setup-helm@v3
            with:
              version: "latest"
    
          - name: Setup Access To Linode LKE
            uses: azure/k8s-set-context@v4
            with:
              kubeconfig: ${{ secrets.KUBECONFIG }}
    
          - name: Check For Namespace
            run: |
              if kubectl get namespace ${{ env.NAMESPACE }} &> /dev/null; then
                echo "Namespace '${{ env.NAMESPACE }}' already exists."
              else
                echo "Creating namespace '${{ env.NAMESPACE }}.'"
                kubectl create namespace ${{ env.NAMESPACE }}
                echo "Namespace '${{ env.NAMESPACE }}' was created successfully"
              fi
    
          - name: Create or Update Pull Secret
            uses: Azure/k8s-create-secret@v4
            with:
              namespace: ${{ env.NAMESPACE }}
              container-registry-url: ${{ env.REGISTRY }}
              container-registry-username: ${{ github.actor }}
              container-registry-password: ${{ secrets.GITHUB_TOKEN }}
              secret-name: regcred
    
          - name: Deploy Platform Application
            run: |
              helm upgrade --install --force ${{ env.HELM_RELEASE_NAME }} ./helm-chart \
                --namespace ${{ env.NAMESPACE }} \
                --create-namespace \
                --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
                --set image.tag=${{ github.ref_name }} \
                --set imagePullSecrets[0].name=regcred \
                --set example.variable=${{ secrets.RANDOM_SECRET }}
    
          - name: Verify Deployment
            run: |
              kubectl get pods -n ${{ env.NAMESPACE }}
              kubectl describe deployment ${{ env.HELM_RELEASE_NAME }} -n ${{ env.NAMESPACE }}
    

    The above action is pretty involved, here is the actual flow of the action and a brief overview of each section.

    1. checkout repository - get the source code for the project.
    2. docker login - perform authentication with docker for the GitHub package registry.
    3. extract metadata - extract information from the docker build, this is tags, labels, and other attributes associated with the docker build.
    4. build docker image - execute the action to actually build the docker image and push to the container registry.
    5. attestation - this is specific to GitHub enterprise, but we verify image in this step ensuring that everything went accordingly.
    6. configure kubectl - configure the usage of the kubectl tool for managing Kubernetes.
    7. configure helm - configure helm to use the latest version.
    8. set k8-context - set the context that the kubectl should use, we use the azure set context action in order to load the kubeconfig from a secret associated with the company.
    9. ensure namespace exist - checks to ensure the namespace exists in the cluster and creates the namespace if it wasn't found.
    10. ensures updated pull-secret - automatically updates the pull secret for the image.
    11. execute helm update - actually deploys the application as helm chart, we set many different values that are overrides from the values.yaml for helm.
    12. verify deployment - check to ensure that the deployment went successfully, and the pods are initializing.

    Well, that pretty much sums up the GitHub action side of things, we will briefly go over the structure of the Helm chart to sum things up.

    Helm Chart Configuration

    Here at Wolfi Solutions we have found that Helm makes it pretty simple to create deployments and group applications together. The templating itself isn't difficult but does take time to get familiar and before more adapt with customizations that can be performed.

    Let's get started having a look.

    The Chart.yaml

    The chart.yaml is the main packaging structure for the helm chart. It is a rather simple configuration file, but has many things you can add to it in addition to what is currently shown.

    apiVersion: v2
    name: qwik-framework-lke
    description: A helm chart that is a template for utilizing with Helm, LKE, GitHub Actions
    type: application
    version: 0.1.0
    appVersion: "1.0.0"
    

    The Values.yaml

    The values.yaml stores the values that can be substituted into the templates. We can also set values like we did in the GitHub action in order to utilize secrets and not have anything exposed inside the repository.

    image:
      repository: ghcr.io/wolfi-technology/qwik-framework-lke
      tag: latest
      pullPolicy: IfNotPresent
    
    imagePullSecrets: []
    
    replicaCount: 1
    
    service:
      type: ClusterIP
      port: 80
      targetPort: 3000
    
    ingress:
      enabled: true
      annotations:
        cert-manager.io/cluster-issuer: letsencrypt-prod
        ingress.kubernetes.io/force-ssl-redirect: "true"
        kubernetes.io/ingress.class: contour
        kubernetes.io/tls-acme: "true"
        projectcontour.io/response-timeout: "120s"
      tls:
        - secretName: qwik-framework-lke-tls
          hosts:
            - lke-example.wolfisolutions.tech
      hosts:
        - host: lke-example.wolfisolutions.tech
          paths:
            - path: /
              pathType: Prefix
              port: 80
    certificate:
      enabled: true
      duration: "2160h"
      renewBefore: "360h"
      organizations:
        - "Wolfi Solutions LLC"
      dnsNames:
        - lke-example.wolfisolutions.tech
      issuerRef:
        name: letsencrypt-prod
        kind: ClusterIssuer
      secretName: qwik-framework-lke-tls
    
    example:
      variable: "I am just a place holder as an example!!!"
    

    The _helpers.tpl

    The _helpers.tpl file contains simple helper functions that can be used to resolve values. If you are new to Helm, I would suggest looking deeper into the documentation to get an idea of what all can be done with helpers. The helper file helps us create readable names and versions for the chart and the application. You can view our current one below, note where you are seeing the qwik-framework-lke value can be replaced with your actual application name. That is what the helpers will look for when replacing values.

    {{/*
    Expand the name of the chart.
    */}}
    {{- define "qwik-framework-lke.name" -}}
    {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
    {{- end }}
    
    {{/*
    Create a default fully qualified app name.
    We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
    If release name contains chart name it will be used as a full name.
    */}}
    {{- define "qwik-framework-lke.fullname" -}}
    {{- if .Values.fullnameOverride }}
    {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
    {{- else }}
    {{- $name := default .Chart.Name .Values.nameOverride }}
    {{- if contains $name .Release.Name }}
    {{- .Release.Name | trunc 63 | trimSuffix "-" }}
    {{- else }}
    {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
    {{- end }}
    {{- end }}
    {{- end }}
    
    {{/*
    Create chart name and version as used by the chart label.
    */}}
    {{- define "qwik-framework-lke.chart" -}}
    {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
    {{- end }}
    
    {{/*
    Common labels
    */}}
    {{- define "qwik-framework-lke.labels" -}}
    helm.sh/chart: {{ include "qwik-framework-lke.chart" . }}
    {{ include "qwik-framework-lke.selectorLabels" . }}
    {{- if .Chart.AppVersion }}
    app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
    {{- end }}
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    {{- end }}
    
    {{/*
    Selector labels
    */}}
    {{- define "qwik-framework-lke.selectorLabels" -}}
    app.kubernetes.io/name: {{ include "qwik-framework-lke.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    {{- end }}
    

    Time To Dive into Manifest

    Getting started we will be having a look at the deployment.yaml manifest. It is rather simplistic and easy to understand what is going on with the content.

    deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {{ include "qwik-framework-lke.fullname" . }}
      labels: {{- include "qwik-framework-lke.labels" . | nindent 4 }}
    spec:
      replicas: {{ .Values.replicaCount }}
      selector:
        matchLabels: {{- include "qwik-framework-lke.selectorLabels" . | nindent 6 }}
      template:
        metadata:
          labels: {{- include "qwik-framework-lke.selectorLabels" . | nindent 8 }}
        spec:
          imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
          containers:
            - name: {{ .Chart.Name }}
              image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
              imagePullPolicy: {{ .Values.image.pullPolicy }}
              env:
                - name: EXAMPLE_ENVIRONMENT_VARIABLE
                  value: {{ .Values.example.variable }}
    
    service.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: {{ include "qwik-framework-lke.fullname" . }}
      labels: {{- include "qwik-framework-lke.labels" . | nindent 4 }}
    spec:
      type: {{ .Values.service.type }}
      ports:
        - port: {{ .Values.service.port }}
          targetPort: {{ .Values.service.targetPort }}
          protocol: TCP
          name: http
      selector: {{- include "qwik-framework-lke.selectorLabels" . | nindent 4 }}
    
    ingress.yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: {{ include "qwik-framework-lke.fullname" . }}-ingress
      annotations:
        {{- with .Values.ingress.annotations }}
        {{- toYaml . | nindent 4 }}
        {{- end }}
    spec:
      {{- if .Values.ingress.tls }}
      tls:
        {{- range .Values.ingress.tls }}
        - hosts:
            {{- range .hosts }}
            - {{ . | quote }}
            {{- end }}
          secretName: {{ .secretName }}
        {{- end }}
      {{- end }}
      rules:
        {{- range .Values.ingress.hosts }}
        - host: {{ .host | quote }}
          http:
            paths:
              {{- range .paths }}
              - path: {{ .path }}
                pathType: {{ .pathType }}
                backend:
                  service:
                    name: {{ include "qwik-framework-lke.fullname" $ }}
                    port:
                      number: {{ .port }}
              {{- end }}
        {{- end }}
    
    certificate.yaml

    Now with the certificate we utilize cert-manager in order to inject the certificate for the ingress. We currently use project contour as our ingress controller. The certificate might be specific to our use case but gives an idea of how to template the resource.

    {{- if .Values.certificate.enabled -}}
    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: {{ include "qwik-framework-lke.fullname" . }}-tls
      labels:
        {{- include "qwik-framework-lke.labels" . | nindent 4 }}
    spec:
      duration: {{ .Values.certificate.duration | quote }}
      renewBefore: {{ .Values.certificate.renewBefore | quote }}
      subject:
        organizations:
          {{- range .Values.certificate.organizations }}
          - {{ . | quote }}
          {{- end }}
      dnsNames:
        {{- range .Values.certificate.dnsNames }}
        - {{ . | quote }}
        {{- end }}
      issuerRef:
        name: {{ .Values.certificate.issuerRef.name }}
        kind: {{ .Values.certificate.issuerRef.kind }}
      secretName: {{ .Values.certificate.secretName }}
    {{- end }}
    

    Final Conclusion:

    In summary GitHub has made it easier for us to deploy our services to the cluster. A simple time with deployment and modification is a must for feature / product delivery. If you stuck around this long, thank you for reading and if you want to join our newsletter use the form below. Once again, all this code can be found in the following repository.

    Starter Repository

    Thanks for reading and keep on writing code :)

    Want to get notified with new content?

    Sign up for our newsletter.

    Related Article Post

    Mastering the Intersection Observer API: Boost Web Performance with Qwik

    Mastering the Intersection Observer API: Boost Web Performance with Qwik

    This article explores the Intersection Observer API, a powerful tool for efficiently tracking element visibility in web applications. It explains the API's key features and benefits, including improved performance and simplified code. The guide then demonstrates how to integrate the Intersection Observer API with the Qwik framework to create smooth, responsive user interfaces with a practical example of implementing a fade-in effect. By combining these technologies, developers can create more engaging and performance-oriented web experiences.

    Site Load Speed Benefits

    Site Load Speed Benefits

    In today's digital age, having a strong online presence is crucial for the success of any business. Two key factors that significantly impact a website's performance and visibility are site loading speed and search engine optimization (SEO) practices.

    The Effects of Toxic Agile

    The Effects of Toxic Agile

    The Agile software development methodology has been around for a long time; however, the current state of what companies perceive as Agile methodology is completely incorrect. Let's discuss some toxic work environments that pertain to companies attempting to adopt the so-called "AGILE".