Testing servers with Inspec

Just as your application, code should be covered with unit and integration tests. Infrastructure as Code should have good test coverage as well. There are multiple ways to do infrastructure code tests. They are as follows:

  • Test only the code, without creating a real infrastructure (with tools such as ChefSpec and rspec-puppet)
  • Test a single server
  • Test the complete system

We won't look at the first option to test our infrastructure in this book. For guidance on how to test the complete system (with all the cloud resources, servers, and so on), look at Chapter 7, Collaborative Infrastructure. As for this chapter, let's check one way to test a single server with Terraform and Inspec.

Inspec is a testing framework, written in Ruby. It provides an easy-to-understand DSL that allows you to write human-readable descriptions of what the server should run, which packages it should have, and so on. The only thing Inspec needs is the address of the server. But first things first: let's install Inspec.

Inspec is a Ruby gem, so you need to have Ruby installed on your system. Depending on your operating system, it could be either already installed or available via package manager (yum, dnf, apt-get, homebrew, and so on). Once Ruby is installed, installing Inspec is just one command away:

$> gem install inspec

Let's write a test that will check that the  wget package is installed. Create a new folder specs/, and place a file named base_spec.rb inside of it with the following contents:

describe package('wget') do 
  it { should be_installed } 
end 

Note

For complete documentation on Inspec, consult the official website http://inspec.io/.

The only thing that prevents us from running this test is the missing IP address of the instance. And that's where Terraform will help us. First, update ./modules/application/application.tf to have the output defined at the very button:

output "public_ip" { 
  value = "${aws_instance.app-server.public_ip}" 
} 

Accordingly, add an output to the root template.tf file:

output "mighty_trousers_public_ip" { 
  value = "${module.mighty_trousers.public_ip}" 
} 

In order for instance to actually receive public IP automatically, the subnet needs to have a special option enabled. Modify the aws_subnet.public resource to enable map_public_ip_on_launch:

resource "aws_subnet" "public" { 
  vpc_id = "${aws_vpc.my_vpcmy_vpc.id}" 
  cidr_block = "${lookup(var.subnet_cidrs, "public")}" 
  map_public_ip_on_launch = true 
} 

Tip

You can remove all VPC peering code from your template, we won't need it anymore.

And now apply the template as you've done so many times by now:

$> terraform apply

Give EC2 some time to completely set up the instance with cloud-init (remember we are passing the custom cloud-init script generated with the template_file data resource). Then, run the test we wrote earlier:

$> inspec exec specs/base_spec.rb -t ssh://centos@$
(terraform output mighty_trousers_public_ip)-i ~/.ssh/id_rsa

Here, ~/.ssh/id_rsa is used to connect to the server. Change it to point to the private key of a keypair you generated for Terraform.

The test will fail because we forgot to configure routing and add an Internet gateway for our VPC. Currently, none of the instances can talk to the Internet or be connected from the Internet. The Internet gateway is responsible for enabling Internet connectivity inside VPC. Let's add it to template.tf:

resource "aws_internet_gateway" "gw" { 
  vpc_id = "${aws_vpc.my_vpc.id}" 
} 

Then, modify a default route table to have routing to the outside world:

resource "aws_default_route_table" "default_routing" { 
  default_route_table_id = "${aws_vpc.my_vpc.default_route_table_id}" 
  route { 
    cidr_block = "0.0.0.0/0" 
    gateway_id = "${aws_internet_gateway.gw.id}" 
  } 
} 

Note

There is always a default route table in AWS. Terraform provides the aws_default_route_table resource to give an easy way to update it. Otherwise, you would have to create it from scratch.

Apply the template and try to run Inspec again! You will get a bunch of error messages again, this time looking similar to the following:

INFO -- : [SSH] connection failed, retrying in 1 seconds 
(#<Net::SSH::AuthenticationFailed: Authentication failed for user [email protected]>)

Remember that we created a key pair in the previous chapter? Well, we never actually used it. Let's pass the name of a key pair from a root template to the module and use it for our aws_instance resource:

module "mighty_trousers" { 
  source = "./modules/application" 
  vpc_id = "${aws_vpc.my_vpc.id}" 
  subnet_id = "${aws_subnet.public.id}" 
  name = "MightyTrousers" 
  keypair = "${aws_key_pair.terraform.key_name}" 
  environment = "${var.environment}" 
  extra_sgs = ["${aws_security_group.default.id}"] 
  extra_packages = "${lookup(var.extra_packages, "MightyTrousers")}" 
  external_nameserver = "${var.external_nameserver}" 
} 

Add a new variable to the application module:

variable "keypair" {} 

And use it inside the module:

resource "aws_instance" "app-server" { 
  ami = "${data.aws_ami.app-ami.id}" 
  instance_type = "${lookup(var.instance_type, var.environment)}" 
  subnet_id = "${var.subnet_id}" 
  vpc_security_group_ids = ["${concat(var.extra_sgs, aws_security_group.allow_http.*.id)}"] 
  user_data = "${data.template_file.user_data.rendered}" 
  key_name = "${var.keypair}" 
  tags { 
    Name = "${var.name}" 
  } 
} 

Changing a key pair requires instance recreation, but that's fine because we didn't have a production instance running - we don't have to bother with a safe replacement procedure yet. Just run terraform apply again and give it a minute to run.

Note

What we are doing now can be considered a Test Drive Development: we started with a failing test, and now we are slowly moving to make it green.

After it's done, rerun the tests and see them go green:

$> inspec exec specs/base_spec.rb -t ssh://centos@$
 (terraform output mighty_trousers_public_ip) -i ~/.ssh/id_rsa 
Target: ssh://[email protected]:22 
System Package
wget should be installed 
Test Summary: 1 successful, 0 failures, 0 skipped

This works well for testing a single server, but most likely, your infrastructure has more than one server. You have full flexibility of how to test them. The simplest option is to wrap both Terraform and Inspec into another shell script. We will take a look at some more sophisticated setups in a bit. What's important to memorize for now is that it is really easy to connect Terraform to external tools just using outputs. However, of course, it's only one of multiple ways of doing it. Another way is to use provisioners.

Note

Don't take it as a valid example of how to test a single server with Inspec. If you want to perform tests like this, Terraform is the wrong tool to spin up the server. Consider using TestKitchen (discussed in Chapter 7, Collaborative Infrastructure).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.14.219