{"id":13846252,"url":"https://github.com/zalando-incubator/stackset-controller","last_synced_at":"2026-01-17T14:56:56.073Z","repository":{"id":34084502,"uuid":"144249334","full_name":"zalando-incubator/stackset-controller","owner":"zalando-incubator","description":"Opinionated StackSet resource for managing application life cycle and traffic switching in Kubernetes","archived":false,"fork":false,"pushed_at":"2025-04-09T11:34:21.000Z","size":2411,"stargazers_count":181,"open_issues_count":24,"forks_count":26,"subscribers_count":16,"default_branch":"master","last_synced_at":"2025-04-13T00:43:37.404Z","etag":null,"topics":["blue-green","cloud","crd","kubernetes","stack","stackset","stackset-controller","traffic-switching","zalando"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zalando-incubator.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-08-10T07:01:06.000Z","updated_at":"2025-04-09T11:16:49.000Z","dependencies_parsed_at":"2023-10-12T21:24:20.198Z","dependency_job_id":"b9342f75-33a3-49ec-99f4-11457b28cb13","html_url":"https://github.com/zalando-incubator/stackset-controller","commit_stats":{"total_commits":405,"total_committers":35,"mean_commits":"11.571428571428571","dds":0.8148148148148149,"last_synced_commit":"5bd77d42369bc522e18912eb8c3aaca9cd27e754"},"previous_names":[],"tags_count":220,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalando-incubator%2Fstackset-controller","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalando-incubator%2Fstackset-controller/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalando-incubator%2Fstackset-controller/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zalando-incubator%2Fstackset-controller/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zalando-incubator","download_url":"https://codeload.github.com/zalando-incubator/stackset-controller/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248650419,"owners_count":21139672,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blue-green","cloud","crd","kubernetes","stack","stackset","stackset-controller","traffic-switching","zalando"],"created_at":"2024-08-04T18:00:21.124Z","updated_at":"2026-01-17T14:56:56.030Z","avatar_url":"https://github.com/zalando-incubator.png","language":"Go","funding_links":[],"categories":["Operators vs Controllers","Configuration Management"],"sub_categories":["Ingress"],"readme":"# Kubernetes StackSet Controller\n[![Build Status](https://travis-ci.org/zalando-incubator/stackset-controller.svg?branch=master)](https://travis-ci.org/zalando-incubator/stackset-controller)\n[![Coverage Status](https://coveralls.io/repos/github/zalando-incubator/stackset-controller/badge.svg?branch=master)](https://coveralls.io/github/zalando-incubator/stackset-controller?branch=master)\n\nThe Kubernetes StackSet Controller is a concept (along with an\nimplementation) for easing and automating application life cycle for\ncertain types of applications running on Kubernetes.\n\nIt is not meant to be a generic solution for all types of applications but it's\nexplicitly focusing on \"Web Applications\", that is, application which receive\nHTTP traffic and are continuously deployed with new versions which should\nreceive traffic either instantly or gradually fading traffic from one version\nof the application to the next one. Think Blue/Green deployments as one\nexample.\n\nBy default Kubernetes offers the\n[Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)\nresource type which, combined with a\n[Service](https://kubernetes.io/docs/concepts/services-networking/service/),\ncan provide some level of application life cycle in the form of rolling updates.\nWhile rolling updates are a powerful concept, there are some limitations for\ncertain use cases:\n\n* Switching traffic in a Blue/Green style is not possible with rolling\n  updates.\n* Splitting traffic between versions of the application can only be done by\n  scaling the number of Pods. E.g. if you want to give 1% of traffic to a new\n  version, you need at least 100 Pods.\n* Impossible to run smoke tests against a new version of the application before\n  it gets traffic.\n\nTo work around these limitations I propose a different type of resource called\nan `StackSet` which has the concept of `Stacks`.\n\nThe `StackSet` is a declarative way of describing the application stack as a\nwhole, and the `Stacks` describe individual versions of the\napplication. The `StackSet` also allows defining a \"global\" load balancer\nspanning all stacks of the stackset which makes it possible to switch\ntraffic to different stacks at the load balancer (for example Ingress) level.\n\n```mermaid\nflowchart\n  A[Ingress Controller] --\u003e|0%| B[Stack\\nVersion 1]\n  A[Ingress Controller] --\u003e|20%| C[Stack\\nVersion 2]\n  A[Ingress Controller] --\u003e|80%| D[Stack\\nVersion 3]\n```\n\nThe `StackSet` and `Stack` resources are implemented as\n[CRDs][crd].  A `StackSet` looks like this:\n\n```yaml\napiVersion: zalando.org/v1\nkind: StackSet\nmetadata:\n  name: my-app\nspec:\n  # optional Ingress definition.\n  ingress:\n    hosts: [my-app.example.org, alt.name.org]\n    backendPort: 80\n  # optional desired traffic weights defined by stack\n  traffic:\n  - stackName: mystack-v1\n    weight: 80\n  - stackName: mystack-v2\n    weight: 20\n  # optional percentage of required Replicas ready to allow traffic switch\n  # if none specified, defaults to 100\n  minReadyPercent: 90\n  stackLifecycle:\n    scaledownTTLSeconds: 300\n    limit: 5 # maximum number of scaled down stacks to keep.\n             # If there are more than `limit` stacks, the oldest stacks which are scaled down\n             # will be deleted.\n  stackTemplate:\n    spec:\n      version: v1 # version of the Stack.\n      replicas: 3\n      # optional autoscaler definition (will create an HPA for the stack).\n      autoscaler:\n        minReplicas: 3\n        maxReplicas: 10\n        metrics:\n        - type: CPU\n          averageUtilization: 50\n      # full Pod template.\n      podTemplate:\n        spec:\n          containers:\n          - name: skipper\n            image: ghcr.io/zalando/skipper:latest\n            args:\n            - skipper\n            - -inline-routes\n            - '* -\u003e inlineContent(\"OK\") -\u003e \u003cshunt\u003e'\n            - -address=:80\n            ports:\n            - containerPort: 80\n              name: ingress\n            resources:\n              limits:\n                cpu: 10m\n                memory: 50Mi\n              requests:\n                cpu: 10m\n                memory: 50Mi\n```\n\nThe above `StackSet` would generate a `Stack` that looks like this:\n\n```yaml\napiVersion: zalando.org/v1\nkind: Stack\nmetadata:\n  name: my-app-v1\n  labels:\n    stackset: my-app\n    stackset-version: v1\nspec:\n  ingress:\n    hosts: [my-app.example.org, alt.name.org]\n    backendPort: 80\n  replicas: 3\n  autoscaler:\n    minReplicas: 3\n    maxReplicas: 10\n    metrics:\n    - type: CPU\n      averageUtilization: 50\n  podTemplate:\n    spec:\n      containers:\n        image: ghcr.io/zalando/skipper:latest\n        args:\n        - skipper\n        - -inline-routes\n        - '* -\u003e inlineContent(\"OK\") -\u003e \u003cshunt\u003e'\n        - -address=:80\n        ports:\n        - containerPort: 80\n          name: ingress\n        resources:\n          limits:\n            cpu: 10m\n            memory: 50Mi\n          requests:\n            cpu: 10m\n            memory: 50Mi\n```\n\nFor each `Stack`, the StackSet controller creates a `Service`, a `Deployment`,\nand an optional `Ingress` resource automatically with the right labels. An\noptional `autoscaler` resource can also be created per stack for\nhorizontally scaling the deployment.\n\nFor the most part the `Stacks` will be dynamically managed by the\ncontroller and the users don't have to touch them. You can think of this similar\nto the relationship between `Deployments` and `ReplicaSets`.\n\nIf the `Stack` is deleted the related resources like `Service` and\n`Deployment` will be automatically cleaned up.\n\nThe `stackLifecycle` let's you configure two settings to change the cleanup\nbehavior for the `StackSet`:\n\n* `scaleDownTTLSeconds` defines for how many seconds a stack should not receive\n  traffic before it's scaled down.\n* `limit` defines the total number of stacks to keep. That is, if you have a\n  `limit` of `5` and currently have `6` stacks for the `StackSet` then it will\n  clean up the oldest stack which is **NOT** getting traffic. The `limit` is\n  not enforced if it would mean deleting a stack with traffic. E.g. if you set\n  a `limit` of `1` and have two stacks with `50%` then none of them would be\n  deleted. However, if you switch to `100%` traffic for one of the stacks then\n  the other will be deleted after it has not received traffic for\n  `scaleDownTTLSeconds`.\n\n## Features\n\n* Automatically create new Stacks when the `StackSet` is updated with a new\n  version in the `stackTemplate`.\n* Traffic switch between Stacks: The controller creates a new Ingress and/or\n  RouteGroup per Stack for StackSets with a `routegroup` or `ingress` specified\n  in the `spec`. The controller automatically updates each Stacks'\n  Ingress/RouteGroup when updating the main StackSet's `traffic` weights. The\n  ingress controller must implement the `TrafficSegment` predicate to\n  effectively switch traffic. For example, [Skipper] implements this predicate. \n* Safely switch traffic to scaled down stacks. If a stack is scaled down, it\n  will be scaled up automatically before traffic is directed to it.\n* Dynamically provision Ingresses per stack, with per stack host names. I.e.\n    `my-app.example.org`, `my-app-v1.example.org`, `my-app-v2.example.org`.\n* Automatically scale down stacks when they don't get traffic for a specified\n  period.\n* Automatically delete stacks that have been scaled down and are not getting\n  any traffic for longer time.\n* Automatically clean up all dependent resources when a `StackSet` or\n    `Stack` resource is deleted. This includes `Service`,\n    `Deployment`, `Ingress` and optionally `HorizontalPodAutoscaler`.\n* Command line utility (`traffic`) for showing and switching traffic between\n  stacks.\n* You can opt-out of the global `Ingress` creation with\n  `externalIngress:` spec, such that external controllers can manage\n  the Ingress or CRD creation, that will configure the routing into\n  the cluster.\n* You can use skipper's\n  [RouteGroups](https://opensource.zalando.com/skipper/kubernetes/routegroups)\n  to configure more complex routing rules.\n\n[Skipper]: https://opensource.zalando.com/skipper/reference/predicates/#trafficsegment\n\n## Docs\n\n* [How To's](/docs/howtos.md)\n\n### Kubernetes Compatibility\n\nThe StackSet controller works with Kubernetes `\u003e=v1.23`.\n\n## How it works\n\nThe controller watches for `StackSet` resources and creates `Stack` resources\nwhenever the version is updated in the `StackSet` `stackTemplate`. For each\n`StackSet` it will create an optional \"main\" `Ingress` resource and keep it up\nto date when new `Stacks` are created for the `StackSet`. For each `Stack` it\nwill create a `Deployment`, a `Service`. When specified in the parent\n`StackSet`, the controller will create also for each `Stack` an `Ingress` and/or\na `HorizontalPodAutoscaler` for the `Deployment`. The corresponding `Stack` owns\nthese resources, which are cleaned up if the stack is deleted.\n\n## Setup\n\nUse an existing cluster or create a test cluster with [kind](https://kind.sigs.k8s.io/docs/user/quick-start/)\n\n```bash\nkind create cluster --name testcluster001\n```\n\nThe `stackset-controller` can be run as a deployment in the cluster.\nSee [deployment.yaml](/docs/deployment.yaml).\n\nThe controller depends on the [StackSet](/docs/stackset_crd.yaml) and\n[Stack](/docs/stack_crd.yaml)\n[CRDs][crd].\nYou must install these into your cluster before running the controller:\n\n```bash\n$ kubectl apply -f docs/stackset_crd.yaml -f docs/stack_crd.yaml\n```\n\nAfter the CRDs are installed the controller can be deployed:\n\n*please adjust the controller version and cluster-domain to your environment*\n\n```bash\n$ kubectl apply -f docs/rbac.yaml -f docs/deployment.yaml\n```\n\n### Custom configuration\n\n## controller-id\n\nThere are cases where it might be desirable to run multiple instances of the\nstackset-controller in the same cluster, e.g. for development.\n\nTo prevent the controllers from fighting over the same `StackSet` resources\nthey can be configured with the flag `--controller-id=\u003csome-id\u003e` which\nindicates that the controller should only manage the `StackSets` which has an\nannotation `stackset-controller.zalando.org/controller=\u003csome-id\u003e` defined.\nIf the controller-id is not configured, the controller will manage all\n`StackSets` which does not have the annotation defined.\n\n## Quick intro\n\nOnce you have deployed the controller you can create your first `StackSet`\nresource:\n\n```bash\n$ kubectl apply -f docs/stackset.yaml\nstackset.zalando.org/my-app created\n```\n\nThis will create the stackset in the cluster:\n\n```bash\n$ kubectl get stacksets\nNAME          CREATED AT\nmy-app        21s\n```\n\nAnd soon after you will see the first `Stack` of the `my-app`\nstackset:\n\n```bash\n$ kubectl get stacks\nNAME                  CREATED AT\nmy-app-v1             30s\n```\n\nIt will also create `Ingress`, `Service`, `Deployment` and\n`HorizontalPodAutoscaler` resources:\n\n```bash\n$ kubectl get ingress,service,deployment.apps,hpa -l stackset=my-app\nNAME                                                     HOSTS                   ADDRESS                                  PORTS     AGE\ningress.networking.k8s.io/my-app-v1-traffic-segment      my-app.example.org      kube-ing-lb-3es9a....elb.amazonaws.com   80        7m\ningress.networking.k8s.io/my-app-v1                      my-app-v1.example.org   kube-ing-lb-3es9a....elb.amazonaws.com   80        7m\n\nNAME                TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)  AGE\nservice/my-app-v1   ClusterIP   10.3.204.136   \u003cnone\u003e        80/TCP   7m\n\nNAME                        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE\ndeployment.apps/my-app-v1   1         1         1            1           7m\n\nNAME                                            REFERENCE              TARGETS         MINPODS   MAXPODS   REPLICAS   AGE\nhorizontalpodautoscaler.autoscaling/my-app-v1   Deployment/my-app-v1   \u003cunknown\u003e/50%   3         10        0          20s\n```\n\nImagine you want to roll out a new version of your stackset. You can do this\nby changing the `StackSet` resource. E.g. by changing the version:\n\n```bash\n$ kubectl patch stackset my-app --type='json' -p='[{\"op\": \"replace\", \"path\": \"/spec/stackTemplate/spec/version\", \"value\": \"v2\"}]'\nstackset.zalando.org/my-app patched\n```\n\nSoon after, we will see a new stack:\n\n```bash\n$ kubectl get stacks -l stackset=my-app\nNAME        CREATED AT\nmy-app-v1   14m\nmy-app-v2   46s\n```\n\nAnd using the `traffic` tool we can see how the traffic is distributed (see\nbelow for how to build the tool):\n\n```bash\n./build/traffic my-app\nSTACK          TRAFFIC WEIGHT\nmy-app-v1      100.0%\nmy-app-v2      0.0%\n```\n\nIf we want to switch 100% traffic to the new stack we can do it like this:\n\n```bash\n# traffic \u003cstackset\u003e \u003cstack\u003e \u003ctraffic\u003e\n./build/traffic my-app my-app-v2 100\nSTACK          TRAFFIC WEIGHT\nmy-app-v1      0.0%\nmy-app-v2      100.0%\n```\n\nSince the `my-app-v1` stack is no longer getting traffic it will be scaled down\nafter some time and eventually deleted.\n\nIf you want to delete it manually, you can simply do:\n\n```bash\n$ kubectl delete stack my-app-v1\nstack.zalando.org \"my-app-v1\" deleted\n```\n\nAnd all the related resources will be gone shortly after:\n\n```bash\n$ kubectl get ingress,service,deployment.apps,hpa -l stackset=my-app,stack-version=v1\nNo resources found.\n```\n\n## Building\n\nThis project uses [Go modules](https://github.com/golang/go/wiki/Modules) as\nintroduced in Go 1.11 therefore you need Go \u003e=1.11 installed in order to build.\nIf using Go 1.11 you also need to [activate Module\nsupport](https://github.com/golang/go/wiki/Modules#installing-and-activating-module-support).\n\nAssuming Go has been setup with module support it can be built simply by running:\n\n```sh\n$ export GO111MODULE=on # needed if the project is checked out in your $GOPATH.\n$ make\n```\n\nNote that the Go client interface for talking to the custom `StackSet` and\n`Stack` CRD is generated code living in `pkg/client/` and\n`pkg/apis/zalando.org/v1/zz_generated_deepcopy.go`. If you make changes to\n`pkg/apis/*` then you must run `make clean \u0026\u0026 make` to regenerate the code.\n\nTo understand how this works see the upstream\n[example](https://github.com/kubernetes/apiextensions-apiserver/tree/master/examples/client-go)\nfor generating client interface code for CRDs.\n\n[crd]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/\n\n## Upgrade\n\n### \u003c= v1.0.0 to \u003e= v1.1.0\n\nClients that write the desired traffic switching value have to move\nfrom ingress annotation `zalando.org/stack-traffic-weights: '{\"mystack-v1\":80, \"mystack-v2\": 20}'`\nto stackset `spec.traffic`:\n\n```yaml\nspec:\n  traffic:\n  - stackName: mystack-v1\n    weight: 80\n  - stackName: mystack-v2\n    weight: 20\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzalando-incubator%2Fstackset-controller","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzalando-incubator%2Fstackset-controller","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzalando-incubator%2Fstackset-controller/lists"}