Testing your infrastructure as code with Pulumi. Part 1

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



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:





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 ], // reference the group object above ami: "ami-c55673a0" // AMI for us-east-2 (Ohio), userData: userData // start a simple web server }); exports.group = group; exports.server = server; exports.publicIp = server.publicIp; exports.publicHostName = server.publicDns;
      
      





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() { // TODO(check 1):    Name. // TODO(check 2):    inline- userData. }); let group = infra.group; describe("#group", function() { // TODO(check 3):    SSH,   . }); });
      
      





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:



  // check 1:    Name. it("must have a name tag", function(done) { pulumi.all([server.urn, server.tags]).apply(([urn, tags]) => { if (!tags || !tags["Name"]) { done(new Error(`Missing a name tag on server ${urn}`)); } else { done(); } }); });
      
      





It looks like a regular test, but with a few features worthy of attention:





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:



  // check 2:    inline- userData. it("must not use userData (use an AMI instead)", function(done) { pulumi.all([server.urn, server.userData]).apply(([urn, userData]) => { if (userData) { done(new Error(`Illegal use of userData on server ${urn}`)); } else { done(); } }); });
      
      







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:



  // check 3:    SSH,   . it("must not open port 22 (SSH) to the Internet", function(done) { pulumi.all([ group.urn, group.ingress ]).apply(([ urn, ingress ]) => { if (ingress.find(rule => rule.fromPort == 22 && rule.cidrBlocks.find(block => block === "0.0.0.0/0"))) { done(new Error(`Illegal SSH port 22 open to the Internet (CIDR 0.0.0.0/0) on group ${urn}`)); } else { done(); } }); });
      
      





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:






All Articles