Good afternoon friends. In anticipation of the start of a new stream on the course
"DevOps Practices and Tools" we are sharing with you a new translation. Go.
Using Pulumi and general-purpose programming languages for Infrastructure as Code provides many advantages: having skills and knowledge, eliminating boilerplate in the code through abstraction, tools familiar to your team, such as IDEs and linters. All of these software engineering tools not only make us more productive, but also improve the quality of the code. Therefore, it is natural that the use of general-purpose programming languages allows you to implement another important practice in software development -
testing .
In this article, we will look at how Pulumi helps test our "infrastructure as code."
Why test the infrastructure?
Before going into details, it is worth asking the question: “Why do we need to test the infrastructure at all?” There are many reasons for this, and here are some of them:
- Unit testing individual functions or pieces of logic in your program
- Check the desired state of the infrastructure for compliance with certain restrictions.
- Detection of common errors, such as lack of storage bucket encryption or insecure, open access from the Internet to virtual machines.
- Verification of infrastructure provisioning.
- Performing runtime-testing of the logic of the application running inside your “programmed” infrastructure to check the health after provisioning.
- As we can see, there is a wide range of infrastructure testing options. Polumi has mechanisms for testing at every point in this spectrum. Let's get started and see how it works.
Unit testing
Pulumi programs are created in general-purpose programming languages such as JavaScript, Python, TypeScript, or Go. Therefore, the full power of these languages is available for them, including their tools and libraries, including test frameworks. Pulumi is multi-cloud, which means the ability to use any cloud providers for testing.
(In this article, despite being multi-lingual and multi-cloud, we use JavaScript and Mocha and focus on AWS. You can use Python
unittest
, the Go test framework or any other test framework you like. And, of course, Pulumi works great with Azure, Google Cloud, Kubernetes.)
As we have seen, there are several reasons why you might need to test your infrastructure code. One of them is the usual unit testing. Since your code may have functions - for example, to calculate CIDR, dynamically calculate names, tags, etc. - you probably want to test them. This is the same as writing regular unit tests for applications in your favorite programming language.
If you complicate things a bit, you can check how your program allocates resources. To illustrate, let's imagine that we need to create a simple EC2 server and we want to be sure of the following:
- Instances have a
Name
tag. - Instances should not use the userData inline script - we must use the AMI (image).
- There should not be SSH open on the Internet.
This example is written based on
my aws-js-webserver example :
index.js:
"use strict"; let aws = require("@pulumi/aws"); let group = new aws.ec2.SecurityGroup("web-secgrp", { ingress: [ { protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] }, { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] }, ], }); let userData = `#!/bin/bash echo "Hello, World!" > index.html nohup python -m SimpleHTTPServer 80 &`; let server = new aws.ec2.Instance("web-server-www", { instanceType: "t2.micro", securityGroups: [ group.name ],
This is the basic Pulumi program: it simply allocates the EC2 security group and instance. However, it should be noted that here we violate all three rules set forth above. Let's write tests!
Writing tests
The general structure of our tests will look like regular Mocha tests:
ec2tests.js
test.js: let assert = require("assert"); let mocha = require("mocha"); let pulumi = require("@pulumi/pulumi"); let infra = require("./index"); describe("Infrastructure", function() { let server = infra.server; describe("#server", function() {
Now let's write our first test: make sure that the instances have a
Name
tag. To verify this, we simply get the EC2 instance object and check the corresponding
tags
property:
It looks like a regular test, but with a few features worthy of attention:
- Since we request the state of the resource before deployment, our tests are always executed in the “plan” (or “preview" mode). Thus, there are many properties whose values simply will not be received or will not be determined. This includes all output properties calculated your cloud provider. For our tests, this is normal - we only check the input data. We will come back to this issue later when it comes to integration tests.
- Since all properties of Pulumi resources are "outputs", and many of them are calculated asynchronously, we need to use the apply method to access the values. This is very similar to promises and
then
. - Since we use several properties in order to show the resource URN in the error message, we need to use the
pulumi.all
function to combine them. - Finally, since these values are computed asynchronously, we need to use Mocha's built-in asynchronous feature with a
done
callback or a promise return.
After we configure everything, we will have access to the input data as simple JavaScript values. The
tags
property is a map (associative array), so we’ll just make sure that it is (1) not false, and (2) there is a key for
Name
. It is very simple and now we can check anything!
Now let's write our second check. This is even simpler:
And finally, we will write the third test. This will be a bit more complicated, because we are looking for login rules associated with a security group, which may be many, and CIDR ranges in these rules, which may also be many. But we managed:
That's all. Now let's run the tests!Running tests
In most cases, you can run tests in the usual way using the test framework of your choice. But there is one Pulumi feature that you should pay attention to.
Typically, Pulimi CLI (Command Line interface, command line interface) is used to start Pulumi programs. It configures the runtime of the language, controls the Pulumi engine starts, so that you can record operations with resources and include them in the plan, etc. However, there is one problem. When launched under the control of your test framework, there will be no communication between the CLI and the Pulumi engine.
To get around this problem, we just need to specify the following:
- The name of the project, which is contained in the environment variable
PULUMI_NODEJS_PROJECT
(or, more generally, PULUMI__PROJECT ).
The name of the stack that is specified in the environment variable PULUMI_NODEJS_STACK
(or, more generally, PULUMI__ STACK).
Your stack configuration variables. They can be obtained using the PULUMI_CONFIG
environment PULUMI_CONFIG
and their format is a JSON map with key / value pairs.
The program will issue warnings indicating that at run time a connection to the CLI / engine is not available. This is important, because in reality, your program will not deploy anything and this may come as a surprise if this is not what you wanted to do! To tell Pulumi that this is exactly what you need, you can set PULUMI_TEST_MODE
to true
.
Imagine we need to specify the name of the project in my-ws
, the name of the dev
stack, and the AWS us-west-2
. The command line for running Mocha tests will look like this:
$ PULUMI_TEST_MODE=true \ PULUMI_NODEJS_STACK="my-ws" \ PULUMI_NODEJS_PROJECT="dev" \ PULUMI_CONFIG='{ "aws:region": "us-west-2" }' \ mocha tests.js
Doing this, as expected, will show us that we have three fallen tests!
Infrastructure #server 1) must have a name tag 2) must not use userData (use an AMI instead) #group 3) must not open port 22 (SSH) to the Internet 0 passing (17ms) 3 failing 1) Infrastructure #server must have a name tag: Error: Missing a name tag on server urn:pulumi:my-ws::my-dev::aws:ec2/instance:Instance::web-server-www 2) Infrastructure #server must not use userData (use an AMI instead): Error: Illegal use of userData on server urn:pulumi:my-ws::my-dev::aws:ec2/instance:Instance::web-server-www 3) Infrastructure #group must not open port 22 (SSH) to the Internet: Error: Illegal SSH port 22 open to the Internet (CIDR 0.0.0.0/0) on group
Let's fix our program:
"use strict"; let aws = require("@pulumi/aws"); let group = new aws.ec2.SecurityGroup("web-secgrp", { ingress: [ { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] }, ], }); let server = new aws.ec2.Instance("web-server-www", { tags: { "Name": "web-server-www" }, instanceType: "t2.micro", securityGroups: [ group.name ],
And then re-run the tests:
Infrastructure #server ✓ must have a name tag ✓ must not use userData (use an AMI instead) #group ✓ must not open port 22 (SSH) to the Internet 3 passing (16ms)
Everything went well ... Hooray! ✓ ✓ ✓
That's all for today, but we'll talk about testing deployment in the second part of the translation ;-)