DEV Community

Apurv Kiri
Apurv Kiri

Posted on Originally published at apurv.me

Kubernetes - Automated Dependency Updates with Renovate

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:

gh secret set RENOVATE_TOKEN -R kiriapurv/k8s-gitops
Enter fullscreen mode Exit fullscreen mode

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:

name: renovate

on:
  workflow_dispatch:
    inputs:
      logLevel:
        description: Renovate log level
        type: choice
        options: [info, debug]
        default: info
  schedule:
    - cron: "0 3 * * 1"

permissions:
  contents: read

jobs:
  renovate:
    runs-on: ubuntu-latest
    steps:
      - name: Self-hosted Renovate
        uses: renovatebot/github-action@2804199d260ad00c1fae0e9e842888369391c776 # v46.3.10
        with:
          token: ${{ secrets.RENOVATE_TOKEN }}
        env:
          RENOVATE_REPOSITORIES: ${{ github.repository }}
          RENOVATE_GIT_AUTHOR: "Renovate Bot <3841024+kiriapurv@users.noreply.github.com>"
          LOG_LEVEL: ${{ inputs.logLevel || 'info' }}
Enter fullscreen mode Exit fullscreen mode

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:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended",
    "helpers:pinGitHubActionDigests"
  ],
  "timezone": "Asia/Kolkata",
  "flux": {
    "managerFilePatterns": [
      "/cluster/.+\\.ya?ml$/",
      "/apps/.+\\.ya?ml$/"
    ]
  },
  "packageRules": [
    {
      "description": "Sample app image tags are managed by Flux image automation",
      "matchPackageNames": [
        "ghcr.io/kiriapurv/**"
      ],
      "enabled": false
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

docker run --rm -v "$PWD":/usr/src/app -w /usr/src/app renovate/renovate:44.190.1 renovate-config-validator --strict renovate.json

 INFO: Validating renovate.json as global config
 INFO: Config validated successfully against 1 file(s)
Enter fullscreen mode Exit fullscreen mode

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:

gh workflow run renovate.yaml -R kiriapurv/k8s-gitops
Enter fullscreen mode Exit fullscreen mode

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

 INFO: Renovate started
 INFO: Repository started (repository=kiriapurv/k8s-gitops)
 INFO: Dependency extraction complete (repository=kiriapurv/k8s-gitops, baseBranch=main)
       "stats": {
         "managers": {
           "flux": {"fileCount": 9, "depCount": 9},
           "github-actions": {"fileCount": 2, "depCount": 7}
         "total": {"fileCount": 11, "depCount": 16}
 INFO: Branch created (repository=kiriapurv/k8s-gitops, branch=renovate/pin-dependencies)
 INFO: PR created (repository=kiriapurv/k8s-gitops, branch=renovate/pin-dependencies)
       "prTitle": "Pin dependencies",
 INFO: Branch created (repository=kiriapurv/k8s-gitops, branch=renovate/fluxcd-flux2-2.x)
 INFO: PR created (repository=kiriapurv/k8s-gitops, branch=renovate/fluxcd-flux2-2.x)
       "prTitle": "Update dependency fluxcd/flux2 to v2.10.0",
 INFO: Repository finished (repository=kiriapurv/k8s-gitops)
       "cloned": true,
       "durationMs": 41762,
       "result": "done",
Enter fullscreen mode Exit fullscreen mode

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:

Renovate Dependency Dashboard listing rate-limited Helm chart updates

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:

kubectl get pods -l app.kubernetes.io/name=reflector -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.spec.containers[0].image}{"\n"}{end}'

reflector-dcc5cf554-4wl95  docker.io/emberstack/kubernetes-reflector:9.0.318
Enter fullscreen mode Exit fullscreen mode

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:

   chart:
     spec:
       chart: reflector
-      version: "9.0.318"
+      version: "9.1.45"
       interval: 10m
       sourceRef:
         kind: HelmRepository
Enter fullscreen mode Exit fullscreen mode

Renovate pull request updating the Reflector Helm release to 9.1.45

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

flux reconcile source git flux-system

◎ waiting for GitRepository reconciliation
✔ fetched revision main@sha1:0f4e575ef86d2bb51fce04a0301141cf8cd1a74a
Enter fullscreen mode Exit fullscreen mode
flux reconcile helmrelease reflector -n default

✔ HelmRelease annotated
◎ waiting for HelmRelease reconciliation
✔ applied revision 9.1.45
Enter fullscreen mode Exit fullscreen mode

Now let's check if our release was upgraded:

flux get helmrelease reflector -n default

NAME        REVISION    SUSPENDED   READY   MESSAGE
reflector   9.1.45      False       True    Helm upgrade succeeded for release default/reflector.v2 with chart reflector@9.1.45
Enter fullscreen mode Exit fullscreen mode
kubectl get pods -l app.kubernetes.io/name=reflector

NAME                         READY   STATUS    RESTARTS   AGE
reflector-556f7cbb75-mfvvz   1/1     Running   0          43s
Enter fullscreen mode Exit fullscreen mode

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

kubectl get secrets -A --field-selector metadata.name=github-registry-secret

NAMESPACE         NAME                     TYPE                             DATA   AGE
cert-manager      github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
cilium-secrets    github-registry-secret   kubernetes.io/dockerconfigjson   1      596d
default           github-registry-secret   kubernetes.io/dockerconfigjson   1      609d
flux-system       github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
ingress-nginx     github-registry-secret   kubernetes.io/dockerconfigjson   1      596d
kube-node-lease   github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
kube-public       github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
kube-system       github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
monitoring        github-registry-secret   kubernetes.io/dockerconfigjson   1      602d
reloader          github-registry-secret   kubernetes.io/dockerconfigjson   1      601d
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)