We're currently running skoap with a single route like this
Path("/*") && Method("GET")
-> auth("/services")
-> dropRequestHeader("Authorization")
-> basicAuth("username", "secret")
-> "https://special.service.example.com";
with auth("/services") -> oauthTokeninfoAllKV("realm","/services") for newer skipper versions.
We would now need an ingress definition with incoming host names defined and one disconnected kubernetes service, that is not deleted in later and has a special meaning for this route. We have no pods involved in this route. The current zalando.org/skipper-routes annotation can be used as workaround, but needs to have a kubernetes service that exists, too.
https://github.com/zalando/skipper/issues/512
We might want to think about having a customer skipper route CRD for the use case of custom skipper routes.
Request for comments:
apiVersion: zalando.org/v1
kind: SkipperRoute
metadata:
name: "{{{APPLICATION}}}-blogposts-put"
labels:
application: "{{{APPLICATION}}}"
version: "{{{BUILD_VERSION}}}"
spec:
ingress: "name-reference" # optional, copy base predicates and filters from ingress
filters: # optional
- localRatelimit(“1m”, 10)
- enableAccessLog()
- flowId("reuse")
predicates:
- JWTPayloadAllKV("iss", "https://idp.example.com", “https://identity.example.com/role”, “publisher”)
- Method(“PUT")
- PathSubtree(“/blogposts/:id”)
My Questions:
- [ ] How should the behavior of the status field be?
@aermakov-zalando do you have ideas for the status field, because you talked about "the usual pattern is that the controller writes the latest value from metadata.generation into status.observedGeneration when it does stuff". One problem is that we have N skippers and only one object status field, but maybe you have an idea.
No idea. I'd either create a separate controller that would get the results from all Skipper instances and update the status, or use leader election in Skippers and collect the data in one of the instances. Both of these would be annoying to implement, though.
A couple of suggestions:
We think about creating a RouteGroup CRD that can be traffic switched as a set of routes with https://github.com/zalando-incubator/stackset-controller
We started to work on the skipper RouteGroup CRD. Some example yaml files you can see in https://github.com/zalando/skipper/tree/feature/routegroup-crd/dataclients/kubernetes/fixtures
Why:
apiVersion: zalando.org/v1
kind: RouteGroup
metadata:
name: my-routes
spec:
hosts:
- foo.example.org
backends:
- serviceName: foo-service
servicePort: 80
paths:
- path: /
One example case that happens more often is only possible with more than 1 ingress: ingress + redirect:
apiVersion: zalando.org/v1
kind: RouteGroup
metadata:
name: my-routes
spec:
backends:
- serviceName: foo-service
servicePort: 80
paths:
- path: /login
method: get
configs:
- filters:
- redirectTo(301, "https://login.example.org")
- path: /
apiVersion: zalando.org/v1
kind: RouteGroup
metadata:
name: my-routes
spec:
hosts:
- www.complex.example.org
- complex.example.org
backends:
- serviceName: foo-service-v1
servicePort: 80
paths:
- path: /api/resource
method: post
configs:
- filters:
- ratelimit(20, "1m")
- oauthTokeninfoAllKV("iss", "https://accounts.google.com", "email", "[email protected]")
predicates:
- JWTPayloadAllKV("iss", "https://accounts.google.com", "email", "[email protected]")
- path: /api/resource/*
method: get
configs:
- filters:
- clientRatelimit(100, "1m", "Authorization")
- oauthTokeninfoAllScope("read.resource", "list.resource")
apiVersion: zalando.org/v1
kind: RouteGroup
metadata:
name: my-routes
annotations:
zalando.org/backend-weights:
{"foo-service-v1": 80, "foo-service-v2": 20}
spec:
backends:
- serviceName: foo-service-v1
servicePort: 80
- serviceName: foo-service-v2
servicePort: 80
paths:
- path: /api/resource
method: post
configs:
- filters:
- ratelimit(20, "1m")
- oauthTokeninfoAllKV("iss", "https://accounts.google.com", "email", "[email protected]")
predicates:
- JWTPayloadAllKV("iss", "https://accounts.google.com", "email", "[email protected]")
- path: /api/resource/*
method: get
A/B test via cookie canary
apiVersion: zalando.org/v1
kind: RouteGroup
metadata:
name: my-routes
spec:
backends:
- serviceName: service-b
servicePort: 80
paths:
- path: /
config:
- filters:
- responseCookie("canary", "A")
predicates:
- Traffic(.1)
backends:
- serviceName: service-a
servicePort: 80
- path: /
config:
- filters:
- responseCookie("canary", "B")
- path: /
config:
- predicates:
- Cookie("canary", "team-foo")
- path: /
config:
- predicates:
- Cookie("canary", "A")
backends:
- serviceName: service-a
servicePort: 80
- path: /
config:
- predicates:
- Cookie("canary", "B")
Looks very nice! A couple of suggestions/questions:
filters/predicates into the path object. The extra config key doesn't really bring any value here. They also probably shouldn't be an array, unless I'm missing a use case?spec or in status). It's a new resource anyway, no need to have it as a weird annotation.path here an explicit path, or a prefix? <shunt>, or a URL)? Maybe worth adding a destination key, with several options, e.g. destination:
shunt: true
destination:
url: https://foo.zalan.do
destination:
backends:
- serviceName: service-a
servicePort: 80
for destination: let's use a backend type enumeration, shunt/loopback/network/service, where service is the default and resolves to the LB endpoints.
@aermakov-zalando The config key is used to have filters+predicates as a list of configs, which is an internal use case with fabric. I am open for renaming config
The problem we discussed with the traffic switching is that we have multiple controllers writing this object and if we have the annotation it's more safe (IIRC) to change from stackset controller the traffic settings. The problem with status is that it should reflect the current state not the goal.
The destinations I don't fully understand now with @aryszka's addition. Do you mean
destination:
backend_type: <shunt>
backends:
- serviceName: service-a
servicePort: 80
Additional questions: Would we ignore backends, if we have a backend_type?
@szuecs Could you describe the use case, or how this would be used in practice? I still don't understand why an extra key is necessary.
Having traffic weights in the spec is perfectly fine, having it in the annotation doesn't really get you any extra safety.
@szuecs yes, something like that.
the possible types could be: shunt, loopback, service (default), external, and lb.
shunt would not require further fields, loopback neither. Service would be the default, but it would require the fields as you described above. The external would mean any free URL address (without path).
Shunt:
destinations:
- type: shunt
Loopback:
destinations:
- type: loopback
Service, explicit, no list required:
destinations:
- type: service
serviceName: service-a
port: 80
Service, default, no list required:
destinations:
- serviceName: service-a
port: 80
Load Balanced (the above service still can be load balanced when applied, here just the user may explicitly set LB endpoints):
destinations:
- type: lb
algorithm: consistent-hash
endpoints:
- address: 1.2.3.4:9000
External:
destinations:
- type: external
address: https://foo.example.org
see the above suggestion for defining the variants of a single destination. As we discussed since then, we will probably have to support multiple destinations for the traffic switching.
@aermakov-zalando
Having traffic weights in the spec is perfectly fine, having it in the annotation doesn't really get you any extra safety.
The only point against the spec would be if we want to support re-applying the RouteGroup without overwriting the traffic state. This would not be needed in a stackset case if we store the desired/actual traffic somewhere other than the RouteGroup.
@mikkeloscar apply shouldn't touch any keys that it didn't create, so I don't think it'll be an issue.
Can we have some design proposal document (Markdown file in this repo?) to have a nice overview on how it will look like? It's really hard for me to follow this comment thread as I was not involved from the start.
@hjacobs good idea, creating one. Just one last comment, to jot down what we just discussed IRL. The traffic weights, could also be part of the destination entries, s.g. like:
destinations:
- type: service
serviceName: service-a-1
port: 80
- type: service
serviceName: service-a-2
port: 80
trafficWeight: 0.2
@aermakov-zalando Use case for example admins have a different scope check than a client service for the same route. Different clients, filtered by Predicates could get different filter settings.
@szuecs Couldn't they just create 2 path objects then? I'd rather reduce the noise in the most common use case (path object mapping to 1 route). If one object is supposed to generate multiple routes, then why is path not an array as well?
@aermakov-zalando true it can be compiled into the list of paths. I am fine with that. I will soon post a proposal and link it
There are some open questions raised by @aryszka:
My answers would be:
@szuecs I'd make it controllable per-route (because why not), e.g. have path: / versus exactPath: /, or path: /, exactPath: true. No need to have it only globally.
I've only breifly looked through the proposal so far but I have a few thoughts:
defaultBackends property is the way to go or if it might be better as a default bool property on the backend.path / pathSubtree support regex or wildcard paths?Will the CRD controller create an Ingress resource in kubernetes? If not, will there be any way to use RouteGroup in conjunction with external-dns?
Good point. This should be addressed in the proposal.
@MarcusNoble: thanks for your input!
path and pathSubtree will not support regexps, because the routing tree does not allow this. I believe, that most users should not use regexp, because it's slow, consumes a lot of resources and regexps are not intuitive. We support pathRegexp so you can use just this without path or pathSubtree.I will add a section about the components that need to be changed in order to make the RouteGroup useful to the proposal.
FYI: I added https://github.com/zalando/skipper/blob/feature/routegroup-crd/dataclients/kubernetes/crd.md#external-components-that-need-to-be-changed to show what external components need to be changed and added https://github.com/zalando/skipper/blob/feature/routegroup-crd/dataclients/kubernetes/crd.md#why-not-adapt-other-solutions to show why we do not adapt SMI or other CRDs.
https://github.com/zalando/skipper/pull/1180 is in progress
This was done also for external-dns and kube-ingress-aws-controller and is in use by Zalando.
Most helpful comment
Can we have some design proposal document (Markdown file in this repo?) to have a nice overview on how it will look like? It's really hard for me to follow this comment thread as I was not involved from the start.