Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Peter Benjamin (they/them)
Peter Benjamin (they/them)

Posted on

     

Unit Test Your Configuration Files

pile of paper garbage
Infrastructure as Code. Photo courtesy:@Bass Emmen

Originally published athttps://pbnj.dev

Table of Contents

Overview

The era of Infrastructure-as-Code (IaC) has unlocked tremendous developer
productivity and agility features. Now, as an Engineer, we can declare our
infrastructure and environments as structured data in configuration files, such
as Terraform templates, Dockerfiles, and Kubernetes manifests.

However, this agility and speed of provisioning and configuring infrastructure
comes with a high risk of bugs in the form of misconfigurations.

Fortunately, we can solve this problem just as we can solve for other bugs in
our products, by writingunit tests.

One such tool that can help us unit test our configuration files is
conftest. What is unique about
conftest is that it uses
Open-Policy-Agent (OPA) and a policy
language, called
Rego to
accomplish this.

This might appear difficult at first, but it will start to make sense.

Let's explore 2 use-cases where we can test our configurations!

Getting Started

First, some prerequisites:

  • conftest:
    • macOS:brew install instrumenta/instrumenta/conftest
  • (Optional)opa:
    • macOS:brew install opa

Dockerfile

Let's say we want to prevent some images and/or tags (e.g.latest).

We need to create a simple Dockerfile:

FROM kalilinux/kali-linux-docker:latestENTRYPOINT ["echo"]
Enter fullscreen modeExit fullscreen mode

Now, we need to create our first unit test file, let's call ittest.rego, and
place it in a directory, let's call itpolicy (this is configurable).

packagemaindisallowed_tags:=["latest"]disallowed_images:=["kalilinux/kali-linux-docker"]deny[msg]{input[i].Cmd=="from"val:=input[i].Valuetag:=split(val[i],":")[1]contains(tag,disallowed_tags[_])msg=sprintf("[%s] tag is not allowed",[tag])}deny[msg]{input[i].Cmd=="from"val:=input[i].Valueimage:=split(val[i],":")[0]contains(image,disallowed_images[_])msg=sprintf("[%s] image is not allowed",[image])}
Enter fullscreen modeExit fullscreen mode

Assuming we are in the right directory, we can test our Dockerfile:

$lsDockerfile      policy/$conftesttest-i Dockerfile ./DockerfileFAIL - ./Dockerfile -[latest] tag is not allowedFAIL - ./Dockerfile -[kalilinux/kali-linux-docker] image is not allowed
Enter fullscreen modeExit fullscreen mode

Just to be sure, let's change this Dockerfile to pass the test:

# FROM kalilinux/kali-linux-docker:latestFROM debian:busterENTRYPOINT ["echo"]
Enter fullscreen modeExit fullscreen mode
$lsDockerfile      policy/$conftesttest-i Dockerfile ./DockerfilePASS - ./Dockerfile - data.main.deny
Enter fullscreen modeExit fullscreen mode

"It works! But I don't understand how," I hear you thinking to yourself.

Let's break the Rego syntax down:

  • package main is a way for us to put some rules that belong together in anamespace. In this case, we named itmain becauseconftest defaults to it,but we can easily do something likepackage docker and then runconftest test -i Dockerfile --namespace docker ./Dockerfile
  • disallowed_tags &disallowed_images are just simple variables that hold anarray of strings
  • deny[msg] { ... } is the start of the deny rule and it means that theDockerfile should be rejected and the user should be given an error messagemsg if the conditions in the body (i.e.{ ... }) are true
  • Expressions in the body of the deny rule are treated as logical AND. Forexample:
1==1# IF 1 is equal to 1contains("foobar","foo")# AND "foobar" contains "foo"# This would trigger the deny rule
Enter fullscreen modeExit fullscreen mode
  • input[i].Cmd == "from" checks if the Docker command isFROM.input[i]means we can have multiple Dockerfiles being tested at once. This will iterateover them
  • The next 2 lines are assignments just to split a string and store some data invariables
  • contains(tag, disallowed_tags[_]) will return true if thetag we obtainedfrom the Dockerfile contains one of thedisallowed_tags.array[_] syntaxmeans iterate over values
  • msg := sprinf(...) creates the message we want to tell our user if this denyrule is triggered
  • The seconddeny[msg] rule checks that the image itself is not on theblocklist.

Kubernetes

Let's say we want to ensure that all pods are running as a non-root user.

We need to create our deployment

$mkdir-p kubernetes$cat<<EOF >./kubernetes/deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:  name: nginx-deployment  labels:    app: nginxspec:  replicas: 3  selector:    matchLabels:      app: nginx  template:    metadata:      labels:        app: nginx    spec:      containers:        - name: nginx          image: nginx:1.7.9          ports:            - containerPort: 80EOF
Enter fullscreen modeExit fullscreen mode

Now, we need to create our unit test:

$mkdir-p ./kubernetes/policy$cat<<EOF >./kubernetes/policy/test.regopackage mainname := input.metadata.namedeny[msg] {  input.kind == "Deployment"  not input.spec.template.spec.securityContext.runAsNonRoot  msg = sprintf("Containers must run as non root in Deployment %s. See: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", [name])}EOF
Enter fullscreen modeExit fullscreen mode

And, let's run it:

conftesttest-i yaml ./kubernetes/deployment.yamlFAIL - ./kubernetes/deployment.yaml - Containers must run as non rootinDeployment nginx-deployment. See: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
Enter fullscreen modeExit fullscreen mode

This is a bit more straightforward:

  • Get themetadata.name from theinput (which is the Kubernetes Deploymentyaml file)
  • Create a deny rule that is triggered if:
    • input.kind isDeployment and
    • securityContext.runAsNonRoot is not set
  • Return an error message to the user that containers must run as non-root andpoint them to the docs.

Next Steps

So, where to go from here?

The Rego language is vast and it can take a bit to wrap your head around how it
works. You can even send and receive HTTP requests inside Rego.

I recommend reading the docs to learn more about Rego's capabilities:

I also barely scratched the surface withconftest in this blog post. The
repository has a nice list of
examples that you should
peruse at your leisure.conftest even supports sharing policies via uploading
OPA bundles to OCI-compliant registries, e.g.conftest push ...,
conftest pull ....

Lastly, if you have any questions, the OPA community is friendly and welcoming.
Feel free to join the#conftest channel in
OPA Slack.

Happy coding!

Top comments(5)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
pbnj profile image
Peter Benjamin (they/them)
{Security,Software,Infrastructure} Engineer. {Rust,Go,TypeScript} Enthusiast. K8s herder.
  • Email
  • Education
    MS, Cyber Security and Ethical Hacking
  • Pronouns
    they/them
  • Work
    Software Engineer
  • Joined
• Edited on• Edited

Hi Phil,

That's a great question.

I don't know of many tools or testing frameworks that are general enough to be applied to any structured data. One such tool that falls under this category isterratest, which requires you to write your tests in Go (I haven't had a chance to explore it).

On the other hand, there are a number of specialized testing frameworks/tools for specific types of configuration files, like:

So, it depends on your use-case.

CollapseExpand
 
ievansanz profile image
Ian Evans
  • Joined

Great article Peter, thank you!
I've been looking for further information (and bending my mind to OPA) on what you explained for...
input[i].Cmd == "from"
... I understandinput is an OPA reserved construct but how did you come to understand.Cmd in relation to Dockerfiles (or is it more OPA generic)? I've seen several docker examples that all leverageinput[i].Cmd == but always assumed this was some Conftest special-sauce as an iterator for the commands within a Dockerfile.
Have you found a Conftest or OPA explaination that explains this?
Thanks again,
Ian

CollapseExpand
 
ievansanz profile image
Ian Evans
  • Joined

Aha, found that conftest parser for Dockerfile implements the .Cmd... case closed!

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

{Security,Software,Infrastructure} Engineer. {Rust,Go,TypeScript} Enthusiast. K8s herder.
  • Education
    MS, Cyber Security and Ethical Hacking
  • Pronouns
    they/them
  • Work
    Software Engineer
  • Joined

More fromPeter Benjamin (they/them)

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp