Make infrastructure as code testable and callable

Infrastructure as code is not programming, it’s only configuration.

— as a software developer

With the big cloud migration wave, there are a lot of cloud provision and configuration effort. There is a modern name called “ Infrastructure as code “. But after AWS released in the market over 1 decade, it is still in earlier stage from a software engineer point view.

The biggest market winner is https://www.terraform.io/, which it can deliver infrastructure easily. But it was same year in 2014, AWS released boto3 API, https://pypi.org/project/boto3/0.0.1/ which can also provision all AWS service in python code easily, link here

import boto3
client = boto3.resource('s3')
response = client.create_bucket(
    Bucket='examplebucket',
    CreateBucketConfiguration={
        'LocationConstraint': 'eu-west-1',
    },
)

print(response)

How is the definition of simplicity from Terraform point view? I have different options. Is boto3 complicated?

The main disadvantage for Terraform is the DSL language can’t test easily with Unit Test, Mock Service, Integration Test, E2E test like other modern language Python, Java. Many Infra Engineers or Architects are always challenging me, WHY need test? —– It’s like a joke.

However, there are some innovative solution is coming,

  1. AWS CDK https://aws.amazon.com/cdk/
  2. Plumi https://www.pulumi.com/
  3. Terraform https://www.terraform.io/cdktf

Which is providing modern programming productivity to make infrastructure as real code with quality.

import pulumi
from pulumi_aws import s3

# Create an AWS resource (S3 Bucket)
bucket = s3.Bucket('my-bucket')

# Export the name of the bucket
pulumi.export('bucket_name',  bucket.id)

As today, there are maybe many existing legacies Terraform code or module in your organization which you can’t drop directly. I made a hand-on dirty to find a testable solution to maintain TF code. And today SonarQube support TF code check for AWS.

1. Overview of Test Cost

Cost to Run Test

2. Unit Test in Terraform

It is not really unit test, it is more grammar check and plan explanation.

terraform fmt -check
tflint
terraform validate
terraform plan

3. Integration Test Terraform

In terraform, you can apply to deploy your code at Integration Test Directly.

terraform apply
terraform destory [don't forget release code]

But you can also use advanced IT test framework such as Terratest4 or kitchen-terraform5. It’s a provision PostgreSQL test example.


terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
		TerraformDir: tfFilePath,
		Vars: map[string]interface{}{
			"postfix":     uniquePostfix,
			"db_user":     expectedUser,
			"db_password": expectedPassword,
		},
		NoColor: false,
	})
	subscriptionID := "xxx"

	defer terraform.Destroy(t, terraformOptions)

	terraform.InitAndApply(t, terraformOptions)
	expectedServername := "postgresqlserver-" + uniquePostfix // see fixture
	actualServername := terraform.Output(t, terraformOptions, "servername")
	rgName := terraform.Output(t, terraformOptions, "rgname")
	expectedSkuName := terraform.Output(t, terraformOptions, "sku_name")
	actualServer := azure.GetPostgreSQLServer(t, rgName, actualServername, subscriptionID)
	actualServerAddress := *actualServer.ServerProperties.FullyQualifiedDomainName
	actualServerUser := *actualServer.ServerProperties.AdministratorLogin

	// Expectation
	assert.NotNil(t, actualServer)
	assert.Equal(t, expectedUser, actualServerUser)
	assert.Equal(t, expectedServername, actualServername)
	assert.Equal(t, expectedSkuName, *actualServer.Sku.Name)

In go libraries, you can use SQL to make end2end test like this,

func ConnectDB(t *testing.T, userName string, expectedPassword string, databaseAddress string, actualServername string) {
	var connectionString string = fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=require", databaseAddress, userName+"@"+actualServername, expectedPassword, "postgres")
	print(connectionString)
	db, err := sql.Open("postgres", connectionString)
	assert.Nil(t, err, "open db failed")
	err = db.Ping()
	assert.Nil(t, err, "connect db failed")
	fmt.Println("Successfully created connection to database")
	var currentTime string
	err = db.QueryRow("select now()").Scan(&currentTime)
	assert.Nil(t, err, "query failed ")
	assert.NotEmpty(t, currentTime, "Get Query Time "+currentTime)

github code is here https://github.com/wuqunfei/tfmodule-azure-resource-postgresql/blob/main/test/mod_test.go#L56

4. Policy Test in Terraform

IT Compliance and Security become into code in these years when you provision your infrastructure. Azure has an example blog https://learn.microsoft.com/en-us/azure/developer/terraform/best-practices-compliance-testing

terraform show -json main.tfplan > main.tfplan.json

docker run --rm -v $PWD:/target -it eerkunt/terraform-compliance -f features -p main.tfplan.json

# https://github.com/terraform-compliance/cli  a lightweight, security focused, BDD test framework against terraform.

5. Terraform Code Quality Check

Since last year, SonarQube supports Terraform Grammar and Security Check. It helps us reduce a lot manually setup and check.

For example, this code has Omitting “public_network_access_enabled” allows network access from the Internet. Make sure it is safe here.

# public_network_access_enabled = true
# default is true, need to set false
public_network_access_enabled = false

Which developer can’t find it easily because public network access as default.

6. Making Terraform code callable

1. Shell spaghetti easily: Using some shell and scripts or github ci/cd or jenkins, the provision can work automation quickly and easily.

2. Integrating with automation tools : we can also use Ansible TF-module and define a play book to run automatically. Ansible TF model link Ansible Towner can explore their API easily to automation tools.

3. Crossplane Open API definition

It is a new design architecture from Crossplane which had considered each component can be callable by Open API definition.

  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              parameters:
                type: object
                properties:
                  storageGB:
                    type: integer
                required:
                  - storageGB
            required:
              - parameters

Original code is here https://github.com/crossplane/crossplane/blob/master/docs/getting-started/create-configuration.md

Summary

To write infrastructure code goes straightforward, but the foundation of high-quality software mindset which is still missing in many cloud engineering daily job. In this blog, I summarized the existing marketing options, it may help someone like to improve their automation and quality in their infrastructure daily work.

Posted in

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.