Kubernetes - Automated Dependency Updates with Renovate

Table of Contents

This post is part of our ongoing series of posts for Kubernetes. In this post, we will automate dependency updates for our GitOps repository using Renovate, an open-source tool that finds outdated dependencies and raises pull requests to update them.

All components in our cluster are pinned to a specific version: Helm charts in HelmRelease, Flux itself in gotk-components.yaml, and actions in our GitHub workflows. This is what GitOps expects, but nothing tells us when these versions become outdated. We hadn’t touched our cluster for a while, and by the time we came back, every Helm chart had a newer release available. Checking release pages manually for each of them is error-prone and time-consuming.

Renovate addresses this challenge by scanning the repository, looking up newer versions of each dependency, and raising a pull request per update with release notes attached. We stay in control by reviewing and merging, and FluxCD takes care of rolling out the change. Let’s dive into setting up Renovate as a self-hosted GitHub Actions workflow.

1. Create a Token for Renovate

Renovate needs a token to access the repository, push update branches and open pull requests. Let’s create a fine-grained personal access token that only has access to our GitOps repository:

  • Repository access: Only select repositories → k8s-gitops
  • Read and write permission for Contents, Pull requests, Issues and Workflows
  • Read-only access to Metadata (added automatically)

Workflows permission is needed so Renovate can update action versions in .github/workflows, and Issues is used for the Dependency Dashboard we’ll see later.

Next, save the token as a repository secret. gh asks for the value, so the token never ends up in shell history:

1gh secret set RENOVATE_TOKEN -R kiriapurv/k8s-gitops
Important
Don’t forget to give the token access to the repository itself, not just the permissions. With Public repositories selected, Renovate fails on a private repository with platform-unknown-error, and only the debug log shows the actual problem: Could not resolve to a Repository with the name 'kiriapurv/k8s-gitops'.

2. Setup Renovate Workflow

Let’s schedule weekly Renovate runs by creating .github/workflows/renovate.yaml:

 1name: renovate
 2
 3on:
 4  workflow_dispatch:
 5    inputs:
 6      logLevel:
 7        description: Renovate log level
 8        type: choice
 9        options: [info, debug]
10        default: info
11  schedule:
12    - cron: "0 3 * * 1"
13
14permissions:
15  contents: read
16
17jobs:
18  renovate:
19    runs-on: ubuntu-latest
20    steps:
21      - name: Self-hosted Renovate
22        uses: renovatebot/github-action@2804199d260ad00c1fae0e9e842888369391c776 # v46.3.10
23        with:
24          token: ${{ secrets.RENOVATE_TOKEN }}
25        env:
26          RENOVATE_REPOSITORIES: ${{ github.repository }}
27          RENOVATE_GIT_AUTHOR: "Renovate Bot <[email protected]>"
28          LOG_LEVEL: ${{ inputs.logLevel || 'info' }}

This configuration:

  • Runs every Monday at 03:00 UTC, and on demand through workflow_dispatch with an optional debug log level for troubleshooting
  • Pins the Renovate action to a full commit SHA instead of a tag, as this step gets a token with write access
  • Restricts Renovate to the current repository with RENOVATE_REPOSITORIES
  • Sets RENOVATE_GIT_AUTHOR so commits are made with our account instead of Renovate’s default Mend address

3. Configure Renovate

Next, let’s tell Renovate what to look for by creating renovate.json in the root of the repository:

 1{
 2  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
 3  "extends": [
 4    "config:recommended",
 5    "helpers:pinGitHubActionDigests"
 6  ],
 7  "timezone": "Asia/Kolkata",
 8  "flux": {
 9    "managerFilePatterns": [
10      "/cluster/.+\\.ya?ml$/",
11      "/apps/.+\\.ya?ml$/"
12    ]
13  },
14  "packageRules": [
15    {
16      "description": "Sample app image tags are managed by Flux image automation",
17      "matchPackageNames": [
18        "ghcr.io/kiriapurv/**"
19      ],
20      "enabled": false
21    }
22  ]
23}

This configuration includes several important customizations:

  • config:recommended gives us Renovate’s sensible defaults, like the Dependency Dashboard and PR rate limiting
  • helpers:pinGitHubActionDigests pins GitHub Actions to commit digests and keeps those digests updated
  • flux.managerFilePatterns is the most important setting for our layout. By default, Renovate’s flux manager only looks at gotk-components.yaml, so it wouldn’t find the HelmRelease definitions in cluster/default/
  • The packageRules entry stops updates for our sample application image, since Flux image automation already handles it as set up in Kubernetes GitOps with FluxCD - Part 3 - Automated Image Updates

Before committing, let’s validate the configuration with Renovate’s own validator:

1docker run --rm -v "$PWD":/usr/src/app -w /usr/src/app renovate/renovate:44.190.1 renovate-config-validator --strict renovate.json
2
3 INFO: Validating renovate.json as global config
4 INFO: Config validated successfully against 1 file(s)

Now let’s commit and push these changes to our GitOps repository.

4. Validate Setup

Let’s run the workflow manually right away instead of waiting for Monday:

1gh workflow run renovate.yaml -R kiriapurv/k8s-gitops

Once the run is done, the workflow log shows what Renovate found:

 1 INFO: Renovate started
 2 INFO: Repository started (repository=kiriapurv/k8s-gitops)
 3 INFO: Dependency extraction complete (repository=kiriapurv/k8s-gitops, baseBranch=main)
 4       "stats": {
 5         "managers": {
 6           "flux": {"fileCount": 9, "depCount": 9},
 7           "github-actions": {"fileCount": 2, "depCount": 7}
 8         "total": {"fileCount": 11, "depCount": 16}
 9 INFO: Branch created (repository=kiriapurv/k8s-gitops, branch=renovate/pin-dependencies)
10 INFO: PR created (repository=kiriapurv/k8s-gitops, branch=renovate/pin-dependencies)
11       "prTitle": "Pin dependencies",
12 INFO: Branch created (repository=kiriapurv/k8s-gitops, branch=renovate/fluxcd-flux2-2.x)
13 INFO: PR created (repository=kiriapurv/k8s-gitops, branch=renovate/fluxcd-flux2-2.x)
14       "prTitle": "Update dependency fluxcd/flux2 to v2.10.0",
15 INFO: Repository finished (repository=kiriapurv/k8s-gitops)
16       "cloned": true,
17       "durationMs": 41762,
18       "result": "done",

Renovate detected 16 dependencies in 11 files and already opened two pull requests. The rest of the updates are listed in the Dependency Dashboard, an issue created and maintained by Renovate itself:

All Helm charts in our cluster had an update available:

Chart Current Available
cilium 1.17.1 1.20.4
cert-manager 1.17.1 v1.21.3
ingress-nginx 4.11.3 4.15.1
kube-prometheus-stack 69.5.2 69.8.2, 92.14.1
descheduler 0.32.2 0.37.0
reflector 9.0.318 9.1.45, 10.0.69
reloader 1.3.0 2.2.19

These updates are listed under Rate-Limited because config:recommended allows at most two new pull requests per hour, so a stale repository isn’t flooded with PRs. Ticking a checkbox on the dashboard lifts the limit for that one update. Major versions, like kube-prometheus-stack 92, get a separate branch, so minor and major upgrades can be reviewed separately.

5. Rolling Out an Update

To show the full flow, we’ll upgrade Reflector, which we configured in Kubernetes - Replicate Secrets with Reflector. First, let’s check which version is running now:

1kubectl get pods -l app.kubernetes.io/name=reflector -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.spec.containers[0].image}{"\n"}{end}'
2
3reflector-dcc5cf554-4wl95  docker.io/emberstack/kubernetes-reflector:9.0.318

After ticking Update Helm release reflector to v9.1.45 on the dashboard and running the workflow again, Renovate creates a pull request with the release notes. The only line that changes:

1   chart:
2     spec:
3       chart: reflector
4-      version: "9.0.318"
5+      version: "9.1.45"
6       interval: 10m
7       sourceRef:
8         kind: HelmRepository

Once the pull request is merged, FluxCD picks up the change. Instead of waiting for the next polling interval, let’s trigger the reconciliation:

1flux reconcile source git flux-system
2
3◎ waiting for GitRepository reconciliation
4✔ fetched revision main@sha1:0f4e575ef86d2bb51fce04a0301141cf8cd1a74a
1flux reconcile helmrelease reflector -n default
2
3✔ HelmRelease annotated
4◎ waiting for HelmRelease reconciliation
5✔ applied revision 9.1.45

Now let’s check if our release was upgraded:

1flux get helmrelease reflector -n default
2
3NAME     	REVISION	SUSPENDED	READY	MESSAGE
4reflector	9.1.45  	False    	True 	Helm upgrade succeeded for release default/reflector.v2 with chart [email protected]
1kubectl get pods -l app.kubernetes.io/name=reflector
2
3NAME                         READY   STATUS    RESTARTS   AGE
4reflector-556f7cbb75-mfvvz   1/1     Running   0          43s

The new pod runs docker.io/emberstack/kubernetes-reflector:9.1.45, and the GitHub registry secret is still replicated to all namespaces:

 1kubectl get secrets -A --field-selector metadata.name=github-registry-secret
 2
 3NAMESPACE         NAME                     TYPE                             DATA   AGE
 4cert-manager      github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
 5cilium-secrets    github-registry-secret   kubernetes.io/dockerconfigjson   1      596d
 6default           github-registry-secret   kubernetes.io/dockerconfigjson   1      609d
 7flux-system       github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
 8ingress-nginx     github-registry-secret   kubernetes.io/dockerconfigjson   1      596d
 9kube-node-lease   github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
10kube-public       github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
11kube-system       github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
12monitoring        github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
13reloader          github-registry-secret   kubernetes.io/dockerconfigjson   1      601d

The update went from pull request to running pod without touching the cluster directly.

6. Retiring the Custom Flux Update Workflow

In Kubernetes GitOps with FluxCD - Part 7 - Resource Optimization and Automated Version Management with Github Actions, we created a GitHub Actions workflow to regenerate gotk-components.yaml every month. Renovate’s flux manager does this automatically: the Update dependency fluxcd/flux2 to v2.10.0 pull request regenerates cluster/default/flux-system/gotk-components.yaml for the new Flux version.

Since both would now conflict over the same file, we removed .github/workflows/update-flux.yaml and closed its pending pull request. Renovate saw that the workflow was gone and automatically closed its own Pin dependencies pull request, which only changed that file.

Note
We are holding back the Flux v2.10.0 update and the major chart upgrades for now. They need a newer Kubernetes version, so we’ll upgrade K3s first in an upcoming post.

What next ?

Future posts will keep bringing the cluster up to date, including:

  • Automated K3s upgrades with System Upgrade Controller
  • Migrating from ingress-nginx to Gateway API

Stay tuned for each of these topics.

References