<img src="https://ad.doubleclick.net/ddm/activity/src=11631230;type=pagevw0;cat=pw_allpg;dc_lat=;dc_rdid=;tag_for_child_directed_treatment=;tfua=;npa=;gdpr=${GDPR};gdpr_consent=${GDPR_CONSENT_755};ord=1;num=1?" width="1" height="1" alt="">

Remove GitOps Maintenance Toil: How to Automate Your ArgoCD Patches with Renovate

Virtru's Technical Series provides tips and tricks for engineers and developers.

As we all take the journey towards aligning with GitOps Principles together, I am sure we can all remember when we initially used ArgoCD to deploy our first application to a Kubernetes cluster via GitOps. At Virtru, we loved how much easier GitOps made managing applications and how quickly it allowed us to move while keeping all of our environments continuously in sync. 

We all know that after that first deployment, there is that rush and need to move quickly. Before you realize it, you have numerous components deployed across your clusters like:

It seems like every week, there is some new tool that fills a gap. Having all of these options is amazing, but introducing each adds to that growing list of components being managed.

Security is at Virtru’s core, and when you’re working at a company where that is the case, you need to make sure that any components that you’re leveraging are updated in a timely fashion. However, we also don't want to introduce any issues into the environment.

Managing An Ever-Growing Number of Components

Let’s take a step back and think about what it takes to manage each component. For every component, you need to:

  • Continuously watch for new releases
  • Review the release notes for any potential breaking changes (especially for any deprecated APIs in Kubernetes)
  • Slowly progress the new version through your development, staging, and production clusters (usually a week bake-in period between each environment)

This quickly becomes a full-time responsibility for someone on the team. We needed to reduce some of the unnecessary toil on our team at Virtru.

Typically when managing dependencies you can turn to something like Dependabot. In our case we needed something that could manage more than your typical npm or go package versions. We needed to manage our helm chart versions defined within an ArgoCD Application along with our Terraform providers and modules.

That’s why we started to look out in the community to see if there was any existing technology that we could leverage or decide if we would have to build something on our own.

Lo and behold, we weren't the only ones thinking about this problem!

Welcome to the World of Renovate

Renovate is a tool to manage automated dependency updates. It is open-source and integrates with various version control systems, including GitHub and GitLab. There are two ways to install Renovate: via GitHub application or self-hosted.

We wanted to have more control of how we run Renovate, and what version is deployed. This limited us to the self-hosted app. We wanted to run it in Kubernetes. Renovate is available as a container from DockerHub or you can also easily build your own container image by installing the application from NPM, which is the route we chose to have the most control over the Renovate configuration.

Renovate is deployed in the Virtru clusters as a CronJob. Our configuration is similar to the examples from the official documentation:

apiVersion: v1
kind: ConfigMap
metadata:
name: renovate-config
namespace: renovate-test
data:
config.json: |-
 {
   "repositories": ["<your-org>/<your-repo-1>, <your-org>/<your-repo-2>"]
  }
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: renovate-bot
namespace: renovate-test
spec:
schedule: "@hourly"
concurrencyPolicy: Forbid
jobTemplate:
  spec:
    template:
      spec:
        containers:
          - image: renovate/renovate:31.14.0
            name: renovate-bot
            env:
              - name: RENOVATE_PLATFORM
                value: "github"
              - name: RENOVATE_AUTODISCOVER
                value: "false"
              - name: RENOVATE_BASE_DIR
                value: "/tmp/renovate/"
              - name: RENOVATE_CONFIG_FILE
                value: "/opt/renovate/config.json"
              - name: LOG_LEVEL
                value: debug
              - name: RENOVATE_TOKEN
                valueFrom:
                  secretKeyRef:
                    key: github-token
                    name: renovate-secrets
            volumeMounts:
              - name: config-volume
                mountPath: /opt/renovate/
              - name: work-volume
                mountPath: /tmp/renovate/
        restartPolicy: Never
        volumes:
          - name: config-volume
            configMap:
              name: renovate-config
          - name: work-volume
            emptyDir: {}

This manifest assumes that Renovate namespace contains a Kubernetes secret called renovate-secrets, with key github-token and value of valid GitHub API token. The token permissions should be repo read/write. You can create the secret like so:

❯ kubectl create -n renovate secret generic renovate-secrets
--from-literal=github-token=<token>

Being as flexible as it is, Renovate is easy to start with. It offers repository auto-discovery. However, a larger organization would want to omit it, since this feature causes Renovate to run on every single repository that the GitHub API token has access to. An alternative to auto-discovery is the repository list directive in Renovate’s configuration file. Onboarding for each repository is also automated. Renovate submits pull requests with a default configuration file for this repository.

The most bare-bones configuration for a private GitHub repository looks like:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

This instructs Renovate to use the default configuration, which is a great place to start.

PR Labels

You may also want to add labels to easily filter out PRs created by Renovate:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "labels": ["renovate"]
}

This will cause all the PRs to be labeled as “renovate.”

Ignoring Paths

Ignoring certain paths can also be useful when a project has parts that aren’t ready to accept updates. For example, this is how you ignore everything inside “/argocd/sandbox” folder:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
    "ignorePaths": [
      "argocd/sandbox/**"
    ]
}

Package Rules

Specifying custom package rules is crucial for any decent-sized repository. These rules instruct Renovate on how to apply a piece of configuration to a number of selected files/packages. The target can be selected based on a set of selectors, including path and manager.

Let’s talk more about “managers,” which is short for “package managers.” These include traditional package managers like npm, Gradle, and Bundle, as well as less traditional file formats, like Dockerfiles or ArgoCD manifests. The managers are used to detect and maintain files in a repository.

As a matter of fact, the minimum configuration for ArgoCD project requires usage of the custom package rules, because by default ArgoCD manager doesn’t define any file match patterns, so it won't match any files until the pattern is configured.

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "packageRules": [
    {
      "matchManagers": [
        "argocd"
      ],
      "matchPaths": [
        "argocd/production/**",
        "argocd/development/**",
        "argocd/staging/**"
      ],
    }   
  ]
}

The setup from above will manage ArgoCD manifests in the project with the following structure:

└── argocd
   ├── development
   ├── production
   └── staging

Custom rules inside the package rules list have higher precedence than global rules. This way, you can redefine global configuration inside the custom rules. For example, you can overwrite global label “green” for all ArgoCD files at path “argocd/production”:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "labels": ["green"],
  "packageRules": [
    {
      "matchManagers": [
        "argocd"
      ],
      "matchPaths": [
        "argocd/production/**"
      ],
     "labels": ["red"]
    },
    {
      "matchManagers": [
        "argocd"
      ],
      "matchPaths": [
        "argocd/staging/**",
        "argocd/development/**"
      ]
    }
  ]
}

The previous example also illustrates how you can match based on multiple factors. In that case, it was managers and paths.

Grouping Updates

Sometimes, it is useful to group some of the objects that have been matched. This could be achieved using the group name directive. All matching updates sharing the groupName will be placed into the same branch and PR. Grouping all non-major version updates together can be done like so:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "packageRules": [
    {
      "matchManagers": ["argocd"],
      "matchUpdateTypes": ["minor", "patch", "pin", "digest", "lockFileMaintenance", "rollback", "bump"],
      "groupName": "argocd non-major updates"
    },
    {
      "matchManagers": ["argocd"],
      "matchUpdateTypes": ["major"]    
    }
  ]
}

Custom Schedules

Last, but not least, you can define a custom schedule for a repository. This setting was essential for us, and we ended up enabling it for every single renovate-managed repository. A repository-based schedule allowed us to run renovate every 15 minutes, without worrying about spinning up an excessive amount of CI/CD jobs or creating other undesired activity triggered by GitHub events.

Cron and Later syntaxes can be used to configure the schedule. There are also some presets that are supported. When a schedule is in use, Renovate will no longer create any new PRs. However, you need to disable the updateNotScheduled value in order to activity in the existing PRs.

Here is an example of monthly schedule with no updates to existing PRs:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "schedule": [
    "before 3am on the first day of the month"
  ],
  "updateNotScheduled": false
}

Conclusion

As you can see, Renovate brings a plethora of configuration options and provides the flexibility that any team might need. 

Renovate has helped solve that first step in the lifecycle management process of having to check if there is a new release. By tightly coupling release notes to the pull requests, it makes it easier for the team to review what has changed. 

Right now, everything after Renovate is still manual, but we plan to take it a few steps further:

  • Ability to auto-merge errata fixes
  • Test suite that will review release notes for breaking changes 
  • An automated promotion schedule to different environments

Keep an eye out for more Virtru Technical Series posts from the Virtru Platform Engineering team as we continue to explore and implement these additional steps.