From 5192d81bba57c3f73d5eaae422b03e41ae9222fe Mon Sep 17 00:00:00 2001 From: Wilfred Date: Tue, 14 Apr 2020 11:32:42 +0200 Subject: [PATCH 1/4] adds scaling properties to appengine standard version --- products/appengine/api.yaml | 99 ++++++++++++++++++- .../app_engine_standard_app_version.tf.erb | 11 +++ 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/products/appengine/api.yaml b/products/appengine/api.yaml index 58ab43d0f5db..45497773f832 100644 --- a/products/appengine/api.yaml +++ b/products/appengine/api.yaml @@ -215,8 +215,9 @@ objects: name: 'StandardAppVersion' description: | Standard App Version resource to create a new version of standard GAE Application. + Learn about the differences between the standard environment and the flexible environment + at https://cloud.google.com/appengine/docs/the-appengine-environments. Currently supporting Zip and File Containers. - Currently does not support async operation checking. collection_url_key: 'versions' base_url: 'apps/{{project}}/services/{{service}}/versions' delete_url: 'apps/{{project}}/services/{{service}}/versions/{{version_id}}' @@ -401,8 +402,8 @@ objects: name: 'deployment' description: | Code and application artifacts that make up this version. - required: false - properties: + required: true + properties: - !ruby/object:Api::Type::NestedObject name: 'zip' description: 'Zip File' @@ -457,8 +458,96 @@ objects: name: 'instanceClass' description: | Instance class that is used to run this version. Valid values are - AutomaticScaling F1, F2, F4, F4_1G - (Only AutomaticScaling is supported at the moment) + AutomaticScaling: F1, F2, F4, F4_1G + BasicScaling or ManualScaling: B1, B2, B4, B4_1G, B8 + Defaults to F1 for AutomaticScaling and B2 for ManualScaling and BasicScaling. If no scaling is specified, AutomaticScaling is chosen. + - !ruby/object:Api::Type::NestedObject + name: 'automaticScaling' + description: | + Automatic scaling is based on request rate, response latencies, and other application metrics. + exactly_one_of: + - automatic_scaling + - basic_scaling + - manual_scaling + properties: + - !ruby/object:Api::Type::Integer + name: 'maxConcurrentRequests' + description: | + Number of concurrent requests an automatic scaling instance can accept before the scheduler spawns a new instance. + + Defaults to a runtime-specific value. + - !ruby/object:Api::Type::Integer + name: 'maxIdleInstances' + description: | + Maximum number of idle instances that should be maintained for this version. + - !ruby/object:Api::Type::String + name: 'maxPendingLatency' + description: | + Maximum amount of time that a request should wait in the pending queue before starting a new instance to handle it. + - !ruby/object:Api::Type::Integer + name: 'minIdleInstances' + description: | + Minimum number of idle instances that should be maintained for this version. Only applicable for the default version of a service. + - !ruby/object:Api::Type::String + name: 'minPendingLatency' + description: | + Minimum amount of time a request should wait in the pending queue before starting a new instance to handle it. + - !ruby/object:Api::Type::NestedObject + name: 'standardSchedulerSettings' + description: | + Scheduler settings for standard environment. + properties: + - !ruby/object:Api::Type::Double + name: 'targetCpuUtilization' + description: | + Target CPU utilization ratio to maintain when scaling. Should be a value in the range [0.50, 0.95], zero, or a negative value. + - !ruby/object:Api::Type::Double + name: 'targetThroughputUtilization' + description: | + Target throughput utilization ratio to maintain when scaling. Should be a value in the range [0.50, 0.95], zero, or a negative value. + - !ruby/object:Api::Type::Integer + name: 'minInstances' + description: | + Minimum number of instances to run for this version. Set to zero to disable minInstances configuration. + - !ruby/object:Api::Type::Integer + name: 'maxInstances' + description: | + Maximum number of instances to run for this version. Set to zero to disable maxInstances configuration. + - !ruby/object:Api::Type::NestedObject + name: 'basicScaling' + description: | + Basic scaling creates instances when your application receives requests. Each instance will be shut down when the application becomes idle. Basic scaling is ideal for work that is intermittent or driven by user activity. + exactly_one_of: + - automatic_scaling + - basic_scaling + - manual_scaling + properties: + - !ruby/object:Api::Type::String + name: 'idleTimeout' + default_value: 900s + description: | + Duration of time after the last request that an instance must wait before the instance is shut down. + A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". Defaults to 900s. + - !ruby/object:Api::Type::Integer + name: 'maxInstances' + required: true + description: | + Maximum number of instances to create for this version. Must be in the range [1.0, 200.0]. + - !ruby/object:Api::Type::NestedObject + name: 'manualScaling' + description: | + A service with manual scaling runs continuously, allowing you to perform complex initialization and rely on the state of its memory over time. + exactly_one_of: + - automatic_scaling + - basic_scaling + - manual_scaling + properties: + - !ruby/object:Api::Type::Integer + name: 'instances' + required: true + description: | + Number of instances to assign to the service at the start. This number can later be altered by using the Modules API set_num_instances() function. + # StandardAppVersion and FlexibleAppVersion use the same API endpoint (apps.services.versions) # They are split apart as some of the fields will are necessary for one and not the other, and # other fields may have different defaults. However, some fields are the same. If fixing a bug diff --git a/templates/terraform/examples/app_engine_standard_app_version.tf.erb b/templates/terraform/examples/app_engine_standard_app_version.tf.erb index cdf15ef6acbb..b670e0fc81d6 100644 --- a/templates/terraform/examples/app_engine_standard_app_version.tf.erb +++ b/templates/terraform/examples/app_engine_standard_app_version.tf.erb @@ -17,6 +17,13 @@ resource "google_app_engine_standard_app_version" "<%= ctx[:primary_resource_id] port = "8080" } + automatic_scaling { + max_concurrent_requests = 10 + standard_scheduler_settings { + target_cpu_utilization = 0.5 + } + } + delete_service_on_destroy = true } @@ -39,6 +46,10 @@ resource "google_app_engine_standard_app_version" "myapp_v2" { port = "8080" } + basic_scaling { + max_instances = 5 + } + noop_on_destroy = true } From ea22d22aea4c3160e922a8c3385e034059999203 Mon Sep 17 00:00:00 2001 From: Wilfred Date: Mon, 20 Apr 2020 23:53:25 +0200 Subject: [PATCH 2/4] use optional scaling properties as it defaults to automaticScaling --- products/appengine/api.yaml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/products/appengine/api.yaml b/products/appengine/api.yaml index 45497773f832..1892be267bc3 100644 --- a/products/appengine/api.yaml +++ b/products/appengine/api.yaml @@ -465,10 +465,9 @@ objects: name: 'automaticScaling' description: | Automatic scaling is based on request rate, response latencies, and other application metrics. - exactly_one_of: - - automatic_scaling - - basic_scaling - - manual_scaling + conflicts: + - basicScaling + - manualScaling properties: - !ruby/object:Api::Type::Integer name: 'maxConcurrentRequests' @@ -517,10 +516,9 @@ objects: name: 'basicScaling' description: | Basic scaling creates instances when your application receives requests. Each instance will be shut down when the application becomes idle. Basic scaling is ideal for work that is intermittent or driven by user activity. - exactly_one_of: - - automatic_scaling - - basic_scaling - - manual_scaling + conflicts: + - automaticScaling + - manualScaling properties: - !ruby/object:Api::Type::String name: 'idleTimeout' @@ -537,10 +535,9 @@ objects: name: 'manualScaling' description: | A service with manual scaling runs continuously, allowing you to perform complex initialization and rely on the state of its memory over time. - exactly_one_of: - - automatic_scaling - - basic_scaling - - manual_scaling + conflicts: + - automaticScaling + - basicScaling properties: - !ruby/object:Api::Type::Integer name: 'instances' From 7a7c139c25f0711684139714e9549217325b3283 Mon Sep 17 00:00:00 2001 From: Wilfred Date: Wed, 22 Apr 2020 07:28:24 +0200 Subject: [PATCH 3/4] cover all automatic_scaling properties during test --- products/appengine/api.yaml | 12 ++++++++++-- .../examples/app_engine_standard_app_version.tf.erb | 7 +++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/products/appengine/api.yaml b/products/appengine/api.yaml index 1892be267bc3..11fbbfb30923 100644 --- a/products/appengine/api.yaml +++ b/products/appengine/api.yaml @@ -483,6 +483,7 @@ objects: name: 'maxPendingLatency' description: | Maximum amount of time that a request should wait in the pending queue before starting a new instance to handle it. + A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". - !ruby/object:Api::Type::Integer name: 'minIdleInstances' description: | @@ -491,6 +492,7 @@ objects: name: 'minPendingLatency' description: | Minimum amount of time a request should wait in the pending queue before starting a new instance to handle it. + A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". - !ruby/object:Api::Type::NestedObject name: 'standardSchedulerSettings' description: | @@ -543,7 +545,10 @@ objects: name: 'instances' required: true description: | - Number of instances to assign to the service at the start. This number can later be altered by using the Modules API set_num_instances() function. + Number of instances to assign to the service at the start. + + **Note:** When managing the number of instances at runtime through the App Engine Admin API or the (now deprecated) Python 2 + Modules API set_num_instances() you must use `lifecycle.ignore_changes = ["manual_scaling"[0].instances]` to prevent drift detection. # StandardAppVersion and FlexibleAppVersion use the same API endpoint (apps.services.versions) # They are split apart as some of the fields will are necessary for one and not the other, and @@ -1186,7 +1191,10 @@ objects: name: 'instances' required: true description: | - Number of instances to assign to the service at the start. This number can later be altered by using the Modules API set_num_instances() function. + Number of instances to assign to the service at the start. + + **Note:** When managing the number of instances at runtime through the App Engine Admin API or the (now deprecated) Python 2 + Modules API set_num_instances() you must use `lifecycle.ignore_changes = ["manual_scaling"[0].instances]` to prevent drift detection. - !ruby/object:Api::Resource name: 'ApplicationUrlDispatchRules' description: | diff --git a/templates/terraform/examples/app_engine_standard_app_version.tf.erb b/templates/terraform/examples/app_engine_standard_app_version.tf.erb index b670e0fc81d6..9af3a5cceaf3 100644 --- a/templates/terraform/examples/app_engine_standard_app_version.tf.erb +++ b/templates/terraform/examples/app_engine_standard_app_version.tf.erb @@ -19,8 +19,15 @@ resource "google_app_engine_standard_app_version" "<%= ctx[:primary_resource_id] automatic_scaling { max_concurrent_requests = 10 + min_idle_instances = 1 + max_idle_instances = 3 + min_pending_latency = "1s" + max_pending_latency = "5s" standard_scheduler_settings { target_cpu_utilization = 0.5 + target_throughput_utilization = 0.75 + min_instances = 2 + max_instances = 10 } } From 8bc74f4a3fc18b3d2c3716d76da8df6bf5c97af1 Mon Sep 17 00:00:00 2001 From: Wilfred Date: Wed, 22 Apr 2020 07:59:15 +0200 Subject: [PATCH 4/4] update test for standard appengine version --- products/appengine/terraform.yaml | 3 +- ...ce_app_engine_standard_app_version_test.go | 200 ++++++++++++++++++ 2 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 third_party/terraform/tests/resource_app_engine_standard_app_version_test.go diff --git a/products/appengine/terraform.yaml b/products/appengine/terraform.yaml index 0dc263084de7..bc46666acdca 100644 --- a/products/appengine/terraform.yaml +++ b/products/appengine/terraform.yaml @@ -57,8 +57,9 @@ overrides: !ruby/object:Overrides::ResourceOverrides ignore_read: true threadsafe: !ruby/object:Overrides::Terraform::PropertyOverride ignore_read: true + # instanceClass defaults to a value based on the scaling method instanceClass: !ruby/object:Overrides::Terraform::PropertyOverride - ignore_read: true + default_from_api: true examples: - !ruby/object:Provider::Terraform::Examples name: "app_engine_standard_app_version" diff --git a/third_party/terraform/tests/resource_app_engine_standard_app_version_test.go b/third_party/terraform/tests/resource_app_engine_standard_app_version_test.go new file mode 100644 index 000000000000..fadb4523ce87 --- /dev/null +++ b/third_party/terraform/tests/resource_app_engine_standard_app_version_test.go @@ -0,0 +1,200 @@ +package google + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "testing" +) + +func TestAccAppEngineStandardAppVersion_update(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "org_id": getTestOrgFromEnv(t), + "billing_account": getTestBillingAccountFromEnv(t), + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAppEngineStandardAppVersionDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccAppEngineStandardAppVersion_python(context), + }, + { + ResourceName: "google_app_engine_standard_app_version.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"env_variables", "deployment", "entrypoint", "service", "noop_on_destroy"}, + }, + { + Config: testAccAppEngineStandardAppVersion_pythonUpdate(context), + }, + { + ResourceName: "google_app_engine_standard_app_version.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"env_variables", "deployment", "entrypoint", "service", "noop_on_destroy"}, + }, + }, + }) +} + +func testAccAppEngineStandardAppVersion_python(context map[string]interface{}) string { + return Nprintf(` +resource "google_project" "my_project" { + name = "tf-test-appeng-std%{random_suffix}" + project_id = "tf-test-appeng-std%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" +} + +resource "google_app_engine_application" "app" { + project = google_project.my_project.project_id + location_id = "us-central" +} + +resource "google_project_service" "project" { + project = google_project.my_project.project_id + service = "appengine.googleapis.com" + + disable_dependent_services = false +} + +resource "google_app_engine_standard_app_version" "foo" { + project = google_project_service.project.project + version_id = "v1" + service = "default" + runtime = "python38" + + entrypoint { + shell = "gunicorn -b :$PORT main:app" + } + + deployment { + files { + name = "main.py" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.main.name}" + } + + files { + name = "requirements.txt" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.requirements.name}" + } + } + + env_variables = { + port = "8000" + } + + instance_class = "F2" + + automatic_scaling { + max_concurrent_requests = 10 + min_idle_instances = 1 + max_idle_instances = 3 + min_pending_latency = "1s" + max_pending_latency = "5s" + standard_scheduler_settings { + target_cpu_utilization = 0.5 + target_throughput_utilization = 0.75 + min_instances = 2 + max_instances = 10 + } + } + + noop_on_destroy = true +} + +resource "google_storage_bucket" "bucket" { + project = google_project.my_project.project_id + name = "tf-test-%{random_suffix}-standard-ae-bucket" +} + +resource "google_storage_bucket_object" "requirements" { + name = "requirements.txt" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/appengine/hello-world-flask/requirements.txt" +} + +resource "google_storage_bucket_object" "main" { + name = "main.py" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/appengine/hello-world-flask/main.py" +}`, context) +} + +func testAccAppEngineStandardAppVersion_pythonUpdate(context map[string]interface{}) string { + return Nprintf(` +resource "google_project" "my_project" { + name = "tf-test-appeng-std%{random_suffix}" + project_id = "tf-test-appeng-std%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" +} + +resource "google_app_engine_application" "app" { + project = google_project.my_project.project_id + location_id = "us-central" +} + +resource "google_project_service" "project" { + project = google_project.my_project.project_id + service = "appengine.googleapis.com" + + disable_dependent_services = false +} + +resource "google_app_engine_standard_app_version" "foo" { + project = google_project_service.project.project + version_id = "v1" + service = "default" + runtime = "python38" + + entrypoint { + shell = "gunicorn -b :$PORT main:app" + } + + deployment { + files { + name = "main.py" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.main.name}" + } + + files { + name = "requirements.txt" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.requirements.name}" + } + } + + env_variables = { + port = "8000" + } + + instance_class = "B2" + + basic_scaling { + max_instances = 5 + } + + noop_on_destroy = true +} + +resource "google_storage_bucket" "bucket" { + project = google_project.my_project.project_id + name = "tf-test-%{random_suffix}-standard-ae-bucket" +} + +resource "google_storage_bucket_object" "requirements" { + name = "requirements.txt" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/appengine/hello-world-flask/requirements.txt" +} + +resource "google_storage_bucket_object" "main" { + name = "main.py" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/appengine/hello-world-flask/main.py" +}`, context) +}