diff --git a/code/ruby/07-testing-terraform-code/web-server-full-di.rb b/code/ruby/07-testing-terraform-code/web-server-full-di.rb index 4e3b9d1e..8e471d0e 100644 --- a/code/ruby/07-testing-terraform-code/web-server-full-di.rb +++ b/code/ruby/07-testing-terraform-code/web-server-full-di.rb @@ -22,39 +22,20 @@ def initialize(web_service) def handle(path) case path when "/" - self.hello + [200, 'text/plain', 'Hello, World'] when "/api" - self.api + [201, 'application/json', '{"foo":"bar"}'] when "/web-service" - self.web_service - else - self.not_found - end - end - - def hello - [200, 'text/plain', 'Hello, World'] - end - - def api - [201, 'application/json', '{"foo":"bar"}'] - end - + # New endpoint that calls a web service =begin - - def web_service - uri = URI("http://www.example.org") - response = Net::HTTP.get_response(uri) - [response.code.to_i, response['Content-Type'], response.body] - end + uri = URI("http://www.example.org") + response = Net::HTTP.get_response(uri) + [response.code.to_i, response['Content-Type'], response.body] =end - - def web_service - @web_service.proxy - end - - def not_found - [404, 'text/plain', 'Not Found'] + @web_service.proxy + else + [404, 'text/plain', 'Not Found'] + end end end diff --git a/code/ruby/07-testing-terraform-code/web-server-full.rb b/code/ruby/07-testing-terraform-code/web-server-full.rb index bcdfec3d..5516ff7f 100644 --- a/code/ruby/07-testing-terraform-code/web-server-full.rb +++ b/code/ruby/07-testing-terraform-code/web-server-full.rb @@ -15,25 +15,13 @@ class Handlers def handle(path) case path when "/" - self.hello + [200, 'text/plain', 'Hello, World'] when "/api" - self.api + [201, 'application/json', '{"foo":"bar"}'] else - self.not_found + [404, 'text/plain', 'Not Found'] end end - - def hello - [200, 'text/plain', 'Hello, World'] - end - - def api - [201, 'application/json', '{"foo":"bar"}'] - end - - def not_found - [404, 'text/plain', 'Not Found'] - end end # This will only run if this script was called directly from the CLI, but diff --git a/code/ruby/08-terraform-team/web-server.rb b/code/ruby/08-terraform-team/web-server.rb index 6608ba5d..454ae823 100644 --- a/code/ruby/08-terraform-team/web-server.rb +++ b/code/ruby/08-terraform-team/web-server.rb @@ -22,31 +22,15 @@ def initialize(web_service) def handle(path) case path when "/" - self.hello + [200, 'text/plain', 'Hello, World'] when "/api" - self.api + [201, 'application/json', '{"foo":"bar"}'] when "/web-service" - self.web_service + @web_service.proxy else - self.not_found + [404, 'text/plain', 'Not Found'] end end - - def hello - [200, 'text/plain', 'Hello, World'] - end - - def api - [201, 'application/json', '{"foo":"bar"}'] - end - - def web_service - @web_service.proxy - end - - def not_found - [404, 'text/plain', 'Not Found'] - end end class WebService diff --git a/code/terraform/02-intro-to-terraform-syntax/webserver-cluster/main.tf b/code/terraform/02-intro-to-terraform-syntax/webserver-cluster/main.tf index fe2b1090..d4167d13 100644 --- a/code/terraform/02-intro-to-terraform-syntax/webserver-cluster/main.tf +++ b/code/terraform/02-intro-to-terraform-syntax/webserver-cluster/main.tf @@ -19,6 +19,12 @@ resource "aws_launch_configuration" "example" { echo "Hello, World" > index.html nohup busybox httpd -f -p ${var.server_port} & EOF + + # Required when using a launch configuration with an auto scaling group. + # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html + lifecycle { + create_before_destroy = true + } } resource "aws_autoscaling_group" "example" { diff --git a/code/terraform/03-terraform-state/file-layout-example/stage/services/webserver-cluster/main.tf b/code/terraform/03-terraform-state/file-layout-example/stage/services/webserver-cluster/main.tf index db4daa87..1661d468 100644 --- a/code/terraform/03-terraform-state/file-layout-example/stage/services/webserver-cluster/main.tf +++ b/code/terraform/03-terraform-state/file-layout-example/stage/services/webserver-cluster/main.tf @@ -14,6 +14,12 @@ resource "aws_launch_configuration" "example" { instance_type = "t2.micro" security_groups = [aws_security_group.instance.id] user_data = data.template_file.user_data.rendered + + # Required when using a launch configuration with an auto scaling group. + # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html + lifecycle { + create_before_destroy = true + } } data "template_file" "user_data" { diff --git a/code/terraform/04-terraform-module/module-example/modules/services/webserver-cluster/main.tf b/code/terraform/04-terraform-module/module-example/modules/services/webserver-cluster/main.tf index 9b7ce868..fb3ee663 100644 --- a/code/terraform/04-terraform-module/module-example/modules/services/webserver-cluster/main.tf +++ b/code/terraform/04-terraform-module/module-example/modules/services/webserver-cluster/main.tf @@ -7,6 +7,12 @@ resource "aws_launch_configuration" "example" { instance_type = var.instance_type security_groups = [aws_security_group.instance.id] user_data = data.template_file.user_data.rendered + + # Required when using a launch configuration with an auto scaling group. + # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html + lifecycle { + create_before_destroy = true + } } data "template_file" "user_data" { diff --git a/code/terraform/05-tips-and-tricks/loops-and-if-statements/modules/services/webserver-cluster/main.tf b/code/terraform/05-tips-and-tricks/loops-and-if-statements/modules/services/webserver-cluster/main.tf index 557364f7..141e74e0 100644 --- a/code/terraform/05-tips-and-tricks/loops-and-if-statements/modules/services/webserver-cluster/main.tf +++ b/code/terraform/05-tips-and-tricks/loops-and-if-statements/modules/services/webserver-cluster/main.tf @@ -12,6 +12,12 @@ resource "aws_launch_configuration" "example" { ? data.template_file.user_data[0].rendered : data.template_file.user_data_new[0].rendered ) + + # Required when using a launch configuration with an auto scaling group. + # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html + lifecycle { + create_before_destroy = true + } } data "template_file" "user_data" { diff --git a/code/terraform/05-tips-and-tricks/zero-downtime-deployment/modules/services/webserver-cluster/main.tf b/code/terraform/05-tips-and-tricks/zero-downtime-deployment/modules/services/webserver-cluster/main.tf index 743a4b89..8a3aceb1 100644 --- a/code/terraform/05-tips-and-tricks/zero-downtime-deployment/modules/services/webserver-cluster/main.tf +++ b/code/terraform/05-tips-and-tricks/zero-downtime-deployment/modules/services/webserver-cluster/main.tf @@ -8,6 +8,12 @@ resource "aws_launch_configuration" "example" { security_groups = [aws_security_group.instance.id] user_data = data.template_file.user_data.rendered + + # Required when using a launch configuration with an auto scaling group. + # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html + lifecycle { + create_before_destroy = true + } } data "template_file" "user_data" { @@ -22,8 +28,8 @@ data "template_file" "user_data" { } resource "aws_autoscaling_group" "example" { - # Explicitly depend on the launch configuration's name so each time it's replaced, - # this ASG is also replaced + # Explicitly depend on the launch configuration's name so each time it's + # replaced, this ASG is also replaced name = "${var.cluster_name}-${aws_launch_configuration.example.name}" launch_configuration = aws_launch_configuration.example.name diff --git a/code/terraform/06-production-grade-infrastructure/small-modules/modules/cluster/asg-rolling-deploy/main.tf b/code/terraform/06-production-grade-infrastructure/small-modules/modules/cluster/asg-rolling-deploy/main.tf index 01680c0d..23531cbb 100644 --- a/code/terraform/06-production-grade-infrastructure/small-modules/modules/cluster/asg-rolling-deploy/main.tf +++ b/code/terraform/06-production-grade-infrastructure/small-modules/modules/cluster/asg-rolling-deploy/main.tf @@ -7,6 +7,12 @@ resource "aws_launch_configuration" "example" { instance_type = var.instance_type security_groups = [aws_security_group.instance.id] user_data = var.user_data + + # Required when using a launch configuration with an auto scaling group. + # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html + lifecycle { + create_before_destroy = true + } } resource "aws_autoscaling_group" "example" { diff --git a/code/terraform/07-testing-terraform-code/examples/hello-world-app/standalone/variables.tf b/code/terraform/07-testing-terraform-code/examples/hello-world-app/standalone/variables.tf index 0446c2a5..892f51e2 100644 --- a/code/terraform/07-testing-terraform-code/examples/hello-world-app/standalone/variables.tf +++ b/code/terraform/07-testing-terraform-code/examples/hello-world-app/standalone/variables.tf @@ -6,7 +6,7 @@ variable "mysql_config" { description = "The config for the MySQL DB" - type = object({ + type = object({ address = string port = number }) diff --git a/code/terraform/07-testing-terraform-code/modules/cluster/asg-rolling-deploy/main.tf b/code/terraform/07-testing-terraform-code/modules/cluster/asg-rolling-deploy/main.tf index f20c8745..931d80c6 100644 --- a/code/terraform/07-testing-terraform-code/modules/cluster/asg-rolling-deploy/main.tf +++ b/code/terraform/07-testing-terraform-code/modules/cluster/asg-rolling-deploy/main.tf @@ -7,6 +7,12 @@ resource "aws_launch_configuration" "example" { instance_type = var.instance_type security_groups = [aws_security_group.instance.id] user_data = var.user_data + + # Required when using a launch configuration with an auto scaling group. + # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html + lifecycle { + create_before_destroy = true + } } resource "aws_autoscaling_group" "example" { diff --git a/code/terraform/07-testing-terraform-code/test/hello_world_app_example_test.go b/code/terraform/07-testing-terraform-code/test/hello_world_app_example_test.go index 2c3f18a0..90614e19 100644 --- a/code/terraform/07-testing-terraform-code/test/hello_world_app_example_test.go +++ b/code/terraform/07-testing-terraform-code/test/hello_world_app_example_test.go @@ -16,8 +16,8 @@ func TestHelloWorldAppExample(t *testing.T) { t.Parallel() opts := &terraform.Options{ - // You should update this relative path to point at your alb - // example directory! + // You should update this relative path to point at your + // hello-world-app example directory! TerraformDir: "../examples/hello-world-app/standalone", Vars: map[string]interface{}{ diff --git a/code/terraform/07-testing-terraform-code/test/hello_world_integration_test.go b/code/terraform/07-testing-terraform-code/test/hello_world_integration_test.go index 3ec5a275..9d9d23ca 100644 --- a/code/terraform/07-testing-terraform-code/test/hello_world_integration_test.go +++ b/code/terraform/07-testing-terraform-code/test/hello_world_integration_test.go @@ -11,9 +11,30 @@ import ( "time" ) +const dbDirProd = "../live/prod/data-stores/mysql" +const appDirProd = "../live/prod/services/hello-world-app" +// Replace these with the proper paths to your modules +const dbDirStage = "../live/stage/data-stores/mysql" +const appDirStage = "../live/stage/services/hello-world-app" + func TestHelloWorldAppStage(t *testing.T) { t.Parallel() + // Deploy the MySQL DB + dbOpts := createDbOpts(t, dbDirStage) + defer terraform.Destroy(t, dbOpts) + terraform.InitAndApply(t, dbOpts) + + // Deploy the hello-world-app + helloOpts := createHelloOpts(dbOpts, appDirStage) + defer terraform.Destroy(t, helloOpts) + terraform.InitAndApply(t, helloOpts) + + // Validate the hello-world-app works + validateHelloApp(t, helloOpts) +} + +func createDbOpts(t *testing.T, terraformDir string) *terraform.Options { uniqueId := random.UniqueId() bucketForTesting := GetRequiredEnvVar(t, TerraformStateBucketForTestEnvVarName) @@ -21,11 +42,11 @@ func TestHelloWorldAppStage(t *testing.T) { dbStateKey := fmt.Sprintf("%s/%s/terraform.tfstate", t.Name(), uniqueId) - dbOpts := &terraform.Options{ - TerraformDir: "../live/stage/data-stores/mysql", + return &terraform.Options{ + TerraformDir: terraformDir, Vars: map[string]interface{}{ - "db_name": fmt.Sprintf("test_%s", uniqueId), + "db_name": fmt.Sprintf("test%s", uniqueId), "db_password": "password", }, @@ -36,23 +57,32 @@ func TestHelloWorldAppStage(t *testing.T) { "encrypt": true, }, } +} - defer terraform.Destroy(t, dbOpts) - terraform.InitAndApply(t, dbOpts) +func createHelloOpts( + dbOpts *terraform.Options, + terraformDir string) *terraform.Options { - helloOpts := &terraform.Options{ - TerraformDir: "../live/stage/services/hello-world-app", + return &terraform.Options{ + TerraformDir: terraformDir, Vars: map[string]interface{}{ - "db_remote_state_bucket": bucketForTesting, - "db_remote_state_key": dbStateKey, - "environment": fmt.Sprintf("test-%s", uniqueId), + "db_remote_state_bucket": dbOpts.BackendConfig["bucket"], + "db_remote_state_key": dbOpts.BackendConfig["key"], + "environment": dbOpts.Vars["db_name"], }, - } - defer terraform.Destroy(t, helloOpts) - terraform.InitAndApply(t, helloOpts) + // Retry up to 3 times, with 5 seconds between retries, + // on known errors + MaxRetries: 3, + TimeBetweenRetries: 5 * time.Second, + RetryableTerraformErrors: map[string]string{ + "RequestError: send request failed": "Throttling issue?", + }, + } +} +func validateHelloApp(t *testing.T, helloOpts *terraform.Options) { albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") url := fmt.Sprintf("http://%s", albDnsName) @@ -65,261 +95,123 @@ func TestHelloWorldAppStage(t *testing.T) { maxRetries, timeBetweenRetries, func(status int, body string) bool { - return status == 200 && strings.Contains(body, "Hello, World") + return status == 200 && + strings.Contains(body, "Hello, World") }, ) - } -func TestHelloWorldAppStageWithStages(t *testing.T) { +func TestHelloWorldAppStageWithStages(t *testing.T) { t.Parallel() - bucketForTesting := GetRequiredEnvVar(t, TerraformStateBucketForTestEnvVarName) - bucketRegionForTesting := GetRequiredEnvVar(t, TerraformStateRegionForTestEnvVarName) - - dbModuleDir := "../live/stage/data-stores/mysql" - - defer test_structure.RunTestStage(t, "teardown_db", func() { - dbOpts := test_structure.LoadTerraformOptions(t, dbModuleDir) - defer terraform.Destroy(t, dbOpts) - }) - - test_structure.RunTestStage(t, "deploy_db", func() { - uniqueId := random.UniqueId() - dbStateKey := fmt.Sprintf( - "%s/%s/terraform.tfstate", - t.Name(), - uniqueId, - ) - - dbOpts := &terraform.Options{ - TerraformDir: dbModuleDir, - - Vars: map[string]interface{}{ - "db_name": fmt.Sprintf("test_%s", uniqueId), - "db_password": "password", - }, - - BackendConfig: map[string]interface{}{ - "bucket": bucketForTesting, - "region": bucketRegionForTesting, - "key": dbStateKey, - "encrypt": true, - }, - } - - // Save data to disk so that other test stages executed at a later - // time can read the data back in - test_structure.SaveTerraformOptions(t, dbModuleDir, dbOpts) - test_structure.SaveString(t, dbModuleDir, "uniqueId", uniqueId) - - terraform.InitAndApply(t, dbOpts) - }) - - helloWorldAppDir := "../live/stage/services/hello-world-app" - - defer test_structure.RunTestStage(t, "teardown_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) - defer terraform.Destroy(t, helloOpts) - }) - - test_structure.RunTestStage(t, "deploy_hello_world_app", func() { - uniqueId := test_structure.LoadString(t, dbModuleDir, "uniqueId") - dbOpts := test_structure.LoadTerraformOptions(t, dbModuleDir) - - helloOpts := &terraform.Options{ - TerraformDir: helloWorldAppDir, - - Vars: map[string]interface{}{ - "db_remote_state_bucket": bucketForTesting, - "db_remote_state_key": dbOpts.BackendConfig["key"], - "environment": fmt.Sprintf("test-%s", uniqueId), - }, - - // Retry up to 3 times, with 5 seconds between retries, - // on known errors - MaxRetries: 3, - TimeBetweenRetries: 5 * time.Second, - RetryableTerraformErrors: map[string]string{ - "RequestError: send request failed": "Throttling issue?", - }, - } - - test_structure.SaveTerraformOptions(t, helloWorldAppDir, helloOpts) - - terraform.InitAndApply(t, helloOpts) - }) - - test_structure.RunTestStage(t, "validate_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) - - albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") - url := fmt.Sprintf("http://%s", albDnsName) - - maxRetries := 10 - timeBetweenRetries := 10 * time.Second - - http_helper.HttpGetWithRetryWithCustomValidation( - t, - url, - maxRetries, - timeBetweenRetries, - func(status int, body string) bool { - return status == 200 && strings.Contains(body, "Hello, World") - }, - ) - }) - - test_structure.RunTestStage(t, "redeploy_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) - - albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") - url := fmt.Sprintf("http://%s", albDnsName) - - // Start checking every 1s that the app is responding with a 200 OK - stopChecking := make(chan bool, 1) - waitGroup, _ := http_helper.ContinuouslyCheckUrl( - t, - url, - stopChecking, - 1*time.Second, - ) - - // Update the server text and redeploy - newServerText := "Hello, World, v2!" - helloOpts.Vars["server_text"] = newServerText - terraform.Apply(t, helloOpts) - - // Make sure the new version deployed - maxRetries := 10 - timeBetweenRetries := 10 * time.Second - http_helper.HttpGetWithRetryWithCustomValidation( - t, - url, - maxRetries, - timeBetweenRetries, - func(status int, body string) bool { - return status == 200 && strings.Contains(body, newServerText) - }, - ) - - // Stop checking - stopChecking <- true - waitGroup.Wait() - }) - -} - -func TestHelloWorldAppProdWithStages(t *testing.T) { - t.Parallel() - - bucketForTesting := GetRequiredEnvVar(t, TerraformStateBucketForTestEnvVarName) - bucketRegionForTesting := GetRequiredEnvVar(t, TerraformStateRegionForTestEnvVarName) + // Store the function in a short variable name solely to make the + // code examples fit better in the book. + stage := test_structure.RunTestStage - dbModuleDir := "../live/prod/data-stores/mysql" + // Deploy the MySQL DB + defer stage(t, "teardown_db", func() { teardownDb(t, dbDirStage) }) + stage(t, "deploy_db", func() { deployDb(t, dbDirStage) }) - defer test_structure.RunTestStage(t, "teardown_db", func() { - dbOpts := test_structure.LoadTerraformOptions(t, dbModuleDir) - defer terraform.Destroy(t, dbOpts) - }) + // Deploy the hello-world-app + defer stage(t, "teardown_app", func() { teardownApp(t, appDirStage) }) + stage(t, "deploy_app", func() { deployApp(t, dbDirStage, appDirStage) }) - test_structure.RunTestStage(t, "deploy_db", func() { - uniqueId := random.UniqueId() - dbStateKey := fmt.Sprintf("%s/%s/terraform.tfstate", t.Name(), uniqueId) + // Validate the hello-world-app works + stage(t, "validate_app", func() { validateApp(t, appDirStage) }) - dbOpts := &terraform.Options{ - TerraformDir: dbModuleDir, - - Vars: map[string]interface{}{ - "db_name": fmt.Sprintf("test_%s", uniqueId), - "db_password": "password", - }, + // Redeploy the hello-world-app + stage(t, "redeploy_app", func() { redeployApp(t, appDirStage) }) +} - BackendConfig: map[string]interface{}{ - "bucket": bucketForTesting, - "region": bucketRegionForTesting, - "key": dbStateKey, - "encrypt": true, - }, - } +func teardownDb(t *testing.T, dbDir string) { + dbOpts := test_structure.LoadTerraformOptions(t, dbDir) + defer terraform.Destroy(t, dbOpts) +} - test_structure.SaveTerraformOptions(t, dbModuleDir, dbOpts) - test_structure.SaveString(t, dbModuleDir, "uniqueId", uniqueId) +func deployDb(t *testing.T, dbDir string) { + dbOpts := createDbOpts(t, dbDir) - terraform.InitAndApply(t, dbOpts) - }) + // Save data to disk so that other test stages executed at a later + // time can read the data back in + test_structure.SaveTerraformOptions(t, dbDir, dbOpts) - helloWorldAppDir := "../live/prod/services/hello-world-app" + terraform.InitAndApply(t, dbOpts) +} - defer test_structure.RunTestStage(t, "teardown_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) - defer terraform.Destroy(t, helloOpts) - }) +func teardownApp(t *testing.T, helloAppDir string) { + helloOpts := test_structure.LoadTerraformOptions(t, helloAppDir) + defer terraform.Destroy(t, helloOpts) +} - test_structure.RunTestStage(t, "deploy_hello_world_app", func() { - uniqueId := test_structure.LoadString(t, dbModuleDir, "uniqueId") - dbOpts := test_structure.LoadTerraformOptions(t, dbModuleDir) +func deployApp(t *testing.T, dbDir string, helloAppDir string) { + dbOpts := test_structure.LoadTerraformOptions(t, dbDir) + helloOpts := createHelloOpts(dbOpts, helloAppDir) - helloOpts := &terraform.Options{ - TerraformDir: helloWorldAppDir, + // Save data to disk so that other test stages executed at a later + // time can read the data back in + test_structure.SaveTerraformOptions(t, helloAppDir, helloOpts) - Vars: map[string]interface{}{ - "db_remote_state_bucket": bucketForTesting, - "db_remote_state_key": dbOpts.BackendConfig["key"], - "environment": fmt.Sprintf("test-%s", uniqueId), - }, + terraform.InitAndApply(t, helloOpts) +} - // Retry up to 3 times, with 5 seconds between retries, on known errors - MaxRetries: 3, - TimeBetweenRetries: 5 * time.Second, - RetryableTerraformErrors: map[string]string{ - "RequestError: send request failed": "Intermittent error, possibly due to throttling?", - }, - } +func validateApp(t *testing.T, helloAppDir string) { + helloOpts := test_structure.LoadTerraformOptions(t, helloAppDir) + validateHelloApp(t, helloOpts) +} - test_structure.SaveTerraformOptions(t, helloWorldAppDir, helloOpts) +func redeployApp(t *testing.T, helloAppDir string) { + helloOpts := test_structure.LoadTerraformOptions(t, helloAppDir) - terraform.InitAndApply(t, helloOpts) - }) + albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") + url := fmt.Sprintf("http://%s", albDnsName) - test_structure.RunTestStage(t, "validate_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) + // Start checking every 1s that the app is responding with a 200 OK + stopChecking := make(chan bool, 1) + waitGroup, _ := http_helper.ContinuouslyCheckUrl( + t, + url, + stopChecking, + 1*time.Second, + ) - albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") - url := fmt.Sprintf("http://%s", albDnsName) + // Update the server text and redeploy + newServerText := "Hello, World, v2!" + helloOpts.Vars["server_text"] = newServerText + terraform.Apply(t, helloOpts) - maxRetries := 10 - timeBetweenRetries := 10 * time.Second + // Make sure the new version deployed + maxRetries := 10 + timeBetweenRetries := 10 * time.Second + http_helper.HttpGetWithRetryWithCustomValidation( + t, + url, + maxRetries, + timeBetweenRetries, + func(status int, body string) bool { + return status == 200 && + strings.Contains(body, newServerText) + }, + ) - http_helper.HttpGetWithRetryWithCustomValidation(t, url, maxRetries, timeBetweenRetries, func(status int, body string) bool { - return status == 200 && strings.Contains(body, "Hello, World") - }) - }) + // Stop checking + stopChecking <- true + waitGroup.Wait() +} - test_structure.RunTestStage(t, "redeploy_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) +func TestHelloWorldAppProdWithStages(t *testing.T) { + t.Parallel() - albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") - url := fmt.Sprintf("http://%s", albDnsName) + // Deploy the MySQL DB + defer test_structure.RunTestStage(t, "teardown_db", func() { teardownDb(t, dbDirProd) }) + test_structure.RunTestStage(t, "deploy_db", func() { deployDb(t, dbDirProd) }) - // Start checking every 1s that the app is responding with a 200 OK - stopChecking := make(chan bool, 1) - waitGroup, _ := http_helper.ContinuouslyCheckUrl(t, url, stopChecking, 1*time.Second) + // Deploy the hello-world-app + defer test_structure.RunTestStage(t, "teardown_app", func() { teardownApp(t, appDirProd) }) + test_structure.RunTestStage(t, "deploy_app", func() { deployApp(t, dbDirProd, appDirProd) }) - // Update the server text and redeploy - newServerText := "Hello, World, v2!" - helloOpts.Vars["server_text"] = newServerText - terraform.Apply(t, helloOpts) + // Validate the hello-world-app works + test_structure.RunTestStage(t, "validate_app", func() { validateApp(t, appDirProd) }) - // Make sure the new version deployed - maxRetries := 10 - timeBetweenRetries := 10 * time.Second - http_helper.HttpGetWithRetryWithCustomValidation(t, url, maxRetries, timeBetweenRetries, func(status int, body string) bool { - return status == 200 && strings.Contains(body, newServerText) - }) - - // Stop checking - stopChecking <- true - waitGroup.Wait() - }) + // Redeploy the hello-world-app + test_structure.RunTestStage(t, "redeploy_app", func() { redeployApp(t, appDirProd) }) } diff --git a/code/terraform/07-testing-terraform-code/test/mysql_example_test.go b/code/terraform/07-testing-terraform-code/test/mysql_example_test.go index 418434f5..f3799c73 100644 --- a/code/terraform/07-testing-terraform-code/test/mysql_example_test.go +++ b/code/terraform/07-testing-terraform-code/test/mysql_example_test.go @@ -11,7 +11,7 @@ func TestMySqlExample(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ - // You should update this relative path to point at your alb + // You should update this relative path to point at your mysql // example directory! TerraformDir: "../examples/mysql", Vars: map[string]interface{}{ diff --git a/code/terraform/08-terraform-team/modules/cluster/asg-rolling-deploy/main.tf b/code/terraform/08-terraform-team/modules/cluster/asg-rolling-deploy/main.tf index f20c8745..931d80c6 100644 --- a/code/terraform/08-terraform-team/modules/cluster/asg-rolling-deploy/main.tf +++ b/code/terraform/08-terraform-team/modules/cluster/asg-rolling-deploy/main.tf @@ -7,6 +7,12 @@ resource "aws_launch_configuration" "example" { instance_type = var.instance_type security_groups = [aws_security_group.instance.id] user_data = var.user_data + + # Required when using a launch configuration with an auto scaling group. + # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html + lifecycle { + create_before_destroy = true + } } resource "aws_autoscaling_group" "example" { diff --git a/code/terraform/08-terraform-team/test/asg_example_test.go b/code/terraform/08-terraform-team/test/asg_example_test.go index 4c58102c..18b3a6b7 100644 --- a/code/terraform/08-terraform-team/test/asg_example_test.go +++ b/code/terraform/08-terraform-team/test/asg_example_test.go @@ -11,7 +11,7 @@ func TestAsgExample(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ - // You should update this relative path to point at your alb + // You should update this relative path to point at your asg // example directory! TerraformDir: "../examples/asg", Vars: map[string]interface{}{ diff --git a/code/terraform/08-terraform-team/test/hello_world_integration_test.go b/code/terraform/08-terraform-team/test/hello_world_integration_test.go index db8b141a..e463d7b8 100644 --- a/code/terraform/08-terraform-team/test/hello_world_integration_test.go +++ b/code/terraform/08-terraform-team/test/hello_world_integration_test.go @@ -11,253 +11,183 @@ import ( "time" ) -func TestHelloWorldAppStageWithStages(t *testing.T) { - t.Parallel() - - dbModuleDir := "../live/stage/data-stores/mysql" +// Replace these with the proper paths to your modules +const dbDirProd = "../live/prod/data-stores/mysql" +const appDirProd = "../live/prod/services/hello-world-app" +const dbDirStage = "../live/stage/data-stores/mysql" +const appDirStage = "../live/stage/services/hello-world-app" - defer test_structure.RunTestStage(t, "teardown_db", func() { - dbOpts := test_structure.LoadTerraformOptions(t, dbModuleDir) - defer terraform.Destroy(t, dbOpts) - }) - - test_structure.RunTestStage(t, "deploy_db", func() { - uniqueId := random.UniqueId() - - dbOpts := &terraform.Options{ - TerraformDir: dbModuleDir, - - // These modules use Terragrunt as a wrapper for Terraform - TerraformBinary: "terragrunt", - - Vars: map[string]interface{}{ - "db_name": fmt.Sprintf("test_%s", uniqueId), - "db_password": "password", - }, - - // terragrunt.hcl looks up its settings using env vars - EnvVars: map[string]string{ - "TEST_STATE_DYNAMODB_TABLE": uniqueId, - }, - } - - // Save data to disk so that other test stages executed at a later - // time can read the data back in - test_structure.SaveTerraformOptions(t, dbModuleDir, dbOpts) - test_structure.SaveString(t, dbModuleDir, "uniqueId", uniqueId) - terraform.InitAndApply(t, dbOpts) - }) - - helloWorldAppDir := "../live/stage/services/hello-world-app" - - defer test_structure.RunTestStage(t, "teardown_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) - defer terraform.Destroy(t, helloOpts) - }) - - test_structure.RunTestStage(t, "deploy_hello_world_app", func() { - uniqueId := test_structure.LoadString(t, dbModuleDir, "uniqueId") - - helloOpts := &terraform.Options{ - TerraformDir: helloWorldAppDir, - - // These modules use Terragrunt as a wrapper for Terraform - TerraformBinary: "terragrunt", - - Vars: map[string]interface{}{ - "environment": fmt.Sprintf("test-%s", uniqueId), - }, - - // terragrunt.hcl looks up its settings using env vars - EnvVars: map[string]string{ - "TEST_STATE_DYNAMODB_TABLE": uniqueId, - }, - - // Retry up to 3 times, with 5 seconds between retries, - // on known errors - MaxRetries: 3, - TimeBetweenRetries: 5 * time.Second, - RetryableTerraformErrors: map[string]string{ - "RequestError: send request failed": "Throttling issue?", - }, - } - - test_structure.SaveTerraformOptions(t, helloWorldAppDir, helloOpts) - - terraform.InitAndApply(t, helloOpts) - }) - - test_structure.RunTestStage(t, "validate_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) - - albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") - url := fmt.Sprintf("http://%s", albDnsName) - - maxRetries := 10 - timeBetweenRetries := 10 * time.Second - - http_helper.HttpGetWithRetryWithCustomValidation( - t, - url, - maxRetries, - timeBetweenRetries, - func(status int, body string) bool { - return status == 200 && strings.Contains(body, "Hello, World") - }, - ) - }) - - test_structure.RunTestStage(t, "redeploy_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) - - albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") - url := fmt.Sprintf("http://%s", albDnsName) - - // Start checking every 1s that the app is responding with a 200 OK - stopChecking := make(chan bool, 1) - waitGroup, _ := http_helper.ContinuouslyCheckUrl( - t, - url, - stopChecking, - 1*time.Second, - ) - - // Update the server text and redeploy - newServerText := "Hello, World, v2!" - helloOpts.Vars["server_text"] = newServerText - terraform.Apply(t, helloOpts) - - // Make sure the new version deployed - maxRetries := 10 - timeBetweenRetries := 10 * time.Second - http_helper.HttpGetWithRetryWithCustomValidation( - t, - url, - maxRetries, - timeBetweenRetries, - func(status int, body string) bool { - return status == 200 && strings.Contains(body, newServerText) - }, - ) - - // Stop checking - stopChecking <- true - waitGroup.Wait() - }) -} - -func TestHelloWorldAppProdWithStages(t *testing.T) { +func TestHelloWorldAppStageWithStages(t *testing.T) { t.Parallel() - dbModuleDir := "../live/prod/data-stores/mysql" + // Deploy the MySQL DB + defer test_structure.RunTestStage(t, "teardown_db", func() { teardownDb(t, dbDirStage) }) + test_structure.RunTestStage(t, "deploy_db", func() { deployDb(t, dbDirStage) }) - defer test_structure.RunTestStage(t, "teardown_db", func() { - dbOpts := test_structure.LoadTerraformOptions(t, dbModuleDir) - defer terraform.Destroy(t, dbOpts) - }) + // Deploy the hello-world-app + defer test_structure.RunTestStage(t, "teardown_app", func() { teardownHelloApp(t, appDirStage) }) + test_structure.RunTestStage(t, "deploy_app", func() { deployHelloApp(t, dbDirStage, appDirStage) }) - test_structure.RunTestStage(t, "deploy_db", func() { - uniqueId := random.UniqueId() + // Validate the hello-world-app works + test_structure.RunTestStage(t, "validate_app", func() { validateHelloApp(t, appDirStage) }) - dbOpts := &terraform.Options{ - TerraformDir: dbModuleDir, - - // These modules use Terragrunt as a wrapper for Terraform - TerraformBinary: "terragrunt", + // Redeploy the hello-world-app + test_structure.RunTestStage(t, "redeploy_app", func() { redeployHelloApp(t, appDirStage) }) +} - Vars: map[string]interface{}{ - "db_name": fmt.Sprintf("test_%s", uniqueId), - "db_password": "password", - }, +func TestHelloWorldAppProdWithStages(t *testing.T) { + t.Parallel() - // terragrunt.hcl looks up its settings using env vars - EnvVars: map[string]string{ - "TEST_STATE_DYNAMODB_TABLE": uniqueId, - }, - } + // Deploy the MySQL DB + defer test_structure.RunTestStage(t, "teardown_db", func() { teardownDb(t, dbDirProd) }) + test_structure.RunTestStage(t, "deploy_db", func() { deployDb(t, dbDirProd) }) - test_structure.SaveTerraformOptions(t, dbModuleDir, dbOpts) - test_structure.SaveString(t, dbModuleDir, "uniqueId", uniqueId) + // Deploy the hello-world-app + defer test_structure.RunTestStage(t, "teardown_app", func() { teardownHelloApp(t, appDirProd) }) + test_structure.RunTestStage(t, "deploy_app", func() { deployHelloApp(t, dbDirProd, appDirProd) }) - terraform.InitAndApply(t, dbOpts) - }) + // Validate the hello-world-app works + test_structure.RunTestStage(t, "validate_app", func() { validateHelloApp(t, appDirProd) }) - helloWorldAppDir := "../live/prod/services/hello-world-app" + // Redeploy the hello-world-app + test_structure.RunTestStage(t, "redeploy_app", func() { redeployHelloApp(t, appDirProd) }) +} - defer test_structure.RunTestStage(t, "teardown_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) - defer terraform.Destroy(t, helloOpts) - }) +func createDbOpts(terraformDir string, uniqueId string) *terraform.Options { + return &terraform.Options{ + TerraformDir: terraformDir, - test_structure.RunTestStage(t, "deploy_hello_world_app", func() { - uniqueId := test_structure.LoadString(t, dbModuleDir, "uniqueId") + // These modules use Terragrunt as a wrapper for Terraform + TerraformBinary: "terragrunt", - helloOpts := &terraform.Options{ - TerraformDir: helloWorldAppDir, + Vars: map[string]interface{}{ + "db_name": fmt.Sprintf("test%s", uniqueId), + "db_password": "password", + }, - // These modules use Terragrunt as a wrapper for Terraform - TerraformBinary: "terragrunt", + // terragrunt.hcl looks up its settings using env vars + EnvVars: map[string]string{ + "TEST_STATE_DYNAMODB_TABLE": uniqueId, + }, + } +} - Vars: map[string]interface{}{ - "environment": fmt.Sprintf("test-%s", uniqueId), - }, +func createHelloOpts(terraformDir string, uniqueId string) *terraform.Options { + return &terraform.Options{ + TerraformDir: terraformDir, + + // These modules use Terragrunt as a wrapper for Terraform + TerraformBinary: "terragrunt", + + Vars: map[string]interface{}{ + "environment": fmt.Sprintf("test-%s", uniqueId), + }, + + // terragrunt.hcl looks up its settings using env vars + EnvVars: map[string]string{ + "TEST_STATE_DYNAMODB_TABLE": uniqueId, + }, + + // Retry up to 3 times, with 5 seconds between retries, + // on known errors + MaxRetries: 3, + TimeBetweenRetries: 5 * time.Second, + RetryableTerraformErrors: map[string]string{ + "RequestError: send request failed": "Throttling issue?", + }, + } +} - // terragrunt.hcl looks up its settings using env vars - EnvVars: map[string]string{ - "TEST_STATE_DYNAMODB_TABLE": uniqueId, - }, +func validateApp(t *testing.T, helloOpts *terraform.Options) { + albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") + url := fmt.Sprintf("http://%s", albDnsName) - // Retry up to 3 times, with 5 seconds between retries, on known errors - MaxRetries: 3, - TimeBetweenRetries: 5 * time.Second, - RetryableTerraformErrors: map[string]string{ - "RequestError: send request failed": "Intermittent error, possibly due to throttling?", - }, - } + maxRetries := 10 + timeBetweenRetries := 10 * time.Second - test_structure.SaveTerraformOptions(t, helloWorldAppDir, helloOpts) + http_helper.HttpGetWithRetryWithCustomValidation( + t, + url, + maxRetries, + timeBetweenRetries, + func(status int, body string) bool { + return status == 200 && strings.Contains(body, "Hello, World") + }, + ) +} - terraform.InitAndApply(t, helloOpts) - }) +func teardownDb(t *testing.T, dbDir string) { + dbOpts := test_structure.LoadTerraformOptions(t, dbDir) + defer terraform.Destroy(t, dbOpts) +} - test_structure.RunTestStage(t, "validate_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) +func deployDb(t *testing.T, dbDir string) { + uniqueId := random.UniqueId() + dbOpts := createDbOpts(dbDir, uniqueId) - albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") - url := fmt.Sprintf("http://%s", albDnsName) + // Save data to disk so that other test stages executed at a later + // time can read the data back in + test_structure.SaveTerraformOptions(t, dbDir, dbOpts) + test_structure.SaveString(t, dbDir, "uniqueId", uniqueId) - maxRetries := 10 - timeBetweenRetries := 10 * time.Second + terraform.InitAndApply(t, dbOpts) +} - http_helper.HttpGetWithRetryWithCustomValidation(t, url, maxRetries, timeBetweenRetries, func(status int, body string) bool { - return status == 200 && strings.Contains(body, "Hello, World") - }) - }) +func teardownHelloApp(t *testing.T, helloAppDir string) { + helloOpts := test_structure.LoadTerraformOptions(t, helloAppDir) + defer terraform.Destroy(t, helloOpts) +} - test_structure.RunTestStage(t, "redeploy_hello_world_app", func() { - helloOpts := test_structure.LoadTerraformOptions(t, helloWorldAppDir) +func deployHelloApp(t *testing.T, dbDir string, helloAppDir string) { + uniqueId := test_structure.LoadString(t, dbDir, "uniqueId") + helloOpts := createHelloOpts(helloAppDir, uniqueId) - albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") - url := fmt.Sprintf("http://%s", albDnsName) + // Save data to disk so that other test stages executed at a later + // time can read the data back in + test_structure.SaveTerraformOptions(t, helloAppDir, helloOpts) - // Start checking every 1s that the app is responding with a 200 OK - stopChecking := make(chan bool, 1) - waitGroup, _ := http_helper.ContinuouslyCheckUrl(t, url, stopChecking, 1*time.Second) + terraform.InitAndApply(t, helloOpts) +} - // Update the server text and redeploy - newServerText := "Hello, World, v2!" - helloOpts.Vars["server_text"] = newServerText - terraform.Apply(t, helloOpts) +func validateHelloApp(t *testing.T, helloAppDir string) { + helloOpts := test_structure.LoadTerraformOptions(t, helloAppDir) + validateApp(t, helloOpts) +} - // Make sure the new version deployed - maxRetries := 10 - timeBetweenRetries := 10 * time.Second - http_helper.HttpGetWithRetryWithCustomValidation(t, url, maxRetries, timeBetweenRetries, func(status int, body string) bool { +func redeployHelloApp(t *testing.T, helloAppDir string) { + helloOpts := test_structure.LoadTerraformOptions(t, helloAppDir) + + albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name") + url := fmt.Sprintf("http://%s", albDnsName) + + // Start checking every 1s that the app is responding with a 200 OK + stopChecking := make(chan bool, 1) + waitGroup, _ := http_helper.ContinuouslyCheckUrl( + t, + url, + stopChecking, + 1*time.Second, + ) + + // Update the server text and redeploy + newServerText := "Hello, World, v2!" + helloOpts.Vars["server_text"] = newServerText + terraform.Apply(t, helloOpts) + + // Make sure the new version deployed + maxRetries := 10 + timeBetweenRetries := 10 * time.Second + http_helper.HttpGetWithRetryWithCustomValidation( + t, + url, + maxRetries, + timeBetweenRetries, + func(status int, body string) bool { return status == 200 && strings.Contains(body, newServerText) - }) + }, + ) - // Stop checking - stopChecking <- true - waitGroup.Wait() - }) + // Stop checking + stopChecking <- true + waitGroup.Wait() } diff --git a/code/terraform/08-terraform-team/test/mysql_example_test.go b/code/terraform/08-terraform-team/test/mysql_example_test.go index 418434f5..f3799c73 100644 --- a/code/terraform/08-terraform-team/test/mysql_example_test.go +++ b/code/terraform/08-terraform-team/test/mysql_example_test.go @@ -11,7 +11,7 @@ func TestMySqlExample(t *testing.T) { t.Parallel() terraformOptions := &terraform.Options{ - // You should update this relative path to point at your alb + // You should update this relative path to point at your mysql // example directory! TerraformDir: "../examples/mysql", Vars: map[string]interface{}{