Software development is changing rapidly. On one hand, you must quickly adapt to evolving requirements, while on the other, your applications need to operate continuously without downtime.

DevOps helps you quickly adapt to changes. Among other initiatives, continuous integration (CI) and continuous delivery (CD) are integral to any DevOps practice.

With CI, developers can merge their code with others’ through automated build and test. The developer commits their code to the source control system, which is usually Git. The code commit triggers an automated build operation, followed by unit tests. The new code then merges into the main branch. You can fix any problems quickly when building and testing after incremental code changes. This also helps resolve any issues, including merge conflicts and tricky bugs, as you integrate the codebase in small steps.

Continuous delivery goes further by enabling you to deploy the application right after the build, provided the test suite has passed. You can adapt to rapidly changing demands and ensure the application is functioning with minimum downtime.

While DevOps is very useful in developing and deploying software, using Git combined with CI/CD is useful beyond the world of software engineering. The idea of applying these principles to non-software endeavors is known as GitOps.

One of the most common uses of GitOps is provisioning cloud infrastructure using infrastructure-as-code (IaC) tools.

Using GitOps with IaC

As described above, DevOps workflows aim to build and deploy software. But before DevOps can work, we need a place to deploy software to. Applications require an environment comprising compute, storage, and network services. During the development lifecycle, you use many environments such as development, staging, and production. Traditionally, each environment has its configuration, often changing over time, causing so-called environment drift. After this, configurations cannot be automatically reproduced, causing hard-to-track errors and requiring manual maintenance.

To solve this problem, you can use infrastructure as code (IaC). With IaC, you create a declaration of the infrastructure you want using tools like Terraform, CloudFormation, and Azure Resource Manager templates, then keep this declaration in a Git repository. By doing so, you follow the same approach developers use for their code. After changing an infrastructure declaration, you commit the updated files to the repository. This triggers a CI/CD pipeline that validates the configuration and then runs the automated tools to provision or decommission infrastructure resources.

You can then use Git workflows for infrastructure deployments. For example, you can create a new Git branch where you work with environment declaration files. When you are done, you create a pull request. Your changes will be then reviewed, and after the approval, the branch will be merged into the main one. If the CI/CD system is monitoring the repository, the merge will trigger an automated infrastructure deployment.

You can even extend the GitOps IaC approach to manage the JSON or YAML files Kubernetes uses for configuration. By modifying and deploying these files you can automatically perform a rolling update of containers in a cluster, essentially using GitOps to deploy an application. In such a case, you would just need to change the configuration file and push it to a Git repository. The CI/CD system then deploys the change to the cluster automatically. More specifically, the CI/CD system uses the appropriate command-line interface (CLI), like kubectl, to apply changes you committed.

In practice, you set up a build and deploy pipeline comprising various jobs. Each job is composed of one or more steps. The first job can provision resources. For instance, it can create or update a Kubernetes cluster for you automatically. Again, the job uses the CLI scripts to provision resources. Given that the environment is ready, the second job can check the source code, build it, and deploy the application to the environment.

Continuous integration using GitOps - an example

Here are the steps for setting up a pipeline using CircleCI. We will create the pipeline with two jobs. The first job will deploy the managed Kubernetes cluster in Azure. The second job will deploy the containerized ASP.NET 5 application to that cluster.

Adding a repository

We start by creating the GitHub repository, CircleCI-AKS-GitOps. To make things simpler, we only have a main branch, with the all-in-one.yml file under the k8s folder (see below). The file contains the deployment and service declarations. The deployment will create two pods using the Docker image with the sample ASP.NET 5 app. Both pods will be exposed to the public using the LoadBalancer service. We will get the public IP of this service after the deployment.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: aspnet-sample-app
spec:
  selector:
      matchLabels:
        app: aspnet-sample-app
  replicas: 2
  template:
      metadata:
        labels:
            app: aspnet-sample-app
      spec:
        containers:
        - name: aspnet-sample-app-pod
          image: mcr.microsoft.com/dotnet/samples:aspnetapp 
          ports:
          - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: aspnet-sample-app-svc  
spec:
  selector:
    app: aspnet-sample-app
  type: LoadBalancer
  pxorts:
  - port: 80

Creating a pipeline

To connect this repository with the CI/CD system, you need the pipeline configuration. That is a YAML file containing jobs and steps declarations. When using CircleCI, you create such declarations in the config.yml file under the .circleci folder.

Here, we create the config.yml using the orbs for Azure Kubernetes Service (AKS) and Kubernetes:

jobs:
  create-deployment:
    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
      - 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: k8s/all-in-one.yml
          resource-name: deployment/dotnet-deployment
orbs:
  azure-aks: circleci/azure-aks@0.2.1
  kubernetes: circleci/kubernetes@0.4.0
version: 2.1
workflows:
  deployment:
    jobs:
      - azure-aks/create-cluster:
          cluster-name: aks-dotnet
          create-resource-group: true
          generate-ssh-keys: true
          location: eastus
          resource-group: aks-dotnet-rg         
      - create-deployment:
          cluster-name: aks-dotnet
          requires:
            - azure-aks/create-cluster
          resource-group: aks-dotnet-rg                          

There are two jobs:

  • azure-aks/create-cluster – creates the Kubernetes cluster with AKS
  • create-deployment – applies the all-in-one.yml Kubernetes configuration to the AKS cluster

Job has additional parameters enabling you to apply other configurations. Here, we use them to set the cluster name (aks-dotnet), region (eastus), and Azure resource group (aks-dotnet-rg).

Building in CircleCI

Once we configure the pipeline, we can watch the project within CircleCI. If using GitHub or Bitbucket, you can use your existing account to sign up in CircleCI. The service will automatically discover your repositories.

CircleCI project page

You then use the Set Up Project button to either create a new configuration file or choose an existing pipeline configuration. For this example, we are using config.yml. Afterward, CircleCI will start building the project.

CircleCI pipeline

CircleCI pipeline details

To successfully connect to Azure, you must provide user credentials or a service principal using project environmental variables. Our user credentials are stored under the AZURE_USERNAME and AZURE_PASSWORD that we typed into Project Settings.

![CircleCI environment variables]2021-05-10-ci-with-gitops-04

After setting up those variables, CircleCI will create the AKS cluster for you and then deploy the application. Note that the first pipeline failed due to the lack of credentials.

CircleCI failed pipeline

Testing the resources

We can now test the deployment. To do so, we logged into the Azure portal, opened the Azure CloudShell, then invoked the following command:

az aks list -o table

This command will display the list of all AKS clusters, including the one (aks-dotnet) we deployed through CircleCI. Now, we need to log in to that cluster and read the public IP of the LoadBalancer service. To do so, invoke the following commands:

az aks get-credentials -n aks-dotnet -g aks-dotnet-rg
kubectl get svc

The IP of our app appears in the EXTERNAL-IP column. Use this address to see the app running.

![Kubernetes services]2021-05-10-ci-with-gitops-06.png{: .zoomable }

Cleaning up resources

To clean up the resources, type:

az group delete -n aks-dotnet-rg --no-wait --yes

This removes the AKS cluster without confirmation. Alternatively, you could supplement the pipeline with another job that deletes the cluster automatically.

Summary and next steps

We have just learned how to deploy the containerized app to the managed Kubernetes cluster with CircleCI and a GitHub repository. In practice, you will supplement the above workflow with another Git branch and create the pull request so others can review the changes you make.

If you are using CI/CD systems already, there is excellent [migration documentation]https://circleci.com/docs/migration-intro/#section=getting-started to help you move your projects to CircleCI.

Fueled by this knowledge, you can now start building your GitOps workflows with CirlceCI, easily managing apps and freeing time to focus on new features. You can get started right away, by signing up for your CircleCI free trial today.