Samples: end-to-end testing (#47)
Add a new sample for end-to-end testing in Terraform projects.
This commit is contained in:
parent
74170d8203
commit
24455a9336
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
## Ignore terraform provider and state files
|
||||
*.terraform
|
||||
*.tfstate
|
||||
*.tfstate*
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
@ -290,3 +290,7 @@ __pycache__/
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# Golang
|
||||
go.sum
|
||||
.test-data/
|
@ -5,3 +5,5 @@ This repository contains real-world examples that will walk you through differen
|
||||
## Testing Best Practices
|
||||
|
||||
- [Integration Testing](integration-testing/README.md)
|
||||
- [Compliance Testing](compliance-testing/README.md)
|
||||
- [End-to-end Testing](end-to-end-testing/README.md)
|
||||
|
119
samples/end-to-end-testing/README.md
Normal file
119
samples/end-to-end-testing/README.md
Normal file
@ -0,0 +1,119 @@
|
||||
# Terraform end-to-end testing
|
||||
|
||||
This is an example of how to use [Terratest](https://github.com/gruntwork-io/terratest) to perform end-to-end testing on a Terraform project.
|
||||
|
||||
## What about end-to-end testing
|
||||
|
||||
End-to-end tests validate that a program actually works in real conditions, the closest as possible to production environment. Let's imagine that we are writing a Terraform module to deploy two virtual machines into a virtual network but we don't want those machines to be able to ping each other. End-to-end tests are exactly what we need to make sure that the deployment of this module create the expected resources, but also that the virtual machines cannot ping each other.
|
||||
|
||||
In order to achieve that, the end-to-end test will apply the Terraform configuration, run all required tests, and finally tear down the infrastructure. They are much longer than integration or unit tests so they are not executed part of the continuous integration process.
|
||||
|
||||
## What about Terratest
|
||||
|
||||
[Terratest](https://github.com/gruntwork-io/terratest) is an open-source framework, written in [Go](http://golang.org/dl) and relying on the Go test framework, that helps to write end-to-end tests for Terraform projects. It provides helper and tools to:
|
||||
|
||||
1. Deploy a Terraform configuration
|
||||
2. Goes back to the Go tests to validate what has been actually deployed
|
||||
3. Tear down the deployed infrastructure
|
||||
|
||||
## Scenario of this sample
|
||||
|
||||
In this sample, we are going to use a Terraform configuration that deploys two Linux virtual machines into the same virtual network. `vm-linux-1` has a public IP address. Only port 22 is opened to allow SSH connection. `vm-linux-2` has no public IP address. The scenario we want to validate with the end-to-end test is to make sure that:
|
||||
|
||||
- infrastructure is deployed correctly
|
||||
- it's possible to open an SSH session to `vm-linux-1` using port 22
|
||||
- it's possible to ping `vm-linux-2` from `vm-linux-1` SSH session
|
||||
|
||||

|
||||
|
||||
> NOTE: this is a simple scenario to illustrate how to write a basic end-to-end test. We don't recommend having production virtual machines that exposes SSH port over a public IP address.
|
||||
|
||||
## Terraform configuration
|
||||
|
||||
The Terraform configuration for this scenario can be found in the [src/main.tf](src/main.tf) file. It contains everything to deploy the Azure infrastructure represented on the figure above.
|
||||
|
||||
If you are not familiar with creating a Linux virtual machine using Terraform we recommend that you read [this page of the documentation](https://docs.microsoft.com/azure/developer/terraform/create-linux-virtual-machine-with-infrastructure) before.
|
||||
|
||||
## End-to-end test
|
||||
|
||||
As mentioned in the introduction, the end-to-end test is written in Go language and uses the Terratest framework. It is defined in the [src/test/end2end_test.go](src/test/end2end_test.go) file.
|
||||
|
||||
This is the common structure of a Golang test using Terratest:
|
||||
|
||||
```Go
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gruntwork-io/terratest/modules/terraform"
|
||||
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
|
||||
)
|
||||
|
||||
func TestEndToEndDeploymentScenario(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fixtureFolder := "../"
|
||||
|
||||
// User Terratest to deploy the infrastructure
|
||||
test_structure.RunTestStage(t, "setup", func() {
|
||||
terraformOptions := &terraform.Options{
|
||||
// Indicate the directory that contains the Terraform configuration to deploy
|
||||
TerraformDir: fixtureFolder,
|
||||
}
|
||||
|
||||
// Save options for later test stages
|
||||
test_structure.SaveTerraformOptions(t, fixtureFolder, terraformOptions)
|
||||
|
||||
// Triggers the terraform init and terraform apply command
|
||||
terraform.InitAndApply(t, terraformOptions)
|
||||
})
|
||||
|
||||
test_structure.RunTestStage(t, "validate", func() {
|
||||
// run validation checks here
|
||||
terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder)
|
||||
publicIpAddress := terraform.Output(t, terraformOptions, "public_ip_address")
|
||||
})
|
||||
|
||||
// When the test is completed, teardown the infrastructure by calling terraform destroy
|
||||
test_structure.RunTestStage(t, "teardown", func() {
|
||||
terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder)
|
||||
terraform.Destroy(t, terraformOptions)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
As you can see in the snippet above, the test is composed by three stages:
|
||||
|
||||
1. `setup`: this stage is responsible for running Terraform to deploy the configuration.
|
||||
2. `validate`: this stage is responsible for doing the validation checks / assertions.
|
||||
3. `teardown`: this stage is responsible for cleaning up the infrastructure.
|
||||
|
||||
Some relevant functions provided by Terratest framework are:
|
||||
|
||||
- `terraform.InitAndApply` allows to run the `terraform init` and `terraform apply` commands from Go code.
|
||||
- `terraform.Output` allows to retrieve the value of a deployment output variable.
|
||||
- `terraform.Destroy` allows to run the `terraform destroy` command from Go code.
|
||||
- `test_structure.LoadTerraformOptions` allows to load Terraform options (config, variables etc.) from the state.
|
||||
- `test_structure.SaveTerraformOptions` allows to save Terraform options (config, variables etc.) to the state.
|
||||
|
||||
## Run the end-to-end test
|
||||
|
||||
Running the test requires that Terraform is installed and configured on your machine and that you are connected to your Azure subscription with the Azure CLI command `az login`.
|
||||
|
||||
Once ready, since the end-to-end test is just a Go test, it can be run like the following:
|
||||
|
||||
```console
|
||||
# Set the path of the SSH private key to use to connect the virtual machine
|
||||
export TEST_SSH_KEY_PATH="/home/bob/.ssh/id_rsa"
|
||||
cd test
|
||||
go test -v ./ -timeout 30m
|
||||
```
|
||||
|
||||
Once the test is ended, it displays the results:
|
||||
|
||||
```console
|
||||
--- PASS: TestEndToEndDeploymentScenario (390.99s)
|
||||
PASS
|
||||
ok test 391.052s
|
||||
```
|
BIN
samples/end-to-end-testing/assets/scenario.png
Normal file
BIN
samples/end-to-end-testing/assets/scenario.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
samples/end-to-end-testing/assets/scenario.pptx
Normal file
BIN
samples/end-to-end-testing/assets/scenario.pptx
Normal file
Binary file not shown.
140
samples/end-to-end-testing/src/main.tf
Normal file
140
samples/end-to-end-testing/src/main.tf
Normal file
@ -0,0 +1,140 @@
|
||||
resource "random_integer" "rand" {
|
||||
min = 1000
|
||||
max = 9999
|
||||
}
|
||||
|
||||
resource "azurerm_resource_group" "rg" {
|
||||
name = "rg-terratest-sample-${random_integer.rand.result}"
|
||||
location = var.location
|
||||
}
|
||||
|
||||
resource "azurerm_virtual_network" "vnet" {
|
||||
name = "vnet-terratest-sample"
|
||||
address_space = ["10.0.0.0/16"]
|
||||
location = azurerm_resource_group.rg.location
|
||||
resource_group_name = azurerm_resource_group.rg.name
|
||||
}
|
||||
|
||||
resource "azurerm_subnet" "subnet" {
|
||||
name = "default"
|
||||
resource_group_name = azurerm_resource_group.rg.name
|
||||
virtual_network_name = azurerm_virtual_network.vnet.name
|
||||
address_prefixes = ["10.0.2.0/24"]
|
||||
}
|
||||
|
||||
## Linux VM 1
|
||||
|
||||
resource "azurerm_public_ip" "pip" {
|
||||
name = "pip-vm-linux-1"
|
||||
location = azurerm_resource_group.rg.location
|
||||
resource_group_name = azurerm_resource_group.rg.name
|
||||
allocation_method = "Dynamic"
|
||||
idle_timeout_in_minutes = 30
|
||||
}
|
||||
|
||||
resource "azurerm_network_interface" "nic1" {
|
||||
name = "nic-vm-linux-1"
|
||||
location = azurerm_resource_group.rg.location
|
||||
resource_group_name = azurerm_resource_group.rg.name
|
||||
|
||||
ip_configuration {
|
||||
name = "internal"
|
||||
subnet_id = azurerm_subnet.subnet.id
|
||||
private_ip_address_allocation = "Dynamic"
|
||||
public_ip_address_id = azurerm_public_ip.pip.id
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_network_security_group" "nsg" {
|
||||
name = "nsg-terraform-sample"
|
||||
location = var.location
|
||||
resource_group_name = azurerm_resource_group.rg.name
|
||||
|
||||
security_rule {
|
||||
name = "SSH"
|
||||
priority = 1001
|
||||
direction = "Inbound"
|
||||
access = "Allow"
|
||||
protocol = "Tcp"
|
||||
source_port_range = "*"
|
||||
destination_port_range = "22"
|
||||
source_address_prefix = "*"
|
||||
destination_address_prefix = "*"
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_network_interface_security_group_association" "nic1-nsg" {
|
||||
network_interface_id = azurerm_network_interface.nic1.id
|
||||
network_security_group_id = azurerm_network_security_group.nsg.id
|
||||
}
|
||||
|
||||
resource "azurerm_linux_virtual_machine" "vm1" {
|
||||
name = "vm-linux-1"
|
||||
resource_group_name = azurerm_resource_group.rg.name
|
||||
location = azurerm_resource_group.rg.location
|
||||
size = "Standard_B2s"
|
||||
admin_username = "azureuser"
|
||||
network_interface_ids = [
|
||||
azurerm_network_interface.nic1.id,
|
||||
]
|
||||
|
||||
admin_ssh_key {
|
||||
username = "azureuser"
|
||||
public_key = file("~/.ssh/id_rsa.pub")
|
||||
}
|
||||
|
||||
os_disk {
|
||||
caching = "ReadWrite"
|
||||
storage_account_type = "Standard_LRS"
|
||||
}
|
||||
|
||||
source_image_reference {
|
||||
publisher = "Canonical"
|
||||
offer = "UbuntuServer"
|
||||
sku = "18.04-LTS"
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
|
||||
## Linux VM 2
|
||||
|
||||
resource "azurerm_network_interface" "nic2" {
|
||||
name = "nic-vm-linux-2"
|
||||
location = azurerm_resource_group.rg.location
|
||||
resource_group_name = azurerm_resource_group.rg.name
|
||||
|
||||
ip_configuration {
|
||||
name = "internal"
|
||||
subnet_id = azurerm_subnet.subnet.id
|
||||
private_ip_address_allocation = "Dynamic"
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_linux_virtual_machine" "vm2" {
|
||||
name = "vm-linux-2"
|
||||
resource_group_name = azurerm_resource_group.rg.name
|
||||
location = azurerm_resource_group.rg.location
|
||||
size = "Standard_B2s"
|
||||
admin_username = "azureuser"
|
||||
|
||||
network_interface_ids = [
|
||||
azurerm_network_interface.nic2.id,
|
||||
]
|
||||
|
||||
admin_ssh_key {
|
||||
username = "azureuser"
|
||||
public_key = file(var.ssh_public_key_file)
|
||||
}
|
||||
|
||||
os_disk {
|
||||
caching = "ReadWrite"
|
||||
storage_account_type = "Standard_LRS"
|
||||
}
|
||||
|
||||
source_image_reference {
|
||||
publisher = "Canonical"
|
||||
offer = "UbuntuServer"
|
||||
sku = "18.04-LTS"
|
||||
version = "latest"
|
||||
}
|
||||
}
|
11
samples/end-to-end-testing/src/output.tf
Normal file
11
samples/end-to-end-testing/src/output.tf
Normal file
@ -0,0 +1,11 @@
|
||||
output "resource_group_name" {
|
||||
value = azurerm_resource_group.rg.name
|
||||
}
|
||||
|
||||
output "vm_linux_1_public_ip_address" {
|
||||
value = azurerm_public_ip.pip.ip_address
|
||||
}
|
||||
|
||||
output "vm_linux_2_private_ip_address" {
|
||||
value = azurerm_network_interface.nic2.ip_configuration[0].private_ip_address
|
||||
}
|
3
samples/end-to-end-testing/src/provider.tf
Normal file
3
samples/end-to-end-testing/src/provider.tf
Normal file
@ -0,0 +1,3 @@
|
||||
provider "azurerm" {
|
||||
features {}
|
||||
}
|
101
samples/end-to-end-testing/src/test/end2end_test.go
Normal file
101
samples/end-to-end-testing/src/test/end2end_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gruntwork-io/terratest/modules/terraform"
|
||||
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestEndToEndDeploymentScenario(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fixtureFolder := "../"
|
||||
sshKeyPath := os.Getenv("TEST_SSH_KEY_PATH")
|
||||
|
||||
if sshKeyPath == "" {
|
||||
t.Fatalf("TEST_SSH_KEY_PATH environment variable cannot be empty.")
|
||||
}
|
||||
|
||||
// User Terratest to deploy the infrastructure
|
||||
test_structure.RunTestStage(t, "setup", func() {
|
||||
terraformOptions := &terraform.Options{
|
||||
// Indicate the directory that contains the Terraform configuration to deploy
|
||||
TerraformDir: fixtureFolder,
|
||||
}
|
||||
|
||||
// Save options for later test stages
|
||||
test_structure.SaveTerraformOptions(t, fixtureFolder, terraformOptions)
|
||||
|
||||
// Triggers the terraform init and terraform apply command
|
||||
terraform.InitAndApply(t, terraformOptions)
|
||||
})
|
||||
|
||||
test_structure.RunTestStage(t, "validate", func() {
|
||||
// run validation checks here
|
||||
terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder)
|
||||
|
||||
vmLinux1PublicIPAddress := terraform.Output(t, terraformOptions, "vm_linux_1_public_ip_address")
|
||||
vmLinux2PrivateIPAddress := terraform.Output(t, terraformOptions, "vm_linux_2_private_ip_address")
|
||||
|
||||
// it takes some time for Azure to assign the public IP address so it's not available in Terraform output after the first apply
|
||||
attemptsCount := 0
|
||||
for vmLinux1PublicIPAddress == "" && attemptsCount < 5 {
|
||||
// add wait time to let Azure assign the public IP address and apply the configuration again, to refresh state.
|
||||
time.Sleep(30 * time.Second)
|
||||
terraform.Apply(t, terraformOptions)
|
||||
vmLinux1PublicIPAddress = terraform.Output(t, terraformOptions, "vm_linux_1_public_ip_address")
|
||||
attemptsCount++
|
||||
}
|
||||
|
||||
if vmLinux1PublicIPAddress == "" {
|
||||
t.Fatal("Cannot retrieve the public IP address value for the linux vm 1.")
|
||||
}
|
||||
|
||||
key, err := ioutil.ReadFile(sshKeyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read private key: %v", err)
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(key)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to parse private key: %v", err)
|
||||
}
|
||||
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: "azureuser",
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(signer),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
sshConnection, err := ssh.Dial("tcp", fmt.Sprintf("%s:22", vmLinux1PublicIPAddress), sshConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot establish SSH connection to vm-linux-1 public IP address: %v", err)
|
||||
}
|
||||
|
||||
defer sshConnection.Close()
|
||||
sshSession, err := sshConnection.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot create SSH session to vm-linux-1 public IP address: %v", err)
|
||||
}
|
||||
|
||||
defer sshSession.Close()
|
||||
err = sshSession.Run(fmt.Sprintf("ping -c 1 %s", vmLinux2PrivateIPAddress))
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot ping vm-linux-2 from vm-linux-2: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// When the test is completed, teardown the infrastructure by calling terraform destroy
|
||||
test_structure.RunTestStage(t, "teardown", func() {
|
||||
terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder)
|
||||
terraform.Destroy(t, terraformOptions)
|
||||
})
|
||||
}
|
8
samples/end-to-end-testing/src/test/go.mod
Normal file
8
samples/end-to-end-testing/src/test/go.mod
Normal file
@ -0,0 +1,8 @@
|
||||
module test
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/gruntwork-io/terratest v0.28.5
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975
|
||||
)
|
11
samples/end-to-end-testing/src/variables.tf
Normal file
11
samples/end-to-end-testing/src/variables.tf
Normal file
@ -0,0 +1,11 @@
|
||||
variable location {
|
||||
type = string
|
||||
description = "The Azure location where the resources will be created."
|
||||
default = "westeurope"
|
||||
}
|
||||
|
||||
variable ssh_public_key_file {
|
||||
type = string
|
||||
description = "The file path of the public SSH key to use for the virtual machine."
|
||||
default = "~/.ssh/id_rsa.pub"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user