├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples └── simple-example │ ├── README.md │ ├── terraform │ ├── .terraform.lock.hcl │ ├── ec2-asg.tf │ ├── init.tf │ ├── main.tf │ ├── outputs.tf │ └── rds.tf │ └── test │ └── infra_test.go ├── go.mod ├── go.sum └── tester ├── interfaces.go ├── retry.go ├── shell_test_case.go ├── shell_test_case_test.go ├── ssm_client.go ├── target.go ├── target_test.go ├── tcp_connection_tester.go ├── tester.go ├── tester_test.go └── util.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. See: https://circleci.com/docs/2.0/configuration-reference 2 | version: 2.1 3 | jobs: 4 | build: 5 | working_directory: ~/repo 6 | docker: 7 | - image: circleci/golang:1.15.8 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | keys: 12 | - go-mod-v4-{{ checksum "go.sum" }} 13 | - run: 14 | name: Install Dependencies 15 | command: go mod download 16 | - save_cache: 17 | key: go-mod-v4-{{ checksum "go.sum" }} 18 | paths: 19 | - "/go/pkg/mod" 20 | - run: 21 | name: Run tests 22 | command: | 23 | mkdir -p /tmp/test-reports 24 | gotestsum --junitfile /tmp/test-reports/unit-tests.xml -- ./tester 25 | - store_test_results: 26 | path: /tmp/test-reports 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ 2 | .idea/ 3 | # envrc for loading AWS Credentials 4 | .envrc 5 | # local tooling 6 | .tool-versions 7 | 8 | ################## 9 | # Go 10 | ################## 11 | 12 | # Binaries for programs and plugins 13 | *.exe 14 | *.exe~ 15 | *.dll 16 | *.so 17 | *.dylib 18 | 19 | # Test binary, built with `go test -c` 20 | *.test 21 | 22 | # Output of the go coverage tool, specifically when used with LiteIDE 23 | *.out 24 | 25 | # Dependency directories (remove the comment below to include it) 26 | # vendor/ 27 | 28 | 29 | ################## 30 | # Terraform 31 | ################## 32 | 33 | # Local .terraform directories 34 | **/.terraform/* 35 | terraform-cache 36 | 37 | # .tfstate files 38 | *.tfstate 39 | *.tfstate.* 40 | 41 | # Terraform plans 42 | *.plan 43 | *.tfplan 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ankit Wal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssm-tester 2 | [![CircleCI](https://circleci.com/gh/ankitwal/ssm-tester/tree/main.svg?style=svg)](https://circleci.com/gh/ankitwal/ssm-tester/tree/main) 3 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/ankitwal/ssm-tester/main/LICENSE) 4 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/ankitwal/ssm-tester/tester?tab=overview) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/ankitwal/ssm-tester)](https://goreportcard.com/report/github.com/ankitwal/ssm-tester) 6 | 7 | 8 | Infrastructure testing helper for AWS Resources that uses AWS SSM to remotely execute commands on EC2 machines, to enable infrastructure engineering teams to write 9 | tests that validate *behaviour*. 10 | 11 | ![automate infrastructure behaviour testing](https://i.ibb.co/cgbthDN/terminal-screenshot.png) 12 | 13 | ## Contents 14 | 15 | - [Why](#why) 16 | - [Quick Start](#quick-start) 17 | - [More Examples](#more-examples) 18 | - [GoDoc](https://pkg.go.dev/github.com/ankitwal/ssm-tester/tester) 19 | - [Working Example](./examples/simple-example/README.md) 20 | 21 | ## Why 22 | 23 | ### Validating infrastructure 24 | 25 | * Manually running commands to validate infrastructure correctness is slow and unreliable. 26 | * At times access like ssh may not even be possible to all instances in a cloud environment, making validation even harder. 27 | * This means some behaviour may only get tested when we run an application on the provisioned infrastructure, eg. connectivity to database, connectivity to required internet endpoint etc. 28 | This slows the feedback loop and in turn means lower quality. The application delivery team is a consumer/customer of the infrastructure delivery team. 29 | Hence an infrastructure delivery team should not have to rely on its customers to validate its code. 30 | * Additionally some behaviour is hard to validate, and won't get immediate feedback even with an application running functionally correctly on the provisioned infra. Example: 31 | * Broken connectivity to logging endpoints/service may only be detected if a team member notices missing logs, often these are not even being looked at in lower environments. Or worse it may only be detected when instance in production start falling over since their disks have gone to full from failing to flush logs to a remote logging service. 32 | 33 | `ssm-tester` allows infrastructure delivery teams to write tests that can execute custom commands on ec2 instances and hence validate for otherwise hard to test behaviour. 34 | 35 | ### Testing Behaviour over Configuration 36 | When teams write infrastructure as code - they should not only test against the correct configuration, but also test the infrastructure for *behaviour*! 37 | Specially when writing infrastructure code in a declarative tooling like terraform, tests that validate configuration may have limited value. 38 | For example, validating for **configuration**: 39 | * Does this security group have outgoing allowed to the RDS Security group 40 | * Does this application subnet network ACL have rules allowing outgoing to the RDS Subnet 41 | * Does this application subnet network ACL have rules allowing ephemeral ports open for return traffic from RDS subnets 42 | * Does this application subnet have a route table attached with routes to the database subnet 43 | 44 | These tests may essentially be a repeat of the configuration specified in our Infrastructure declarative code 45 | and do not validate the behaviour we want to guarantee in our infrastructure. 46 | Instead it would be better if we could write tests to validate **behaviour**: 47 | * Does the provisioned infrastructure allow my application EC2 instances to connect via TCP to my RDS endpoint 48 | * This would ideally validate that the configuration for security groups, subnets, NACLs, route tables cumulatively allows this behaviour. 49 | * Can the provisioned instance pull a required secret from secrets manager 50 | * This would validate that required networking configuration + IAM Instance Profile + Role configuration cumulatively allows for this behaviour. 51 | 52 | `ssm-tester` enables users to write automated tests that validate behaviour, so infrastructure engineering teams do not have to wait for application teams to report 53 | broken infrastructure, or worse, wait for incidents in production. 54 | 55 | ## Quick Start 56 | 57 | ### Requirements 58 | 59 | ssm-tester requires the the EC2 instances to be integrated with [AWS Systems Manager](https://aws.amazon.com/systems-manager/). 60 | This requires your EC2 instances to all ssm-agent installed( installed by default on Amazon Linux 2), and certain AWS SSM related resources provisioned. 61 | You may see this [example](examples/simple-example/terraform/main.tf) of a minimum AWS Systems Manager integrated infra, and 62 | [this AWS Documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-setting-up.html) for a more comprehensive guide. 63 | If you already use AWS Systems Manager in your AWS Infrastructure then you should be able to use this out of box. Alternatively you may 64 | consider layering on AWS SSM required resources in the test environments. 65 | 66 | ### Using ssm-tester/tester 67 | 68 | 1. Import ssm-tester/tester 69 | ```go 70 | import "github.com/ankitwal/ssm-tester/tester" 71 | ``` 72 | 2. Initialise the ssm service client - this is used to the AWS SSM API 73 | ```go 74 | // Initialise AWS SSM client service. 75 | ssmClient := tester.NewSSMClientWithDefaultConfig(t) 76 | ``` 77 | 3. Initialise retry config - this is used to manage to polling for the test result 78 | ```go 79 | // create retry configuration for 80 | retryConfig := tester.NewRetryDefaultConfig() 81 | ``` 82 | 4. Write some tests 83 | ```go 84 | t.Run("TestAppInstanceCanConnectToImportantEndpoint", func(t *testing.T) { 85 | // 4.1 create a new test with a custom test command 86 | // this example uses curl, it relies on your target instances having curl installed 87 | testCase := tester.NewShellTestCase("curl https://www.importantendpoint.com --max-time=2", true) 88 | 89 | // 4.2 specify the ec2 instance to target for the test 90 | target := tester.NewTagNameTarget(terraform.Output(t, terraformOptions, "app_instance_name_tag")) 91 | 92 | // 4.3 run the test 93 | tester.RunTestCaseForTarget(t, ssmClient, testCase, target, retryConfig) 94 | }) 95 | ``` 96 | ### More examples 97 | 98 | Write some tests with built in [TcpConnectionTestWithTagName](https://pkg.go.dev/github.com/ankitwal/ssm-tester/tester#TcpConnectionTestWithNameTag) helper 99 | 100 | ```go 101 | t.Run("TestAppInstanceConnectivityToDatabase", func(t *testing.T) { 102 | dbEndpoint := "mydb.privatedns" 103 | dbPort := "3306" 104 | tag := "app_instance_name_tag" 105 | 106 | // run the test 107 | tester.TcpConnectionTestWithTagName(t, ssmClient, tag, dbEndpoint, dbPort, retryConfig) 108 | }) 109 | ``` 110 | 111 | Write some tests with using [terratest](https://terratest.gruntwork.io), please see [examples](examples/simple-example/) for working examples 112 | 113 | ```go 114 | // this example uses terratest to initialise the terraform stack and get output value 115 | terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ 116 | TerraformDir: "../terraform", 117 | }) 118 | 119 | // init and apply terraform stack ensuring clean up 120 | t.Cleanup(func() { terraform.Destroy(t, terraformOptions) }) 121 | terraform.InitAndApply(t, terraformOptions) 122 | 123 | t.Run("TestAppInstanceConnectivityToDatabase", func(t *testing.T) { 124 | // get the required resource values using terratest's terraform module 125 | dbEndpoint := terraform.Output(t, terraformOptions, "database_endpoint") 126 | dbPort := terraform.Output(t, terraformOptions, "database_port") 127 | tag := terraform.Output(t, terraformOptions, "instance_name_tag") 128 | 129 | // run the test 130 | tester.TcpConnectionTestWithTagName(t, ssmClient, tag, dbEndpoint, dbPort, retryConfig) 131 | }) 132 | ``` 133 | 134 | Write negative tests 135 | ```go 136 | t.Run("TestAppInstanceShouldNOTHaveConnectivityToPublicInternet", func(t *testing.T) { 137 | // build a tcp connectivity test case with public endpoint and port, 138 | // with condition false, i.e the tests passes if the command fails on all target instances 139 | testCase := tester.NewShellTestCase(fmt.Sprintf("timeout 2 bash -c ' **WARNING**: This example tests creates actual AWS resources and may incur costs. The tests should clean up 11 | resources it creates after itself. In the case of an unexpected failure to clean up resources please ensure 12 | that you remove any unwanted AWS resources to avoid incurring additional costs. 13 | 14 | ## Contents 15 | ``` 16 | examples/private-subnets 17 | ├── README.md # this file 18 | ├── terraform # sample terraform project 19 | │   ├── ec2-asg.tf 20 | │   ├── init.tf 21 | │   ├── main.tf 22 | │   ├── outputs.tf 23 | │   ├── rds.tf 24 | │   ├── terraform.tfstate 25 | │   └── terraform.tfstate.backup 26 | └── test # directory for infrastructure tests 27 | └── infra_test.go # go test file with infra tests for the above terraform provisioned infra 28 | ``` 29 | 30 | This is a simple example of infrastructure 31 | * An autoscaling group deployed in a private subnet 32 | * A RDS database deployed in an internal subnet 33 | * A monitoring endpoint for the application instances in the autoscaling group to forward metrics 34 | * A logging endpoint for the application instances in the autoscaling group to forward logs 35 | 36 | The tests in [infra_test.go](./test/infra_test.go) defines automated infrastructure tests using ssm-tester to validate 37 | some *behaviour* of the above provisioned infra. 38 | * do the application have tcp connectivity to the database 39 | * do the application instances have tcp connectivity to the looging, monitoring endpoint 40 | * ensure that application instances do not have connectivity to a public internet endpoint 41 | 42 | -------------------------------------------------------------------------------- /examples/simple-example/terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "3.50.0" 6 | constraints = ">= 3.15.0" 7 | hashes = [ 8 | "h1:gX/faGzGvZvf9W6597S7ZcMiNI1gw9ilQwM9hkc6F2Q=", 9 | "zh:11d5508e180b93ab06935dbebefb08745c78ebfe1ec41d53b4340fb7f27d32eb", 10 | "zh:192ae31ddf1c5c4ed7f64a15c8cf3f1a440b1427fa15604e90eee40037973f0a", 11 | "zh:2f381b6aec131b468409416ffa1a23a15e70f2e64a554bf47da13d499a30e7e2", 12 | "zh:5d7457d0ef1dfe65fbfa89bdddd4132cab728384c99950e2ac2f0c27d0c3ed34", 13 | "zh:6d0067f4bc0d0f16ab9c1143cc6817876e5258504e4893070504cd1559758227", 14 | "zh:8398b8b872173ce9c9144e81c50ed1a3d5904afac50d6431b970a82534a1f1f4", 15 | "zh:adbe50b8504cc0929cd54a945a0509ff2a72ebf790137b492c647314a2d2512a", 16 | "zh:b518a91608fcba26fbbc73121373129b4ac7a4544d983ac922a8dc249702f86d", 17 | "zh:d208c00f9e52b8accdc1b1f8466789e12491cf3129e849e2ba28427a1f86ba59", 18 | "zh:db61d71857e44aa2d3bb3f6ad568c531cb6e2a6b830d8672b46021c5bb194ef1", 19 | "zh:eac7aad200f4df951dd30ec8547b9412909c8dc450c1322b28619499e5e2bdbe", 20 | ] 21 | } 22 | 23 | provider "registry.terraform.io/hashicorp/null" { 24 | version = "3.1.0" 25 | constraints = ">= 2.0.0" 26 | hashes = [ 27 | "h1:xhbHC6in3nQryvTQBWKxebi3inG5OCgHgc4fRxL0ymc=", 28 | "zh:02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2", 29 | "zh:53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515", 30 | "zh:5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521", 31 | "zh:9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2", 32 | "zh:a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e", 33 | "zh:a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53", 34 | "zh:c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d", 35 | "zh:cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8", 36 | "zh:e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70", 37 | "zh:fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b", 38 | "zh:fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e", 39 | ] 40 | } 41 | 42 | provider "registry.terraform.io/hashicorp/random" { 43 | version = "3.1.0" 44 | constraints = ">= 2.2.0, >= 3.1.0" 45 | hashes = [ 46 | "h1:rKYu5ZUbXwrLG1w81k7H3nce/Ys6yAxXhWcbtk36HjY=", 47 | "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", 48 | "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", 49 | "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", 50 | "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", 51 | "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", 52 | "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", 53 | "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", 54 | "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", 55 | "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", 56 | "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", 57 | "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /examples/simple-example/terraform/ec2-asg.tf: -------------------------------------------------------------------------------- 1 | data "aws_ami" "amazon_latest_al2_ami_id" { 2 | most_recent = true 3 | owners = [ 4 | "amazon"] 5 | 6 | filter { 7 | name = "name" 8 | values = [ 9 | "amzn*"] 10 | } 11 | } 12 | 13 | locals { 14 | capacity = length(module.aws_vpc.intra_subnets) 15 | } 16 | 17 | module "asg" { 18 | source = "terraform-aws-modules/autoscaling/aws" 19 | version = "~> 4.0" 20 | 21 | # Autoscaling group 22 | name = local.project 23 | 24 | min_size = local.capacity 25 | max_size = local.capacity 26 | desired_capacity = local.capacity 27 | health_check_type = "EC2" 28 | vpc_zone_identifier = module.aws_vpc.intra_subnets 29 | 30 | # Ensure ASG Instances are refreshed on change in launch template 31 | instance_refresh = { 32 | strategy = "Rolling" 33 | } 34 | 35 | # Launch template 36 | lt_name = local.project 37 | update_default_version = true 38 | 39 | use_lt = true 40 | create_lt = true 41 | 42 | image_id = data.aws_ami.amazon_latest_al2_ami_id.id 43 | instance_type = "t3.micro" 44 | ebs_optimized = true 45 | 46 | # Maps to vpc_security_group_ids 47 | security_groups = [] 48 | tags = [ 49 | { 50 | key = "Environment" 51 | value = "test" 52 | propagate_at_launch = true 53 | }, 54 | { 55 | key = "Project" 56 | value = local.project 57 | propagate_at_launch = true 58 | }, 59 | ] 60 | iam_instance_profile_arn = aws_iam_instance_profile.ec2_instance.arn 61 | } 62 | 63 | # IAM Policy Required for SSM 64 | data "aws_iam_policy" "ssm" { 65 | arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" 66 | } 67 | data "aws_iam_policy_document" "ssm_s3" { 68 | statement { 69 | effect = "Allow" 70 | actions = [ 71 | "s3:GetObject" 72 | ] 73 | resources = [ 74 | "arn:aws:s3:::aws-ssm-${data.aws_region.aws_current_region.name}/*", 75 | "arn:aws:s3:::aws-windows-downloads-${data.aws_region.aws_current_region.name}/*", 76 | "arn:aws:s3:::amazon-ssm-${data.aws_region.aws_current_region.name}/*", 77 | "arn:aws:s3:::amazon-ssm-packages-${data.aws_region.aws_current_region.name}/*", 78 | "arn:aws:s3:::${data.aws_region.aws_current_region.name}-birdwatcher-prod/*", 79 | "arn:aws:s3:::aws-ssm-distributor-file-${data.aws_region.aws_current_region.name}/*", 80 | "arn:aws:s3:::aws-ssm-document-attachments-${data.aws_region.aws_current_region.name}/*", 81 | "arn:aws:s3:::patch-baseline-snapshot-${data.aws_region.aws_current_region.name}/*" 82 | ] 83 | } 84 | } 85 | 86 | resource "aws_iam_policy" "ssm_s3" { 87 | name = "${local.project}-ssm-s3" 88 | policy = data.aws_iam_policy_document.ssm_s3.json 89 | } 90 | 91 | data "aws_iam_policy_document" "instance_assume_role_policy" { 92 | statement { 93 | actions = ["sts:AssumeRole"] 94 | 95 | principals { 96 | type = "Service" 97 | identifiers = ["ec2.amazonaws.com"] 98 | } 99 | } 100 | } 101 | resource "aws_iam_role" "ec2_instance" { 102 | name = "${local.project}-instance-role" 103 | assume_role_policy = data.aws_iam_policy_document.instance_assume_role_policy.json 104 | } 105 | resource "aws_iam_role_policy_attachment" "ec2_instance_ssm" { 106 | policy_arn = data.aws_iam_policy.ssm.arn 107 | role = aws_iam_role.ec2_instance.name 108 | } 109 | resource "aws_iam_role_policy_attachment" "ec2_instance_ssm_s3" { 110 | policy_arn = aws_iam_policy.ssm_s3.arn 111 | role = aws_iam_role.ec2_instance.name 112 | } 113 | resource "aws_iam_instance_profile" "ec2_instance" { 114 | role = aws_iam_role.ec2_instance.id 115 | name = "${local.project}-instance-profile" 116 | } 117 | -------------------------------------------------------------------------------- /examples/simple-example/terraform/init.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.15.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = "~> 3.50" 7 | } 8 | } 9 | } 10 | 11 | provider "aws" { 12 | region = "ap-southeast-1" 13 | default_tags { 14 | tags = { 15 | Environment = "private-subnets-example-test" 16 | Project = "ssm-tester" 17 | Terraform = "true" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/simple-example/terraform/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | project = "private-subnets-example-test" 3 | region = data.aws_region.aws_current_region.name 4 | } 5 | data "aws_region" "aws_current_region" { 6 | } 7 | 8 | module "aws_vpc" { 9 | source = "terraform-aws-modules/vpc/aws" 10 | version = "3.2.0" 11 | name = "simple-example" 12 | cidr = "10.0.0.0/16" 13 | 14 | azs = ["${local.region}a", "${local.region}b", "${local.region}c"] 15 | 16 | # Db Subnets to host RDS 17 | database_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] 18 | database_subnet_tags = { 19 | Name = "${local.project}-db-subnet" 20 | } 21 | database_dedicated_network_acl = true 22 | 23 | # Private Subnets to host EC2 Instances 24 | intra_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 25 | intra_dedicated_network_acl = true 26 | intra_subnet_tags = { 27 | Name = "${local.project}-intra-subnet" 28 | } 29 | 30 | vpc_tags = { 31 | Name = local.project 32 | } 33 | enable_dns_hostnames = true 34 | enable_dns_support = true 35 | } 36 | 37 | module "endpoints" { 38 | source = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints" 39 | version = "3.2.0" 40 | 41 | vpc_id = module.aws_vpc.vpc_id 42 | 43 | # Endpoints requirements for SSM https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-create-vpc.html 44 | endpoints = { 45 | ssm = { 46 | service = "ssm" 47 | private_dns_enabled = true 48 | security_group_ids = [module.aws_vpc.default_security_group_id] 49 | subnet_ids = module.aws_vpc.intra_subnets 50 | tags = { Name = "ssm-vpc-endpoint" } 51 | }, 52 | ssmmessages = { 53 | service = "ssmmessages" 54 | private_dns_enabled = true 55 | security_group_ids = [module.aws_vpc.default_security_group_id] 56 | subnet_ids = module.aws_vpc.intra_subnets 57 | tags = { Name = "ssmmessages-vpc-endpoint" } 58 | }, 59 | ec2messages = { 60 | service = "ec2messages" 61 | private_dns_enabled = true 62 | security_group_ids = [module.aws_vpc.default_security_group_id] 63 | subnet_ids = module.aws_vpc.intra_subnets 64 | tags = { Name = "ec2messages-vpc-endpoint" } 65 | }, 66 | kms = { 67 | service = "kms" 68 | private_dns_enabled = true 69 | security_group_ids = [module.aws_vpc.default_security_group_id] 70 | subnet_ids = module.aws_vpc.intra_subnets 71 | tags = { Name = "kms-vpc-endpoint" } 72 | }, 73 | ec2 = { 74 | service = "ec2" 75 | private_dns_enabled = true 76 | security_group_ids = [module.aws_vpc.default_security_group_id] 77 | subnet_ids = module.aws_vpc.intra_subnets 78 | tags = { Name = "ec2-vpc-endpoint" } 79 | }, 80 | s3 = { 81 | service = "s3" 82 | service_type = "Gateway" 83 | route_table_ids = [module.aws_vpc.default_route_table_id] 84 | tags = { Name = "s3-vpc-endpoint" } 85 | }, 86 | 87 | ## endpoints for application 88 | # Logging 89 | cloudwatch_logs = { 90 | service = "logs" 91 | private_dns_enabled = true 92 | security_group_ids = [module.aws_vpc.default_security_group_id] 93 | subnet_ids = module.aws_vpc.intra_subnets 94 | tags = { Name = "cloudwatchlog-vpc-endpoint" } 95 | }, 96 | # Monitoring 97 | cloudwatch_monitoring = { 98 | service = "monitoring" 99 | private_dns_enabled = true 100 | security_group_ids = [module.aws_vpc.default_security_group_id] 101 | subnet_ids = module.aws_vpc.intra_subnets 102 | tags = { Name = "cloudwatchlog-vpc-endpoint" } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /examples/simple-example/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "vpc_id" { 2 | value = module.aws_vpc.vpc_id 3 | } 4 | output "database_subnet_group_name" { 5 | value = module.aws_vpc.database_subnet_group_name 6 | } 7 | output "database_subnet_group" { 8 | value = module.aws_vpc.database_subnet_group 9 | } 10 | output "instance_name_tag" { 11 | value = local.project 12 | } 13 | output "database_port" { 14 | value = module.db.db_instance_port 15 | } 16 | output "database_endpoint" { 17 | value = module.db.db_instance_address 18 | } 19 | output "monitoring_endpoint" { 20 | value = format("monitoring.%s.amazonaws.com", local.region) 21 | } 22 | output "logging_endpoint" { 23 | value = format("logs.%s.amazonaws.com", local.region) 24 | } 25 | -------------------------------------------------------------------------------- /examples/simple-example/terraform/rds.tf: -------------------------------------------------------------------------------- 1 | module "rds_security_group" { 2 | source = "terraform-aws-modules/security-group/aws" 3 | version = "~> 4" 4 | 5 | name = "${local.project}-db-sg" 6 | description = local.project 7 | vpc_id = module.aws_vpc.vpc_id 8 | 9 | # ingress 10 | ingress_with_cidr_blocks = [ 11 | { 12 | from_port = 3306 13 | to_port = 3306 14 | protocol = "tcp" 15 | description = "MySQL access from within VPC" 16 | cidr_blocks = module.aws_vpc.vpc_cidr_block 17 | }, 18 | ] 19 | } 20 | 21 | module "db" { 22 | source = "terraform-aws-modules/rds/aws" 23 | version = "~> 3.0" 24 | 25 | identifier = local.project 26 | 27 | create_db_option_group = false 28 | create_db_parameter_group = false 29 | 30 | # All available versions: http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_MySQL.html#MySQL.Concepts.VersionMgmt 31 | engine = "mysql" 32 | engine_version = "8.0.25" 33 | family = "mysql8.0" # DB parameter group 34 | major_engine_version = "8.0" # DB option group 35 | instance_class = "db.t2.micro" 36 | 37 | allocated_storage = 20 38 | 39 | name = "completeMysql" 40 | username = "complete_mysql" 41 | create_random_password = true 42 | random_password_length = 12 43 | port = 3306 44 | 45 | subnet_ids = module.aws_vpc.database_subnets 46 | vpc_security_group_ids = [module.rds_security_group.security_group_id] 47 | 48 | maintenance_window = "Mon:00:00-Mon:03:00" 49 | backup_window = "03:00-06:00" 50 | 51 | backup_retention_period = 0 52 | skip_final_snapshot = true 53 | } 54 | -------------------------------------------------------------------------------- /examples/simple-example/test/infra_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ankitwal/ssm-tester/tester" 6 | "github.com/gruntwork-io/terratest/modules/terraform" 7 | "testing" 8 | ) 9 | 10 | func TestInfra(t *testing.T) { 11 | 12 | // this example uses terratest to initialise the terraform stack and get output value 13 | // please see here for some 'how to use terratest' basics: https://terratest.gruntwork.io/examples/ 14 | terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ 15 | TerraformDir: "../terraform", 16 | }) 17 | 18 | // init and apply terraform stack ensuring clean up 19 | t.Cleanup(func() { terraform.Destroy(t, terraformOptions) }) 20 | terraform.InitAndApply(t, terraformOptions) 21 | 22 | // Initialise AWS SSM client service. 23 | ssmClient := tester.NewSSMClientWithDefaultConfig(t) 24 | 25 | // create retry configuration for 26 | // the tester the number of times the tester should retry polling for the result of the test command 27 | retryConfig := tester.NewRetryDefaultConfig() 28 | 29 | t.Run("TestAppInstanceConnectivityToDatabase", func(t *testing.T) { 30 | // get the required resource values using terratest's terraform module 31 | dbEndpoint := terraform.Output(t, terraformOptions, "database_endpoint") 32 | dbPort := terraform.Output(t, terraformOptions, "database_port") 33 | tag := terraform.Output(t, terraformOptions, "instance_name_tag") 34 | tester.TcpConnectionTestWithTagName(t, ssmClient, tag, dbEndpoint, dbPort, retryConfig) 35 | }) 36 | t.Run("TestAppInstanceConnectivityToLoggingService", func(t *testing.T) { 37 | // get the required resource values using terratest's terraform module 38 | loggingEndpoint := terraform.Output(t, terraformOptions, "logging_endpoint") 39 | loggingPort := "443" 40 | tag := terraform.Output(t, terraformOptions, "instance_name_tag") 41 | tester.TcpConnectionTestWithTagName(t, ssmClient, tag, loggingEndpoint, loggingPort, retryConfig) 42 | }) 43 | t.Run("TestAppInstanceConnectivityToMonitoringService", func(t *testing.T) { 44 | // get the required resource values using terratest's terraform module 45 | monitoringEndpoint := terraform.Output(t, terraformOptions, "monitoring_endpoint") 46 | monitoringPort := "443" 47 | tag := terraform.Output(t, terraformOptions, "instance_name_tag") 48 | tester.TcpConnectionTestWithTagName(t, ssmClient, tag, monitoringEndpoint, monitoringPort, retryConfig) 49 | }) 50 | t.Run("TestAppInstanceShouldNotHaveConnectivityToPublicInternet", func(t *testing.T) { 51 | // build a tcp connectivity test case with public endpoint and port 52 | testCase := tester.NewShellTestCase(fmt.Sprintf("timeout 2 bash -c '