Policy as Code with Open Policy Agent and Conftest
Did you ever make a change in a Kubernetes manifest only to find out upon deployment that you’ve messed up a piece of configuration. I know I have. Wouldn’t it be nice to catch these mistakes before deploying? Open Policy Agent (OPA) allows us to do so.
OPA is a policy engine which enables us to write our own policies and run them against our manifests. We will write these policies in Rego, a declarative language that is provided by OPA.
We will use a utility tool called Conftest to run the policies we’ve made against our configuration files.
Starting off
First off, let’s go ahead and install Conftest. OPA will be included in this installation.
brew tap instrumenta/instrumenta brew install conftest
Now that we’ve got everything installed we’ll need a manifest that we can run against our policy check. For this I’ve created an example deployment:
apiVersion: apps/v1 kind: Deployment metadata: name: example-app-deployment labels: app: example-app namespace: example spec: replicas: 1 selector: matchLabels: app: example-app template: metadata: labels: app: example-app spec: imagePullSecrets: - name: example-pull-secret containers: - name: example-app image: <registry>/example-app ports: - containerPort: 80
Writing the first policy
Our deployment currently has a single replica. Imagine our company requires all of our Deployments to have at least 2 replicas. We’ll be able to guarantee this by writing a policy for it.
package main deny_replicas[msg] { input.kind == "Deployment" # If it is a Deployment input.spec.replicas < 2 # And the replicas are < 2 msg := "Deployments must have 2 or more replicas" # Set the return variable to the error message and fail the test }
The policy is fairly straightforward. There are two noteworthy things. First is the input variable that is used. Input is a variable that Conftest creates for you. It takes the input we give it, parses it to JSON format and makes it available as an input variable in our policies.
The second noteworthy thing is something that might feel a bit unnatural. The name of this policy starts with deny. This means that if this test reaches the end, it will fail. If any of the statements in the test resolve to false, the test will abort and it will be considered a success.
Running the policy
Conftest by default requires our policies to be placed in a directory called policy. Therefore our directory structure should look something like this.
We can now run our policy check by executing the following command in our project directory.
As expected our test failed, since we did not meet our policy by having 2 replicas in our Deployment. Let’s go ahead and fix that and rerun our test.
This time the test passed with flying colors!
Create some more!
Testing our example deployment against our policy is fun, but in any realistic scenario you’d have a lot more configuration files and policies. Therefore I’ve added two more configurations, one for a Service and one for a Secret:
apiVersion: v1 data: .dockerconfigjson: supersecret kind: Secret metadata: name: example-pull-secret namespace: example type: kubernetes.io/dockerconfigjson
apiVersion: v1 kind: Service metadata: name: example-app labels: app: example-app namespace: example spec: type: ClusterIP ports: - port: 80 targetPort: http protocol: TCP name: http selector: app: example-app
Now that we’ve added those, we also want a new policy. Let’s say we want every manifest to explicitly declare its namespace. We can write a policy for this:
package main deny_namespace[msg] { not input.metadata.namespace # There is no namespace msg := "Resources must have a namespace" # Set the return variable to the error message; The test has reached the end thus has failed }
Just like the other policy we will deny any input for which all the statements resolve to true. If there is no namespace, input.metadata.namespace will resolve to false. By using the not operator we can resolve this to true if the namespace is missing.In order to run the tests we need to use a wildcard in our conftest command.
We now have 6 tests that have been executed. That’s because we run 2 policies for each of our 3 configuration files.
Unit testing our policies
OPA provides us with an easy way to unit test the policies we’ve written.
test_deny_replicas { deny_namespace[msg] with input as { "kind": "Deployment", "metadata": { "name": "example-app-deployment" } } }
test_deny_namespace { deny_namespace[msg] with input as { "kind": "Deployment", "spec": { "replicas": 2 } } }
Writing the tests is pretty straightforward. The name of the test must begin with test_. In the test itself all we need to do is call the policy we’d like to test, and provide it with our own input. After this we can run all our unit tests by performing the following command in our policy directory.
What’s next?
We’ve now learned how we can create policies and test our yml files against them. This is only the beginning. Conftest supports many more configuration files like JSON, XML and Dockerfile. Writing policies for all these and running them in your pipeline is a strong way to help guarantee certain standards for your configuration files and could prevent issues that would otherwise stay hidden until deployment.
For more information on Conftest reference their documentation. Make sure to check out the documentation for Open Policy Agent as well. There you will find their syntax, Rego, explained in detail.