Why Configuration Matters
If I’m going to get an error for my code, I like to get an error as soon as possible. Unit test failures are better than integration test failures. I prefer compiler errors to unit test failures – that’s what makes TypeScript great. Going even further, a syntax highlighter is a very proximate feedback of errors – if that keyword doesn’t turn green, I can fix the spelling with almost no conscious burden.
Shifting from coding to configuration, I like narrow configuration systems that make it easy to specify a valid configuration and tell me as quickly as possible when I’ve done something wrong. In fact, my dream configuration specification wouldn’t allow me to specify something invalid. Remember Newspeak from 1984? A language so narrow that expressing ungood thoughts become impossible. Apparently, I like my programming and configuration languages to be dystopic and Orwellian.
If you can’t further narrow the language of your configuration without giving away core functionality, the next best option is to narrow the scope of configuration. I think about this in reverse – if I am observing some behavior from the system and say to myself, “Huh, that’s weird”, how much configuration do I have to look at before I understand? It’s great if this is a small and expanding ring – look at config that’s very local to this particular object, then the next layer up (in Kubernetes, maybe the rest of the namespace), and so on to what is hopefully a very small set of global config. Ever tried to debug a program with global variables all over the place? Not fun.
My three principles of ideal config:
- Narrow Like Newspeak, don’t allow me to even think of invalid configuration.
- Scope Only let me affect a small set of things associated with my role.
- Time Tell me as early as possible if it’s broken.
Declarative config is readily narrow. The core philosophy of declarative config is saying what you want, not how you get it. For example, “we’ll meet at the Denver Zoo at noon” is declarative. If instead I specify driving directions to the Denver Zoo, I’m taking a much more imperative approach. What if you want to bike there? What if there is road construction and a detour is required? The value of declarative config is that if we focus on what we want, instead of how to get it, it’s easier for me to bring my controller (my car’s GPS) and you to bring yours (Google Maps in the Bike setting).
On the other hand, a big part of configuration is pulling together a bunch of disparate pieces of information together at the last moment, from a bunch of different roles (think humans, other configuration systems and controllers) just before the system actually starts running. Some amount of flexibility is required here.
Does Cloud Native Get Us To Better Configuration?
I think a key reason for the popularity of Kubernetes is that it has a great syntax for specifying what a healthy, running microservice looks like. Its syntax is powerful enough in all the right places to be practical for infrastructure.
Service meshes like Istio robustly connect all the microservices running in your cluster. They can adaptively route L7 traffic, provide end-to-end mTLS based encryption, and provide circuit breaking and fault injection. The long feature list is great, but it’s not surprising that the result is a somewhat complex set of configuration resources. It’s a natural result of the need for powerful syntax to support disparate use cases coupled with rapid development.
Enabling Fine-grained RBAC with Traffic Claim Enforcer
At Aspen Mesh, we found users (including ourselves) spending too much time understanding misconfiguration. The first way we addressed that problem was with Istio Vet, which is designed to warn you of probably incorrect or incomplete config, and provide guidance to fix it. Sometimes we know enough that we can prevent the misconfiguration by refusing to allow it in the first place. For some Istio config resources, we do that using a solution we call Traffic Claim Enforcer.
There are four Istio configuration resources that have global implications: VirtualService, Gateway, ServiceEntry and DestinationRule. Whenever you create one of these resources, you create it in a particular namespace. They can affect how traffic flows through the service mesh to any target they specify, even if that target isn’t in the current namespace. This surfaces a scope anti-pattern – if I’m observing weird behavior for some service, I have to examine potentially all DestinationRules in the entire Kubernetes cluster to understand why.
That might work in the lab, but we found it to be a serious problem for applications running in production. Not only is it hard to understand the current config state of the system, it’s also easy to break. It’s important to have guardrails that make it so the worst thing I can mess up when deploying my tiny microservice is my tiny microservice. I don’t want the power to mess up anything else, thank you very much. I don’t want sudo. My platform lead really doesn’t want me to have sudo.
Traffic Claim Enforcer is an admission webhook that waits for a user to configure one of those resources with global implications, and before allowing will check:
- Does the resource have a narrow scope that affects only local things?
- Is there a TrafficClaim that grants the resource the broader scope requested?
A TrafficClaim is a new Kubernetes custom resource we defined that exists solely to narrow and define the scope of resources in a namespace. Here are some examples:
kind: TrafficClaim apiVersion: networking.aspenmesh.io/v1alpha3 metadata: name: allow-public namespace: cluster-public claims: # Anything on www.example.com - hosts: [ "www.example.com" ] # Only specific paths on foo.com, bar.com - hosts: [ "foo.com", "bar.com" ] ports: [ 80, 443, 8080 ] http: paths: exact: [ "/admin/login" ] prefix: [ "/products" ] # An external service controlled by ServiceEntries - hosts: [ "my.external.com" ] ports: [ 80, 443, 8080, 8443 ]
TrafficClaims are controlled by Kubernetes Role-Based Access Control (RBAC). Generally, the same roles or people that create namespaces and set up projects would also create TrafficClaims for those namespaces that need power to define service mesh traffic policy outside of their namespace scope. Rule 1 about local scope above can be explained as “every namespace has an implied TrafficClaim for namespace-local traffic policy”, to avoid requiring a boilerplate TrafficClaim.
A pattern we use is to put global config into a namespace like “istio-public” – that’s the only place that needs TrafficClaims for things like public DNS names. Or you might have a couple of namespaces like “istio-public-prod” and “istio-public-dev” or similar. It’s up to you.
Traffic Claim Enforcer does not prevent you from thinking of invalid config, but it does help to limit scope. If I’m trying to understand what happens when traffic goes to my microservice, I no longer have to examine every DestinationRule in the system. I only have to examine the ones in my namespace, and maybe some others that have special TrafficClaims (and hopefully keep that list small).
Traffic Claim Enforcer also provides an early failure for config problems. Without it, it is easy to create conflicting DestinationRules even in separate namespaces. This is a problem that Istio-Vet will tell you about, but cannot fix – it doesn’t know which one should have priority. If you define TrafficClaims, then Traffic Claim Enforcer can prevent you from configuring it at all.
Hat tip to my colleague, Brian Marshall, who developed the initial public spec for TrafficClaims. The Istio community is undertaking a great deal of work to scope/manage config aimed at improving system scalability. We made Traffic Claim Enforcer with a focus on scoping to improve the human config experience as it was a need expressed by several of our users. We’re optimistic that the way Traffic Claim Enforcer helps with human concerns will complement the system scalability side of things.
If you want to give Traffic Claim Enforcer a spin, it’s included as part of Aspen Mesh. By default it doesn’t enforce anything, so out-of-the-box it is compatible with Istio. You can turn it on globally or on a namespace-by-namespace basis.
Click play below to check it out in action!