Install with Helm¶
For a platform engineer with a Kubernetes cluster: at the end, Agent Kourier runs from the published Helm chart and answers a mention in a Slack thread.
The checks below are the ones that catch the usual problems. You do not need an agent for the first install. Without one, Agent Kourier connects to Slack and tells the thread it cannot reach the agent. That reply proves Slack in, Slack out, the volume and the policies, and leaves the agent as the only thing to wire.
Before you start¶
- A Kubernetes cluster whose nodes can pull from
ghcr.io, and a namespace for Agent Kourier. The image is public, so the nodes need no pull Secret. - A Slack app with a bot token and an app-level token, invited to a channel: see Configure Slack.
helm, which pulls the chart from an OCI registry, andkubectl.- For custom resources, the chart's default route: Kubernetes 1.33 or later, and a right to create CustomResourceDefinitions (they are cluster-scoped), or an administrator who applies them once (Apply the CRDs yourself).
- For the check before you install a ConfigMap:
yq, anddockerto run the image.
Choose the route¶
Agent Kourier reads its ChatConnection, AgentBackend and Binding resources from one of two places, and the chart's default is the first.
- Custom resources (
config.source: crds, the default) are the route to run on Kubernetes. The chart installs the CRDs and the broker, and you apply the three resources withkubectlor a GitOps tool, so that each team can own its Bindings and see what became of them inkubectl get akbinding. It needs Kubernetes 1.33 or later. - A ConfigMap (
config.source: files) is the quick route for a proof of concept. The chart writes the resources of your values file into a ConfigMap, which needs no CRDs, no cluster-scoped right and no minimum version. The starter file andagent-kourier initbelow write this route.
Choose a config route says how they differ when something is wrong.
The release¶
Each release publishes a Helm chart and a container image for linux/amd64 and linux/arm64 to GHCR:
| Artifact | Reference |
|---|---|
| Chart | oci://ghcr.io/agentkourier/charts/agent-kourier, version 0.2.0 |
| Image | ghcr.io/agentkourier/agent-kourier:v0.2.0 |
The chart pins the image by digest, so you set no image values to install it. The GitHub Release of each version names both digests.
Verify the release¶
The release workflow signs the image and the chart with cosign, keyless, as itself. To check a signature, take the digest from the GitHub Release and name the workflow's exact identity:
cosign verify ghcr.io/agentkourier/agent-kourier@sha256:<image digest> \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity https://github.com/agentkourier/agent-kourier/.github/workflows/release.yml@refs/tags/v0.2.0
The chart is ghcr.io/agentkourier/charts/agent-kourier@sha256:<chart digest>, checked the same way.
Create the Secrets¶
Create the namespace first. A sealed secret is bound to its namespace and name, so it cannot be applied before the namespace exists.
The ChatConnection's Secret holds two keys with exactly these names:
kubectl -n agent-kourier create secret generic agent-kourier-slack \
--from-literal=botToken="$SLACK_BOT_TOKEN" \
--from-literal=appToken="$SLACK_APP_TOKEN"
If the agent, or a front door in front of it, verifies tokens, first
create the Binding identity and obtain its token from the accepted issuer.
Agent Kourier does not mint $AGENT_TOKEN; it is the issued bearer token. Store it in the Binding's namespace, which
for a Binding under the chart's config is the release namespace (agent-kourier here) unless the entry sets its
own namespace. The Dex example on that page
stores its token itself, as payments-agent-token in payments; for this install, change its last command to
kubectl -n agent-kourier create secret generic agent-kourier-agent-token. With a token from any other issuer:
kubectl -n agent-kourier create secret generic agent-kourier-agent-token --from-literal=token="$AGENT_TOKEN"
Tokens never go in the values file. The chart refuses a value that looks like a Slack token.
Install on custom resources¶
The chart installs the CRDs and the broker, and you apply the resources. They are two steps because a release that renders the CRDs and
resources of them fails in its first apply (Helm cannot map a kind that is not yet served), so the chart refuses to render
config.chatConnections, config.agentBackends or config.bindings as custom resources into a release that is installing their CRDs. A release with none of them renders no custom resource, and installs the CRDs itself.
The values name the ChatConnection the broker serves, as namespace/name, and grant the Secrets its resources name. The chart cannot see the Secrets of
resources you apply yourself, so a Role that names each one is made from rbac.secretNames:
config:
chatConnection: agent-kourier/slack
rbac:
secretNames:
agent-kourier: [agent-kourier-slack] # add agent-kourier-agent-token if the agent verifies tokens
helm install agent-kourier oci://ghcr.io/agentkourier/charts/agent-kourier --version 0.2.0 \
-n agent-kourier -f my-values.yaml
The pod exits with code 2 and restarts until the ChatConnection it is told to serve exists (ChatConnection agent-kourier/slack was not found), so it
is not ready yet. Apply the resources. They are the starter file's, as objects; change the four lines marked yours:
apiVersion: agentkourier.dev/v1alpha1
kind: ChatConnection
metadata:
name: slack
namespace: agent-kourier
spec:
platform: slack
credentialsSecretRef: {name: agent-kourier-slack}
---
apiVersion: agentkourier.dev/v1alpha1
kind: AgentBackend
metadata:
name: agent
namespace: agent-kourier
spec:
dialect: a2a
url: http://my-agent.my-namespace.svc.cluster.local:8083/a2a/ # yours: the agent's A2A URL
---
apiVersion: agentkourier.dev/v1alpha1
kind: Binding
metadata:
name: my-binding
namespace: agent-kourier
spec:
agent:
backendRef: {name: agent}
namespace: my-namespace # yours: the agent's namespace
name: my-agent # yours: the agent's name
identity:
userId: agent-kourier
chat:
connectionRef: {name: slack}
channel: C0123456789 # yours: the channel's ID (starts with C), not its name
output: live
interactions: {askUser: false}
kubectl apply --dry-run=server -f my-resources.yaml
kubectl apply -f my-resources.yaml
kubectl -n agent-kourier get akbinding
The dry run asks the API server to check each object against the CRDs' schema, and names the field of a refusal. The checks that only the broker makes show afterwards in the Binding's status: Check that it worked reads it. The pod starts at its next restart after the ChatConnection exists, which Kubernetes does by itself. Let a team add its own Binding says what a team needs to write its own.
If the chart refuses with config.source crds needs Kubernetes 1.33 or later, the cluster is older: install the ConfigMap route below, which has no minimum.
An installer that may not create CRDs sets crds.install: false (unset, it is true with custom resources), and an administrator applies them first. So does a second broker in the same cluster: the CRDs are
the first release's, and a second release that renders them is refused by Helm (invalid ownership metadata).
The ConfigMap route¶
Write the values, Check the values before you install and Install the chart
install the ConfigMap route, which the starter file and agent-kourier init name with config.source: files. With files, crds.install is false unless you set it, so they install no CRDs.
Moving to custom resources later is the how-to's.
Write the values¶
The chart takes Agent Kourier's three resource kinds under config: the ChatConnection, the AgentBackend and the
Binding. They are nested, so they go in a values file rather than in --set flags. There are two ways to get one: let
agent-kourier init write it, or download a starter file and edit it.
Write it with init¶
agent-kourier init asks for the agent's dialect (a2a or kagent-v1), URL, namespace and name, and for the channel.
It writes my-values.yaml in the shape of the starter file below, loads what the chart would render from it with the
loader that Agent Kourier starts with, and prints the kubectl commands for the Secrets above and the helm install
command below, with your namespace in them. It writes nothing if the values do not load, and it does not overwrite a file
unless you pass --force.
Run it from the released image, in the directory that should get my-values.yaml. -it lets it ask its questions,
and --user runs it as you, so it can write the file into your directory (the image's own user is 65532):
docker run --rm -it --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work \
ghcr.io/agentkourier/agent-kourier:v0.2.0 init
The helm install command it prints names the chart version of the image's release. Releases before 0.1.3 do not
have init. From a checkout of this repository, go run ./cmd/agent-kourier init runs the same command; a build that is
not a release has no chart version to name, so the command it prints has no --version, and Helm takes the newest chart.
Every question has an option, so the same run needs no terminal. Give the channel by ID, or by name when a bot token is set:
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work \
ghcr.io/agentkourier/agent-kourier:v0.2.0 init --dialect a2a \
--agent-url http://my-agent.my-namespace.svc.cluster.local:8083/a2a/ \
--agent-namespace my-namespace --agent-name my-agent --channel C0123456789
With the bot token in SLACK_BOT_TOKEN (exported, and passed with -e SLACK_BOT_TOKEN) or in a file named by
--token-file (mounted into the container), init also looks a channel name up and
checks that the bot can read the channel, as the dry run below does. Without a token it skips Slack, says so, and needs
the channel's ID. Add --agent-token if the agent verifies tokens: the Binding then reads the token from the Secret
agent-kourier-agent-token, and init prints the command that creates it. The options are in
Command line and environment.
Download the starter file¶
Or download the starter file, a complete first install with an agent that does not exist yet:
curl -fsSLo my-values.yaml https://raw.githubusercontent.com/agentkourier/agent-kourier/main/examples/helm/my-values.yaml
It is this file. Change the four lines marked yours: the agent's URL, namespace and name, and the channel's ID, which
starts with C (G for an older private channel), not the channel's name. init writes the same
resources with those four values filled in. The -f flags below pass it to Helm.
# A first install of the Agent Kourier Helm chart: one Slack connection, one agent and one channel.
#
# curl -fsSLo my-values.yaml https://raw.githubusercontent.com/agentkourier/agent-kourier/main/examples/helm/my-values.yaml
#
# Change the four lines marked "yours", then install with -f my-values.yaml. The steps around it (the Secrets, a dry
# run that checks this file, the install) are in https://agentkourier.dev/how-to/install-with-helm/.
# Keep the file: an upgrade passes it again.
#
# This file is the quick route for a proof of concept: the chart writes the resources below into a ConfigMap. The chart's default,
# and the route to run on Kubernetes, reads them as custom resources instead (config.source crds), which other
# teams can add to; https://agentkourier.dev/explanation/config-routes/ says which to choose.
config:
source: files
# How Agent Kourier talks to Slack. The Secret holds the bot token and the app-level token under the keys botToken
# and appToken, in the release's namespace.
chatConnections:
slack:
spec:
platform: slack
credentialsSecretRef: {name: agent-kourier-slack}
# Where the agent is: its A2A endpoint, called exactly as written (keep a trailing slash if the route has one).
agentBackends:
agent:
spec:
dialect: a2a
url: http://my-agent.my-namespace.svc.cluster.local:8083/a2a/ # yours: the agent's A2A URL
# Which channel talks to which agent.
bindings:
my-binding:
spec:
agent:
backendRef: {name: agent}
namespace: my-namespace # yours: the agent's namespace
name: my-agent # yours: the agent's name
identity:
# The identity Agent Kourier calls the agent as. If the agent verifies tokens, also set
# tokenSecretRef: {name: agent-kourier-agent-token}, a Secret with the token under the key token.
userId: agent-kourier
chat:
connectionRef: {name: slack}
channel: C0123456789 # yours: the channel's ID (starts with C), not its name
output: live
interactions: {askUser: false}
Every field is in the Binding, AgentBackend and
ChatConnection references, and every chart value in Helm values.
helm show values oci://ghcr.io/agentkourier/charts/agent-kourier --version 0.2.0 prints the chart's defaults with their
comments. Keep the file: an upgrade passes it again.
Check the values before you install¶
The chart passes each resource's spec through as written, so helm lint and helm template accept a misspelt field
such as chanel:. Agent Kourier rejects it when it starts, and the pod restarts in a loop. agent-kourier dry-run
loads the same files with the same loader, so it finds those errors before you install. Render the files the chart
writes, with yq:
mkdir -p rendered
helm template agent-kourier oci://ghcr.io/agentkourier/charts/agent-kourier --version 0.2.0 \
-n agent-kourier -f my-values.yaml --show-only templates/configmap.yaml |
yq -r '.data[] | "---\n" + .' > rendered/resources.yaml
Then run the dry run from the released image, with the bot token in SLACK_BOT_TOKEN (exported) and the Binding
named as namespace/name:
docker run --rm -e SLACK_BOT_TOKEN -v "$PWD/rendered:/config:ro" \
ghcr.io/agentkourier/agent-kourier:v0.2.0 dry-run --config /config --binding agent-kourier/my-binding
An error that starts agent-kourier dry-run: load /config: names the file, the resource and the field. The files load
before the token is read, so without a token the dry run still checks them, and then stops at no bot token. With one,
it reads the Binding's channel, and it exits 1 if Slack rejects the token. It does not check references between
resources or the Secrets: those errors show only when the pod starts. Reading the rest of its output is in
Tune triggers with a dry run.
This checks the files of the ConfigMap route. With custom resources (config.source: crds, the default) the chart renders no ConfigMap and the
resources are objects: check each against the CRDs' schema with kubectl apply --dry-run=server -f <file>, which names the field of a
refusal, as above. The checks that only the broker makes show afterwards, in the Binding's status
(Move the config between a ConfigMap and custom resources).
Install the chart¶
helm install agent-kourier oci://ghcr.io/agentkourier/charts/agent-kourier --version 0.2.0 \
-n agent-kourier -f my-values.yaml
The defaults run one replica with SQLite on a 1 GiB volume. For Postgres and a standby, see Run two replicas (high availability).
The starter file names config.source: files, so this install reads the resources from a ConfigMap and installs no CRDs. To run on custom resources
instead, so that other teams write their own Bindings, use Install on custom resources for a new install, and
Move the config between a ConfigMap and custom resources on a running one; Choose a config route says when that is worth it.
If the nodes may pull only from your own registry, copy the image there by digest and pass
--set image.repository=<registry>/<repo>/agent-kourier. The chart keeps its pinned digest, so the copy must have the
same one.
NetworkPolicy on Cilium
If you turn on networkPolicy on a cluster that runs Cilium, add the policy in
The pod exits with code 4 first. Without it
the pod cannot reach the API server and never starts.
Install behind an egress proxy¶
Agent Kourier reads the standard proxy variables (HTTPS_PROXY, HTTP_PROXY, NO_PROXY, in upper or lower case) in some of its connections and not in others. Set them in
extraEnv when the cluster reaches Slack through a proxy:
extraEnv:
- name: HTTPS_PROXY
value: http://proxy.example.com:3128 # a proxy that needs a login: build the URL in a Secret and use valueFrom.secretKeyRef
- name: NO_PROXY
value: 10.96.0.1,.svc,.cluster.local # the kubernetes Service's cluster IP, as below, and the in-cluster domains
| Connection | Uses the proxy variables |
|---|---|
| Slack Web API calls and the Socket Mode websocket | Yes |
| The Kubernetes API server: the Secret and custom resource informers, status writes and the leader Lease | Yes |
OTLP trace export, when OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is set |
Yes |
Agents: the card, calls and streams to an AgentBackend's url |
No. The connection is made directly, on purpose, so that an environment variable cannot redirect a Binding's token |
| The database | No |
What to put in NO_PROXY:
- The Kubernetes API server. The broker dials the address in
KUBERNETES_SERVICE_HOST, which Kubernetes sets in every pod: the cluster IP of thekubernetesService in thedefaultnamespace (kubectl -n default get service kubernetes), not the control plane's own address. Put that IP, or the cluster's service CIDR, inNO_PROXY; a CIDR is read by both the Kubernetes client and Go's own proxy code. Without it the broker's requests to the API server go to the proxy. If the proxy does not carry them, the pod exits with code 4, at the latest when its sync wait (AGENTKOURIER_SYNC_TIMEOUT, 30 seconds by default) ends: the Secret caches never sync, and onconfig.source: crdsthe first list of the custom resources fails as soon as the proxy refuses the connection. See the pod exits with code 4 (read from the clients' code and not run through a proxy here). - An in-cluster OTLP collector. Add its domain suffix (
.svcand.cluster.localabove), or the spans go to the proxy. - Agents need no entry. They never go to the proxy, so a
NO_PROXYrule changes nothing for them. An agent that is reachable only through a proxy needs a reverse proxy that its AgentBackend'surlnames.
With networkPolicy.enabled, egress to the proxy's address has to be allowed in networkPolicy.extraEgress, and networkPolicy.slack.cidrs can then be narrowed to []
(see networkPolicy). Narrowing it removes the only rule that allows port 443 to public addresses, and agents never use
the proxy, so an AgentBackend at a public address then needs an extraEgress rule of its own. The policy's networkPolicy.kubeApiServer rule still has to name the control plane's addresses, since the policy sees them after the Service's translation.
Check it¶
A healthy start logs these lines, in this order:
config route
config reloaded
serving a ChatConnection
opened the store
ready
slack: socket mode connecting
slack: socket mode websocket opened
slack: socket mode connected
config route is the first line, and it is how the log says which of the two routes the broker reads: its config_source field is
crds or files. With crds it also has a connection field, the namespace/name of the ChatConnection the broker is set to serve
(AGENTKOURIER_CHAT_CONNECTION), and the later serving a ChatConnection line confirms that the cluster has it. With files there is no connection field: the one
ChatConnection of the directory is named by serving a ChatConnection alone. The line is logged at info: at the default level, the log of a container that has just started and has no config route line is from a release older than the one that added it. With logLevel: warn or error no start line is shown, and a log that kubelet has rotated no longer has its first lines. The order of the first five lines is read from the code and was checked, on both
routes, against the broker started with a fake cluster; the three slack: lines were not re-run against a cluster for this page.
ready means the config loaded and the store opened. It does not mean Slack connected: wait for the last line. It
carries a num_connections field, the number of connections open on the app token. A value above 1 on a fresh start
is worth a look.
Then mention the bot in the channel. With no agent, the first failed send is retried silently; after the second the thread gets "I can't reach the agent
right now, and I'm trying again." If the agent stays unreachable the turn eventually fails with "This request failed,
and I won't try it again." The log shows session turn started and then session turn failed, with the reason in
error.
Now connect a real agent. If you have none yet, start with the docs-agent recipe: a plain A2A agent
that runs on any cluster, with no kagent. To connect your own: kagent,
any A2A agent or an agent on Google AX.
Stop it and keep the data¶
Set replicaCount: 0. Agent Kourier stops, and the volume stays.
On helm uninstall the chart keeps the claim by default: persistence.retainOnUninstall: true sets
helm.sh/resource-policy: keep on it. A GitOps tool may not honour that annotation when it prunes, and that
behaviour is not guaranteed. If the data matters, create the claim yourself and set persistence.existingClaim, so no
release owns it, and stop with replicaCount: 0 rather than by removing the app.
The CustomResourceDefinitions that crds.install renders are kept on helm uninstall the same way (helm.sh/resource-policy: keep), and that too is
Helm's guarantee only. Under Argo CD with prune, turning crds.install off, or deleting the Application, may delete the CRDs, and deleting a CRD deletes every
Binding, AgentBackend or ChatConnection of that kind in the cluster, which is every team's. The chart renders argocd.argoproj.io/sync-options: Prune=false,Delete=false
on each CRD, which Argo CD's documentation says stops that. This is unverified: Argo CD has not been run against this chart. A tool other than Helm or Argo CD needs its own way to be told to leave them,
or have the CRDs applied outside what it prunes (see Apply the CRDs yourself).
Build from source¶
For a change that is not in a release yet. You need a checkout of this repository and docker.
make image builds the production image, agent-kourier:dev, for the architecture of the Docker daemon that builds it.
If the nodes run another architecture, as amd64 nodes do when you build on an Apple silicon laptop, name theirs with
DOCKER_DEFAULT_PLATFORM. The Go build cross-compiles, so this needs no emulation. Then tag the image for your
registry and push it:
DOCKER_DEFAULT_PLATFORM=linux/amd64 make image # the nodes' platform
docker image inspect --format '{{.Os}}/{{.Architecture}}' agent-kourier:dev
docker tag agent-kourier:dev <registry>/<repo>/agent-kourier:<tag>
docker push <registry>/<repo>/agent-kourier:<tag>
- Push each build under a new tag. The chart's
image.pullPolicyisIfNotPresent, so a node that already has a tag runs what it has and does not pull a rebuild pushed under the same tag. - Many registries make tags immutable, which enforces the same thing.
The chart in the source tree has no image, and the render fails without one. Install it from the checkout with the image you pushed:
helm install agent-kourier ./charts/agent-kourier -n agent-kourier -f my-values.yaml \
--set image.repository=<registry>/<repo>/agent-kourier --set image.tag=<tag>
To pin the image, pass --set image.digest=sha256:<digest> in place of the tag; a digest wins over a tag. To run the
dry run from the checkout without an image, use go run ./cmd/agent-kourier dry-run --config rendered ....
make release-image builds what the release workflow publishes, an image for both platforms with provenance and SBOM
attestations and the chart stamped with its digest, and checks them, without pushing anything. The repository's
RELEASING.md covers it and how a release is cut.
Related¶
- Troubleshoot an install for the failures a first install runs into.
- Upgrade to a newer release.
- Export metrics and traces.
- Stored data for what Agent Kourier keeps and for how long.