Pactify a service¶
To pactify a service is to give it a Pacto
contract, pacto.yaml kept next to your code, and let
Pacto check every new version before it runs. This guide takes a plain service to a contract,
signed published versions and environments Pacto only watches, and shows how
an environment moves past watching once it has earned it. Every command is on
this page except the one-time cluster setup for approvals. To preview the
result, press Play the delivery story in the
live dashboard demo, which runs in your browser.
In the cluster, Flux does the deploying and Pacto tells it which version. Each environment starts on observe, the first level of the autonomy ladder: Pacto watches and moves nothing until a person raises it.
Before you begin¶
You need:
- the
pactocommand, from Installation; - a service in a git repository, with a Dockerfile and the Kubernetes files that run it (a Deployment and a Service). Those files are what lands in the cluster. Leave the namespace out of them: each environment gets its own;
- Docker with a builder that can write an image to a folder, as Pacto builds
the image there and pushes it itself:
docker buildx create --use --driver docker-container; - a container registry, and a login to it:
pacto login registry.example -u <user>(ordocker login). Any registry works for this page. The optional signed record of each change is stored next to what you publish, in a registry that serves OCI Referrers. Without records a Stage stops at advise; see What delivery needs; cosign, once, to make a signing key:brew install cosign, orgo install github.com/sigstore/cosign/v2/cmd/cosign@latest;- for step 4, a Kubernetes cluster (1.30 or later),
kubectlandhelm. People approve with a fresh company login, which needs a one-time cluster setup, maybe by you: the cluster trusts your identity provider. Until it works, nobody can approve; see where approving works.
Pacto writes the contract from your service.
If you only want the contract and its CI checks, run
pacto init --describe-only: the contract has no publish section and needs no
signing key. Then go on to Put it in your repository: pacto push
uploads the contract alone.
1. See what Pacto would add¶
From the root of your repository:
pacto init reads what is already there, such as the Dockerfile and the
Kubernetes files, and fills in a contract from it. It reads your registry from
the image name in your Deployment; pass --registry registry.example/acme if it
cannot, or if the image is a bare name such as checkout: a Docker Hub official
image only Docker can push to. --delivery adds the cluster files of step 4, so
the first version you publish already carries them. With --plan it shows
exactly what it would add and changes nothing. For a private registry, add
--registry-secret registry-creds (step 4 creates that Secret). For a local
registry that speaks plain HTTP, add --insecure-registry; to keep a throwaway
key's signatures out of the public
transparency log, add --no-transparency-log.
The plan lists the services yours depends on, what Pacto could not work out, marked Unknown, and a Next list of what to do after. Unknown marks a question that Pacto leaves for you to answer.
2. Write the contract¶
Add any flags you needed in step 1, such as --registry-secret:
--owner names the team that owns the service: the name of its group in your
company login, whose members Pacto asks about this service
(Decide, unfreeze and roll back). init writes
pacto.yaml, the delivery/ folder and a .pactoignore that keeps the signing
key out of what is published, and never overwrites your edits. For an image
registry.example/acme/my-service:1.0.0 and the files k8s/deployment.yaml and
k8s/service.yaml, the contract is:
pactoVersion: "2.1"
service:
name: my-service
version: 1.0.0 # from the image tag (0.1.0 and an Unknown if none)
owner:
team: payments
workload: service # from the Kubernetes workload kind
capabilities: # from a readiness, liveness or startup probe
- type: health
dependencies: # one per service a URL in the environment names
- name: billing
ref: oci://registry.example/acme/contracts/billing # its publish.bundle
required: true # yours cannot run without it
compatibility: "^1.0.0" # a placeholder: the versions yours accepts
configurations: # the environment, ConfigMaps included, values as defaults
- name: env
schema: configuration/schema.json
required: true
publish:
bundle: oci://registry.example/acme/contracts/my-service # where the contract goes
deploy: manifests # what Flux applies
artifacts:
- name: api
type: image
ref: oci://registry.example/acme/my-service
build: {dockerfile: {context: .}}
- name: manifests
type: manifests
ref: oci://registry.example/acme/deploy/my-service
build: # your Kubernetes files, the image just built, delivery/pacto.yaml
command: [sh, -c, 'cp k8s/deployment.yaml k8s/service.yaml "$PACTO_OUT"/ && sed -E "s,(^|[{,[:space:]])(image:[[:space:]]*[\"'']?)registry\.example/acme/my-service:1\.0\.0([\"'']?[[:space:]]*([,}#]|$)),\1\2$PACTO_REF_API@$PACTO_DIGEST_API\3,g" k8s/deployment.yaml > "$PACTO_OUT"/deployment.yaml && { grep -qF "$PACTO_DIGEST_API" "$PACTO_OUT"/deployment.yaml || { echo "pacto: no image: registry.example/acme/my-service:1.0.0 line to pin" >&2; exit 1; }; } && sed "s/@PACTO_VERSION@/$PACTO_VERSION/" delivery/pacto.yaml > "$PACTO_OUT"/pacto-resource.yaml']
sign: {key: cosign.key}
Leave the long command line as it is. It copies your Kubernetes files with
the image pinned to the digest this version built, in every manifest that runs
it, and names any file it cannot rewrite. k8s/deployment.yaml stays as it is.
Fill in what the plan marked Unknown, above all the dependencies: set each
placeholder compatibility to the versions yours accepts, such as ^2.0.0
for any 2.x. With that service's contract in --fleet, init reads its
bundle and version instead. This
version range lets Pacto ask the right team about a change. Declare an OpenAPI
document under interfaces (how): Pacto sees
API changes only there. Every field is in Publish
and Dependencies. For private
images, add imagePullSecrets: [{name: registry-creds}] under
spec.template.spec in your Deployment. With dependencies, run pacto lock .
(Lockfile), and commit what init wrote, as
What to commit lists.
3. Publish a version¶
cosign generate-key-pair # once: cosign.key (private) and cosign.pub
export COSIGN_PASSWORD=... # the password you chose
pacto publish --plan
pacto publish
--plan changes nothing. pacto publish builds the image, pushes it with your
Kubernetes files and the contract to your registry, signs them so anyone can
check they came from you and labels them with the version in the contract. It
reads cosign.key from the root of the repository, as publish.sign says. In
your build pipeline, give the key and its password as secrets and run
pacto publish after your tests.
Versions have three numbers, major.minor.patch, such as 2.4.1. Publishing refuses a version too small for the change (a change that breaks the services using yours needs 3.0.0) and never overwrites a published one; see Publish a version.
4. Connect the service to your cluster¶
Alpha
Delivery is new: its settings and commands may still change.
Install, once per cluster, Flux, the Pacto operator (it checks that each
running service matches its contract) and delivery (it decides which version
runs in each environment). One Helm chart installs both. Delivery reads your contracts: with a private registry, give it a login
in the operator's namespace and add
--set delivery.promoter.registryCredentials.secretName=registry-creds to the
command that turns delivery on below:
kubectl create namespace pacto-operator-system
kubectl -n pacto-operator-system create secret docker-registry registry-creds \
--docker-server=registry.example --docker-username=<user> --docker-password=<token>
helm install pacto-operator \
oci://ghcr.io/trianalab/pacto/charts/pacto-operator \
--version 6.4.1 \
--namespace pacto-operator-system --create-namespace
helm upgrade pacto-operator \
oci://ghcr.io/trianalab/pacto/charts/pacto-operator \
--version 6.4.1 \
--namespace pacto-operator-system \
--reset-then-reuse-values --set delivery.enabled=true \
--set delivery.identities.humanGroups={staff}
Replace staff with your people's group; Install
says why.
Delivery is opt-in: without it, Pacto never changes what runs. Each environment
is a namespace of the same name. Create them, give Flux your public key, so it
takes only versions you signed, and apply the files init wrote:
for env in test prod; do
kubectl create namespace $env
kubectl -n $env create secret generic cosign-pub --from-file=cosign.pub
# a private registry only:
kubectl -n $env create secret docker-registry registry-creds \
--docker-server=registry.example --docker-username=<user> --docker-password=<token>
done
kubectl apply -f delivery/test.yaml -f delivery/prod.yaml
registry-creds is the pull Secret your Deployment names and, with
--registry-secret, the login Flux and the operator use. A read-only token is enough (on GitHub, read:packages); see
Private registries.
Each delivery/<env>.yaml holds a Stage and the Flux pieces that watch
your registry and apply what the Stage picks. delivery/pacto.yaml tells the
operator to check the service; it ships inside every version, so you never
apply it.
Each Stage starts at observe: it plans every new version and moves nothing,
and the OCIRepository stays pinned to the tag you deploy (spec.ref.tag), so
Flux applies what it did. With no version tag on the image, init writes no
environment file.
kubectl get stages.delivery.pacto.trianalab.io -A # POINTER: the digest of the version each one runs
pacto delivery history -n test --stage my-service # what happened in test
kubectl -n test describe stages.delivery.pacto.trianalab.io my-service # if nothing moves: why, under Ready
5. Ship the next version¶
Make a change, raise service.version in pacto.yaml, then run
pacto publish. While the Stages observe, you deploy the new tag as before:
set spec.ref.tag in delivery/<env>.yaml and apply it. Each Stage records
what it would have done.
To leave observe, in this order:
- Keep deploying this way until 10 versions passed their checks
(
autonomy.proposeAfterlowers that, see the ladder). The Stage'sstatus.proposallists what is still missing. -
Remove
spec.reffrom the OCIRepository indelivery/test.yamland apply it, so Flux stops setting the version, and label it so only Pacto may write its digest. Pacto then proposesadvise: -
Ask for the raise; another person of the owner team approves it (see Why you were asked). Be quick: until the raise, nothing moves the version.
Each level above that is one more approved operation. In prod, init also
writes approval: always: true, so every promote there waits for a person:
the contract check cannot see a consumer with no contract, or tell whether the
version is healthy. Delete it from delivery/prod.yaml once a health check
judges each version there (gates.verify.checks, see
Delivery reference: health checks).
A version that fails its checks after release is rolled back or freezes its
Stage, as What happens to a change
explains.
Each move is a change; one that waits for a person shows
Held. To approve one, as a member of the owner team
(Decide, unfreeze and roll back has the details):
kubectl get operations.delivery.pacto.trianalab.io -n prod # PHASE shows Held
pacto delivery approve -n prod <name> --decision approve
Many services at once¶
pacto init takes several folders and ends with a summary table.
--cluster drafts a contract for every workload no Pacto describes yet, from
your current kubeconfig context, in <out>/<namespace>/<name>. Pass
--namespace: without it, --cluster reads every namespace, system ones
included:
pacto init --plan services/checkout services/payments
pacto init --cluster --namespace shop --out drafts --plan
With the Pacto node agent running, the services a workload was seen calling
become dependencies, pinned to exactly the version seen running
(compatibility: "=2.3.0") for you to widen. Until you do, every new version of
that service asks your team. Without the agent, environment URLs name
dependencies, and one with no contract gets a placeholder range. Drafts are
never published, and an invalid one fails the command. The MCP tool
pacto_create with pactify: true takes paths, cluster and namespaces,
and only plans unless dry_run is false.
Generate contracts from what you already have
lists the permissions --cluster needs and walks through a whole folder of
services.
What's next¶
| Goal | Page |
|---|---|
| Decide on a waiting change or roll one back | Decide, unfreeze and roll back |
| Understand what happens to a change | Delivery |
| Fail a commit that would break a consumer | Put it in your repository |
| Check errors and response times after release | Health checks |
| Keep a signed record of every change | Signed records |
| Set up Stages by hand | Setup and gates |