Using CI/CD to deploy web applications on Kubernetes with ArgoCD
Step-by-Step walkthrough for setting up CircleCI and ArgoCD for Deploying applications on Kubernetes
Table of contents
- Prerequisites
- Cloning the Node.js application
- Containerizing the Node.js application
- Configuring Kubernetes manifests for deployment
- Launching the Azure Kubernetes Service (AKS) cluster
- Installing ArgoCD in the AKS Cluster
- Accessing the ArgoCD Web Portal
- Configuring Kubernetes manifests for ArgoCD
- Creating the continuous integration pipeline
- Setting up the project on CircleCI
- Monitoring the application on ArgoCD Dashboard
- Accessing the application on AKS
- Conclusion
Originally published in CircleCI Blog: Using CI/CD to deploy web applications on Kubernetes with ArgoCD by Avik Kundu
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 a 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:
After you have all the prerequisites 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 codecircleci-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 is the targetPort
, port
and type
:
targetPort
is the container portport
is where the application will be runningtype
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 is 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 the 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 to an external IP. Because you will access 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
.
You can access the application at http://<EXTERNAL-IP>
.
In my case, that was http://20.237.108.112/
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 use 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 the 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 can 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 pushes 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 and 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 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
).
When prompted to select your config.yml file, click the Fastest option and type main
the branch name. CircleCI will automatically locate the config.yml
file. Click Set Up Project.
The workflow will run, but will soon display an 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.
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 assign to it.
The environment variables used in the file are:
APP_NAME
: Container Image Name (aks-nodejs-argocd)ARGOCD_PASSWORD
: ArgoCD portal passwordARGOCD_SERVER
: ArgoCD Server IP AddressARGOCD_USERNAME
: ArgoCD portal username (admin)AZURE_PASSWORD
: Azure Account PasswordAZURE_USERNAME
: Azure Account UsernameCLUSTER_NAME
: AKS Cluster Name (NodeCluster)DOCKER_LOGIN
: Docker Hub UsernameDOCKER_PASSWORD
: Docker Hub PasswordGITHUB_EMAIL
: GitHub Account Email AddressRESOURCE_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
.
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]
it 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.
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.
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
.
You can access the application at http://<EXTERNAL-IP>
. In my case, that is http://20.121.253.220/
.
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 configure 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.