GitOps modernizes software management and operations by allowing developers to declaratively manage infrastructure and code using a single source of truth, usually a Git repository. Many development teams and organizations have adopted GitOps procedures to improve the creation and delivery of software applications.

For a GitOps initiative to work, an orchestration system like Kubernetes is crucial. The number of incompatible technologies needed to develop software makes Kubernetes a key tool for managing infrastructure. Without Kubernetes, implementing infrastructure-as-code (IaC) procedures is inefficient or even impossible. Fortunately, the wide adoption of Kubernetes has enabled the creation of tools for implementing GitOps.

One of these tools, ArgoCD, is a Kubernetes-native continuous deployment (CD) tool. It can deploy code changes directly to Kubernetes resources by pulling it from Git repositories instead of an external CD solution. Many of these solutions support only push-based deployments. Using ArgoCD gives developers the ability to control application updates and infrastructure setup from an unified platform. It handles the latter stages of the GitOps process, ensuring that new configurations are correctly deployed to a Kubernetes cluster.

In this tutorial, you will learn how to deploy a Node.js application on Azure Kubernetes Service (AKS) using a CI/CD pipeline and ArgoCD.

Prerequisites

To follow along with this tutorial, you will need a few things first.

Accounts for:

These tools installed on your system:

Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.

After you have all the pre-requisites complete you are ready to go to the next section.

Cloning the Node.js application

In this tutorial, the main focus is on deploying the application on Kubernetes. You can directly clone the Node.js application to your GitHub and continue with the rest of the process.

To clone the project, run:

git clone https://github.com/CIRCLECI-GWP/aks-nodejs-argocd.git

There are 2 branches in this repository:

  • main branch contains only the Node.js Application code
  • circleci-project-setup branch contains the application codes along with all YAML files that you will create

Check out to the main branch.

The Node.js application lives in the app.js file and contains:

const express = require("express");
const path = require("path");
const morgan = require("morgan");
const bodyParser = require("body-parser");
/* eslint-disable no-console */
const port = process.env.PORT || 1337;
const app = express();
app.use(morgan("dev"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: "true" }));
app.use(bodyParser.json({ type: "application/vnd.api+json" }));
app.use(express.static(path.join(__dirname, "./")));
app.get("*", (req, res) => {
  res.sendFile(path.join(__dirname, "./index.html"));
});
app.listen(port, (err) => {
  if (err) {
    console.log(err);
  } else {
    console.log(`App at: http://localhost:${port}`);
  }
});
module.exports = app;

The key takeaway from this code is the port number, This is where the application will be running, which is 1337 for this tutorial.

You can run the application locally by first installing the dependencies. In the project’s root, type:

npm install

Then run the application with the command:

node app.js

The application should now be up and running at the address http://localhost:1337.

Containerizing the Node.js application

To deploy the application of Kubernetes you need to containerize it. To containerize applications using Docker as the container runtime tool, you will create a Dockerfile. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.

Create a new file in the root directory of the project and name it Dockerfile. Copy the following content in the file:

# Set the base image to use for subsequent instructions
FROM node:alpine
# Set the working directory for any subsequent ADD, COPY, CMD, ENTRYPOINT,
# or RUN instructions that follow it in the Dockerfile
WORKDIR /usr/src/app
# Copy files or folders from source to the dest path in the image's filesystem.
COPY package.json /usr/src/app/
COPY . /usr/src/app/
# Execute any commands on top of the current image as a new layer and commit the results.
RUN npm install --production
# Define the network ports that this container will listen to at runtime.
EXPOSE 1337
# Configure the container to be run as an executable.
ENTRYPOINT ["npm", "start"]

If you have Docker installed, you can build and run the container locally for testing. Later on in this tutorial, you will learn how to automate this process with CircleCI orbs.

To build and tag the container, enter:

docker build -t aks-nodejs-argocd:latest .

Confirm that the image was successfully created by running this command from your terminal:

docker images

Then run the container using the command:

docker run -it -p 1337:1337 aks-nodejs-argocd:latest

The application should now be up and running at the address http://127.0.0.1:1337.

Commit and push the changes to your GitHub repository.

Configuring Kubernetes manifests for deployment

To deploy containers on Kubernetes, you will have to configure Kubernetes to incorporate all the settings required to run your application. Kubernetes uses YAML for configuration.

Create a directory named manifests in the root directory of the project. Then create these files in the newly created folder:

  • namespace.yaml
  • deployment.yaml
  • service.yaml
  • kustomization.yaml

In Kubernetes, namespaces provide a mechanism for isolating groups of resources within a single cluster. The contents of the namespace.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name: nodejs
  labels:
    name: nodejs

This file would create a namespace named nodejs inside the Kubernetes cluster. All the resources would be created in this namespace.

Kubernetes Deployments manage stateless services running on your cluster. Their purpose is to keep a set of identical pods running and upgrade them in a controlled way – performing a rolling update by default. The contents of the deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nodejs
  namespace: nodejs
  labels:
    app: nodejs
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nodejs
  template:
    metadata:
      labels:
        app: nodejs
    spec:
      nodeSelector:
        "beta.kubernetes.io/os": linux
      containers:
        - name: aks-nodejs-argocd
          image: aks-nodejs-argocd
          ports:
            - name: http
              containerPort: 1337

The key takeaway from this code is the containerPort. This is where the application will be running and where the container-image will be pulled and deployed in the namespace on the Kubernetes cluster.

Kubernetes Service is an abstraction that defines a logical set of pods and a policy for accessing them. You need the Kubernetes Service type LoadBalancer to make the deployment accessible to the outside world.
The contents of the service.yaml are:

apiVersion: v1
kind: Service
metadata:
  name: nodejs
  namespace: nodejs
  labels:
    app: nodejs
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 1337
  selector:
    app: nodejs

The key takeaway from this code are the targetPort, port and type:

  • targetPort is the container port
  • port is where the application will be running
  • type is the type of service

To deploy the latest version of the application on the Kubernetes cluster, the resources have to be customized to maintain the updated information. You can use Kustomize, which is a tool for customizing Kubernetes configurations.

The contents of the kustomization.yaml are:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
  - namespace.yaml
namespace: nodejs
images:
  - name: aks-nodejs-argocd
    newName: aks-nodejs-argocd
    newTag: v1

The key takeaway from this code are newName and newTag, which will be updated with the latest Docker image information as part of the continuous integration process.

Commit and push these files into the main branch of the GitHub repository you had cloned earlier.

Launching the Azure Kubernetes Service (AKS) cluster

In this tutorial, you will be deploying the application on AKS cluster. To create the AKS cluster, the Azure CLI should be connected to your Azure account.

To launch an AKS cluster using the Azure CLI, create a Resource Group with this command:

az group create --name NodeRG --location eastus

Launch a two-node cluster:

az aks create --resource-group NodeRG --name NodeCluster --node-count 2 --enable-addons http_application_routing

Note: If you generated any SSH keys in your system previously, you need to add the optional --generate-ssh-keys parameter to this command. This auto-generates SSH public and private key files if they are missing. The keys are stored in the ~/.ssh directory.

The AKS cluster will take 10 to 15 minutes to launch.

Installing ArgoCD in the AKS Cluster

Once the cluster is up and running, you can install ArgoCD inside the cluster. You will use ArgoCD for deploying your application.

To install the application, use the Azure CLI. Configure kubectl to connect to AKS using this command:

az aks get-credentials --resource-group NodeRG --name NodeCluster

To install ArgoCD, use these commands:

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

ArgoCD will be installed in the argocd namespace. To get all the resources in the namespace enter:

kubectl get all --namespace argocd

Exposing the ArgoCD API server

By default, the ArgoCD API server is not exposed with an external IP. Because you will accessing the application from the internet during this tutorial, you need to expose the ArgoCD server with an external IP via Service Type Load Balancer.

Change the argocd-server service type to LoadBalancer:

kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'

Note: You can also use Kubectl port forwarding to connect to the API server without exposing the service. Use this command: kubectl port-forward svc/argocd-server -n argocd 8080:443

You can now access the API server using https://localhost:8080.

Accessing the ArgoCD Web Portal

Once you have exposed the ArgoCD API server with an external IP, you can now access the portal with the external IP address you generated.

ArgoCD is installed in the argocd namespace. Use this command to get all the resources in the namespace:

kubectl get all --namespace argocd

Copy the External-IP corresponding to service/argocd-server.

External-IP

You can access the application at http://<EXTERNAL-IP>.
In my case, that was http://20.237.108.112/

ArgoCD application

To log into the portal, you will need the username and password. The username is set as admin by default.

To fetch the password, execute this command:

kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo

Use this username/password combination to log into the ArgoCD portal.

Configuring Kubernetes manifests for ArgoCD

To configure ArgoCD to deploy your application on Kubernetes, you will have to set up ArgoCD to connect the Git Repository and Kubernetes in a declarative way using YAML for configuration.

Apart from this method, you can also set up ArgoCD from the Web Portal or using the ArgoCD CLI. Because this tutorial is following GitOps principles, we are using the Git repository as the sole source of truth. Therefore the declarative method using YAML files works best.

One of the key features and capabilities of ArgoCD is to sync via manual or automatic policy for deployment of applications to a Kubernetes cluster.

To get started, create a directory named argocd in the root directory of the project. Create a new file in the new directory and name it config.yaml.

Manual Sync Policy

Use this policy to manually synchronize your application by way of your CI/CD pipelines. Whenever a code change is made, the CI/CD pipeline is triggered, and calls the ArgoCD server APIs to start the sync process based on the changes you will commit. For communicating with the ArgoCD server APIs, you can use the ArgoCD CLI. You can also use one of the SDKs available for various programming languages.

For setting up the Manual Sync policy for ArgoCD, paste this in the config.yaml:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: aks-nodejs-argocd
  namespace: argocd
spec:
  destination:
    namespace: nodejs
    server: "https://kubernetes.default.svc"
  source:
    path: manifests
    repoURL: "https://github.com/Lucifergene/aks-nodejs-argocd"
    targetRevision: circleci-project-setup
  project: default

Automated Sync policy

ArgoCD has the ability to automatically sync an application when it detects differences between the desired manifests in Git, and the live state in the cluster.

A benefit of automatic sync is that CI/CD pipelines no longer need direct access to the ArgoCD API server to perform the deployment. Instead, the pipeline makes a commit and push to the Git repository with the changes to the manifests in the tracking Git repo.

If you want to set to the Automated Sync policy, you need to paste this in the config.yaml

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: aks-nodejs-argocd
  namespace: argocd
spec:
  destination:
    namespace: nodejs
    server: "https://kubernetes.default.svc"
  source:
    path: manifests
    repoURL: "https://github.com/Lucifergene/aks-nodejs-argocd"
    targetRevision: circleci-project-setup
  project: default
  syncPolicy:
    automated:
      prune: false
      selfHeal: false

Commit and push these files into the main branch of the GitHub repository you cloned earlier.

Creating the continuous integration pipeline

The objective of this tutorial is to show how you can deploy applications on Kubernetes through continuous integration (CI) using CircleCI and continuous deployment (CD) via ArgoCD. The CI pipeline should trigger the process of building the container, pushing it to Docker Hub, and the CD should deploy the application on Kubernetes.

To create the CI pipeline, you will be using CircleCI integrated with your GitHub account. CircleCI configuration is named config.yml and lives in the .circleci directory in the project’s root folder. The path to the configuration is .circleci/config.yml.

The content of config.yml is:

version: 2.1

orbs:
  docker: circleci/docker@2.1.1
  azure-aks: circleci/azure-aks@0.3.0
  kubernetes: circleci/kubernetes@1.3.0

jobs:
  argocd-manual-sync:
    docker:
      - image: cimg/base:stable
    parameters:
      server:
        description: |
          Server IP of of ArgoCD
        type: string
      username:
        description: |
          Username for ArgoCD
        type: string
      password:
        description: |
          Password for ArgoCD
        type: string
    steps:
      - run:
          name: Install ArgoCD CLI
          command: |
            URL=https://<< parameters.server >>/download/argocd-linux-amd64
            [ -w /usr/local/bin ] && SUDO="" || SUDO=sudo
            $SUDO curl --insecure -sSL -o /usr/local/bin/argocd $URL
            $SUDO chmod +x /usr/local/bin/argocd
      - run:
          name: ArgoCD CLI login
          command: argocd login << parameters.server >> --insecure --username << parameters.username >> --password << parameters.password >>
      - run:
          name: Manual sync
          command: argocd app sync $APP_NAME
      - run:
          name: Wait for application to reach a synced and healthy state
          command: argocd app wait $APP_NAME

  argocd-configure:
    executor: azure-aks/default
    parameters:
      cluster-name:
        description: |
          Name of the AKS cluster
        type: string
      resource-group:
        description: |
          Resource group that the cluster is in
        type: string
    steps:
      - checkout
      - run:
          name: Pull Updated code from repo
          command: git pull origin $CIRCLE_BRANCH
      - azure-aks/update-kubeconfig-with-credentials:
          cluster-name: << parameters.cluster-name >>
          install-kubectl: true
          perform-login: true
          resource-group: << parameters.resource-group >>
      - kubernetes/create-or-update-resource:
          resource-file-path: argocd/config.yaml

  bump-docker-tag-kustomize:
    docker:
      - image: cimg/base:stable
    steps:
      - run:
          name: Install kustomize
          command: |
            URL=https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/v4.5.2/kustomize_v4.5.2_linux_amd64.tar.gz
            curl -L $URL | tar zx
            [ -w /usr/local/bin ] && SUDO="" || SUDO=sudo
            $SUDO chmod +x ./kustomize
            $SUDO mv ./kustomize /usr/local/bin
      - checkout
      - run:
          name: Bump Docker Tag
          command: |
            cd manifests
            kustomize edit set image $APP_NAME=$DOCKER_LOGIN/$APP_NAME:$CIRCLE_SHA1
      - add_ssh_keys:
          fingerprints:
            - "$SSH_FINGERPRINT"
      - run:
          name: Commit & Push to GitHub
          command: |
            git config user.email "$GITHUB_EMAIL"
            git config user.name "CircleCI User"
            git checkout $CIRCLE_BRANCH           
            git add manifests/kustomization.yaml
            git commit -am "Bumps docker tag [skip ci]"
            git push origin $CIRCLE_BRANCH

workflows:
  Deploy-App-on-AKS:
    jobs:
      - docker/publish:
          image: $DOCKER_LOGIN/$APP_NAME
          tag: $CIRCLE_SHA1,latest
      - bump-docker-tag-kustomize:
          requires:
            - docker/publish
      - argocd-configure:
          cluster-name: $CLUSTER_NAME
          resource-group: $RESOURCE_GROUP
          requires:
            - bump-docker-tag-kustomize
      # Paste the following only when you opt for the ArgoCD manual-sync-policy:
      - argocd-manual-sync:
          server: $ARGOCD_SERVER
          username: $ARGOCD_USERNAME
          password: $ARGOCD_PASSWORD
          requires:
            - argocd-configure

The CI workflow consists of three jobs:

  • docker/publish builds and pushes the container to Docker Hub.
  • bump-docker-tag-kustomize updates the Docker Image Tag and generates a consolidated Kubernetes configuration file.
  • argocd-configure applies the ArgoCD Configuration on the AKS cluster.
  • argocd-manual-sync is needed only when you will be opting for the manual sync policy. For automatic sync, you can omit this job from the file.

In this workflow, we have extensively used CircleCI orbs. Orbs are open-source, shareable packages of parameterizable, reusable configuration elements, including jobs, commands, and executors. The orbs have been used directly or are used in creating custom jobs.

Commit and push the changes to your GitHub repository.

Setting up the project on CircleCI

The next step to deploying your application to AKS is connecting the application in your GitHub repository to CircleCI.

Go to your CircleCI dashboard and select the Projects tab on the left panel. Click the Set Up Project button for the GitHub repository containing the code (aks-nodejs-argocd).

CircleCI Dashboard

When prompted to select your config.yml file, click the Fastest option and type main as the branch name. CircleCI will automatically locate the config.yml file. Click Set Up Project.

Select your config.yml file

The workflow will run, but will soon display a status of Failed. This is because you need to set up a user key and configure the environment variables.

To set up the user key, go to Project Settings and click SSH Keys from the left panel. In the User Key section, click Authorize with GitHub. The user key is needed by CircleCI to push changes to your GitHub account on behalf of the repository owner during the execution of the workflow.

User key

To configure the environment variables, click Environment Variables. Select the Add Environment Variable option. On the next screen, type the environment variable and the value you want to assigned to it.

Environment Variables

The environment variables used in the file are:

  • APP_NAME : Container Image Name (aks-nodejs-argocd)
  • ARGOCD_PASSWORD : ArgoCD portal password
  • ARGOCD_SERVER : ArgoCD Server IP Address
  • ARGOCD_USERNAME : ArgoCD portal username (admin)
  • AZURE_PASSWORD : Azure Account Password
  • AZURE_USERNAME : Azure Account Username
  • CLUSTER_NAME : AKS Cluster Name (NodeCluster)
  • DOCKER_LOGIN : Docker Hub Username
  • DOCKER_PASSWORD : Docker Hub Password
  • GITHUB_EMAIL : GitHub Account Email Address
  • RESOURCE_GROUP : AKS Resource Group (NodeRG)
  • SSH_FINGERPRINT : SSH Fingerprint of User Key used for pushing the updated Docker tag to GitHub

To locate the SSH Fingerprint, go to Project Settings and select SSH Keys from the sidebar. Scroll down to the User Key section and copy the key.

Re-run the workflow. This time the status will show Success.

Success workflow

You will also find another pipeline having the status as Not Run. That is because you have explicitly instructed CircleCI to skip the pipeline by including [skip ci] in the commit message. When CircleCI commits the updated configuration files to GitHub, [skip ci] prevents a self-triggering loop of the workflow.

Monitoring the application on ArgoCD Dashboard

A status that shows Success when the workflow is re-run means that the application has been deployed on the AKS cluster.

To observe and monitor the resources that are currently running on the AKS Cluster, log in to the ArgoCD Web Portal.

Earlier in this tutorial, you learned how to fetch the ArgoCD Server IP, username, and password for logging in to the portal. After logging in, you will be on the Applications page.

ArgoCD Application

Click the application name. You will be redirected to a page with the tree view of all resources running on the AKS Cluster and their real-time status.

ArgoCD App Tree view

Accessing the application on AKS

To access the application, you need the external IP address of the cluster. You can use the Azure CLI to find the External-IP.

Configure kubectl to connect to AKS using this command:

az aks get-credentials --resource-group NodeRG --name NodeCluster

You created all the resources in the nodejs namespace. To get all the resources in that namespace, use this command:

kubectl get all --namespace nodejs

Copy the External-IP corresponding to service/nodejs.

External-IP

You can access the application at http://<EXTERNAL-IP>. In my case, that is http://20.121.253.220/.

Final application

Conclusion

In this tutorial, you learned how to deploy your applications continuously on a Kubernetes cluster following GitOps practices with ArgoCD. This included configuring an automated CI pipeline. With the pipeline properly configured, any changes made to the application code are instantly updated on the application URL. Say goodbye to manually configuring and deploying applications on Kubernetes.

As a bonus, you can change the values of the environment variables to use the CircleCI configuration file for similar applications.

The complete source code for this tutorial can also be found here on GitHub.


Avik Kundu is a Software Engineer at Red Hat. He is a full-stack developer, opensource contributor, and tech content creator proficient in DevOps and Cloud. He is an AWS Community Builder and Microsoft Learn Student Ambassador. He has written articles and tutorials on various tools and technologies on Medium, Dev and Opensource.com. He loves to learn new technologies and share his knowledge in public.

Read more posts by Avik Kundu