diff --git a/modules/__experimental/cloud-function-scheduled/README.md b/modules/__experimental/cloud-function-scheduled/README.md
new file mode 100644
index 0000000000..beeb88cf70
--- /dev/null
+++ b/modules/__experimental/cloud-function-scheduled/README.md
@@ -0,0 +1,42 @@
+# Scheduled Google Cloud Function Module
+
+This module manages a background Cloud Function scheduled via a recurring Cloud Scheduler job. It also manages the required dependencies: a service account for the cloud function with optional IAM bindings, the PubSub topic used for the function trigger, and optionally the GCS bucket used for the code bundle.
+
+## Example
+
+```hcl
+module "function" {
+ source = "./modules/cloud-function-scheduled"
+ project_id = "myproject"
+ name = "myfunction"
+ bundle_config = {
+ source_dir = "../cf"
+ output_path = "../bundle.zip"
+ }
+}
+```
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---: |:---:|:---:|
+| bundle_config | Cloud function code bundle configuration, output path is a zip file. | object({...})
| ✓ | |
+| name | Name used for resources (schedule, topic, etc.). | string
| ✓ | |
+| project_id | Project id used for all resources. | string
| ✓ | |
+| *bucket_name* | Name of the bucket that will be used for the function code, leave null to create one. | string
| | null
|
+| *function_config* | Cloud function configuration. | object({...})
| | ...
|
+| *prefixes* | Optional prefixes for resource ids, null prefixes will be ignored. | object({...})
| | null
|
+| *region* | Region used for all resources. | string
| | us-central1
|
+| *schedule_config* | Cloud function scheduler job configuration, leave data null to pass the name variable. | object({...})
| | ...
|
+| *service_account_iam_roles* | IAM roles assigned to the service account at the project level. | list(string)
| | []
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| bucket_name | Bucket name. | |
+| function_name | Cloud function name. | |
+| service_account_email | Service account email. | |
+| topic_id | PubSub topic id. | |
+
diff --git a/modules/__experimental/cloud-function-scheduled/main.tf b/modules/__experimental/cloud-function-scheduled/main.tf
new file mode 100644
index 0000000000..ca197f98b1
--- /dev/null
+++ b/modules/__experimental/cloud-function-scheduled/main.tf
@@ -0,0 +1,132 @@
+/**
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+locals {
+ bucket = (
+ var.bucket_name != null
+ ? var.bucket_name
+ : google_storage_bucket.bucket[0].name
+ )
+ job_data = (
+ var.schedule_config.pubsub_data == null || var.schedule_config.pubsub_data == ""
+ ? var.name
+ : var.schedule_config.pubsub_data
+ )
+ prefixes = (
+ var.prefixes == null
+ ? {}
+ : {
+ for k, v in var.prefixes :
+ k => v != null && v != "" ? "${v}-${var.name}" : var.name
+ }
+ )
+ service_account = "serviceAccount:${google_service_account.service_account.email}"
+}
+
+###############################################################################
+# Scheduler / PubSub #
+###############################################################################
+
+resource "google_pubsub_topic" "topic" {
+ project = var.project_id
+ name = lookup(local.prefixes, "topic", var.name)
+}
+
+resource "google_cloud_scheduler_job" "job" {
+ project = var.project_id
+ region = var.region
+ name = lookup(local.prefixes, "job", var.name)
+ schedule = var.schedule_config.schedule
+ time_zone = var.schedule_config.time_zone
+
+ pubsub_target {
+ attributes = {}
+ topic_name = google_pubsub_topic.topic.id
+ data = base64encode(local.job_data)
+ }
+}
+
+###############################################################################
+# Cloud Function service account and IAM #
+###############################################################################
+
+resource "google_service_account" "service_account" {
+ project = var.project_id
+ account_id = lookup(local.prefixes, "service_account", var.name)
+ display_name = "Terraform-managed"
+}
+
+resource "google_project_iam_member" "service_account" {
+ for_each = toset(var.service_account_iam_roles)
+ project = var.project_id
+ role = each.value
+ member = local.service_account
+}
+
+###############################################################################
+# Cloud Function and GCS code bundle #
+###############################################################################
+
+resource "google_cloudfunctions_function" "function" {
+ project = var.project_id
+ region = var.region
+ name = lookup(local.prefixes, "function", var.name)
+ description = "Terraform managed."
+ runtime = var.function_config.runtime
+ available_memory_mb = var.function_config.memory
+ max_instances = var.function_config.instances
+ timeout = var.function_config.timeout
+ entry_point = var.function_config.entry_point
+ service_account_email = google_service_account.service_account.email
+
+ # source_repository {
+ # url = var.source_repository_url
+ # }
+
+ event_trigger {
+ event_type = "providers/cloud.pubsub/eventTypes/topic.publish"
+ resource = google_pubsub_topic.topic.id
+ }
+
+ source_archive_bucket = local.bucket
+ source_archive_object = google_storage_bucket_object.bundle.name
+}
+
+resource "google_storage_bucket" "bucket" {
+ count = var.bucket_name == null ? 1 : 0
+ project = var.project_id
+ name = lookup(local.prefixes, "bucket", var.name)
+ lifecycle_rule {
+ action {
+ type = "Delete"
+ }
+ condition {
+ age = "30"
+ }
+ }
+}
+
+resource "google_storage_bucket_object" "bundle" {
+ name = "bundle-${data.archive_file.bundle.output_md5}.zip"
+ bucket = local.bucket
+ source = data.archive_file.bundle.output_path
+}
+
+data "archive_file" "bundle" {
+ type = "zip"
+ source_dir = var.bundle_config.source_dir
+ output_path = var.bundle_config.output_path
+}
diff --git a/modules/__experimental/cloud-function-scheduled/outputs.tf b/modules/__experimental/cloud-function-scheduled/outputs.tf
new file mode 100644
index 0000000000..2d47bd09c4
--- /dev/null
+++ b/modules/__experimental/cloud-function-scheduled/outputs.tf
@@ -0,0 +1,35 @@
+/**
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+output "bucket_name" {
+ description = "Bucket name."
+ value = local.bucket
+}
+
+output "function_name" {
+ description = "Cloud function name."
+ value = google_cloudfunctions_function.function.name
+}
+
+output "service_account_email" {
+ description = "Service account email."
+ value = google_service_account.service_account.email
+}
+
+output "topic_id" {
+ description = "PubSub topic id."
+ value = google_pubsub_topic.topic.id
+}
diff --git a/modules/__experimental/cloud-function-scheduled/variables.tf b/modules/__experimental/cloud-function-scheduled/variables.tf
new file mode 100644
index 0000000000..6a33f37f2c
--- /dev/null
+++ b/modules/__experimental/cloud-function-scheduled/variables.tf
@@ -0,0 +1,100 @@
+/**
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "bucket_name" {
+ description = "Name of the bucket that will be used for the function code, leave null to create one."
+ type = string
+ default = null
+}
+
+variable "bundle_config" {
+ description = "Cloud function code bundle configuration, output path is a zip file."
+ type = object({
+ source_dir = string
+ output_path = string
+ })
+}
+
+variable "function_config" {
+ description = "Cloud function configuration."
+ type = object({
+ entry_point = string
+ instances = number
+ memory = number
+ runtime = string
+ timeout = number
+ })
+ default = {
+ entry_point = "main"
+ instances = 1
+ memory = 256
+ runtime = "python37"
+ timeout = 180
+ }
+}
+
+variable "name" {
+ description = "Name used for resources (schedule, topic, etc.)."
+ type = string
+}
+
+variable "prefixes" {
+ description = "Optional prefixes for resource ids, null prefixes will be ignored."
+ type = object({
+ bucket = string
+ function = string
+ job = string
+ service_account = string
+ topic = string
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Project id used for all resources."
+ type = string
+}
+
+variable "region" {
+ description = "Region used for all resources."
+ type = string
+ default = "us-central1"
+}
+
+variable "schedule_config" {
+ description = "Cloud function scheduler job configuration, leave data null to pass the name variable."
+ type = object({
+ pubsub_data = string
+ schedule = string
+ time_zone = string
+ })
+ default = {
+ schedule = "*/10 * * * *"
+ pubsub_data = null
+ time_zone = "UTC"
+ }
+}
+
+variable "service_account_iam_roles" {
+ description = "IAM roles assigned to the service account at the project level."
+ type = list(string)
+ default = []
+}
+
+# variable "source_repository_url" {
+# type = string
+# default = ""
+# }
diff --git a/modules/__sandbox/playground/outputs.tf b/modules/__experimental/cloud-function-scheduled/versions.tf
similarity index 72%
rename from modules/__sandbox/playground/outputs.tf
rename to modules/__experimental/cloud-function-scheduled/versions.tf
index 2a0d557aa2..bc4c2a9d71 100644
--- a/modules/__sandbox/playground/outputs.tf
+++ b/modules/__experimental/cloud-function-scheduled/versions.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2019 Google LLC
+ * Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,6 @@
* limitations under the License.
*/
-output "folder" {
- description = "Folder resource."
- value = google_folder.folder
-}
-
-output "folder_name" {
- description = "Folder name."
- value = google_folder.folder.name
+terraform {
+ required_version = ">= 0.12.6"
}
diff --git a/modules/__sandbox/playground/README.md b/modules/__sandbox/playground/README.md
deleted file mode 100644
index 0bafc37d5e..0000000000
--- a/modules/__sandbox/playground/README.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Playground folder module
-
-Simple module to create a playground folder, and grant all relevant permissions on the correct resources (folder, billing account, organization) to enable specific identities to have complete control under it.
-
-Special considerations
-
-- setting Shared VPC Admin roles at the folder level is buggy and not guaranteed to work, ideally those should be set at the organization level
-- if administrators should manage any project under the folder regardless of the identity that created them, the extra role `roles/owner` has to be added to the `folder_roles` variable; testing different levels of access will then require extra identities
-- users from outside the org need the extra role `roles/browser` in the `organization_roles` variable
-
-To retrofit the module after creation, just import an existing folder and apply.
-
-## Example
-
-```hcl
-module "playground-demo" {
- source = "./playground"
- administrators = ["user:user1@example.com", "group:group1@example.com"]
- billing_account = "0123ABC-0123ABC-0123ABC"
- name = "Playground test"
- organization_id = 1234567890
- parent = "folders/1234567890"
-}
-```
-
-
-## Variables
-
-| name | description | type | required | default |
-|---|---|:---: |:---:|:---:|
-| billing_account | Billing account id on which ot assign billing roles. | string
| ✓ | |
-| name | Playground folder name. | string
| ✓ | |
-| organization_id | Top-level organization id on which to apply roles, format is the numeric id. | number
| ✓ | |
-| parent | Parent organization or folder, in organizations/nnn or folders/nnn format. | string
| ✓ | |
-| *administrators* | List of IAM-style identities that will manage the playground. | list(string)
| | []
|
-| *billing_roles* | List of IAM roles granted to administrators on the billing account. | list(string)
| | ...
|
-| *folder_roles* | List of IAM roles granted to administrators on folder. | list(string)
| | ...
|
-| *organization_roles* | List of IAM roles granted to administrators on the organization. | list(string)
| | ...
|
-
-## Outputs
-
-| name | description | sensitive |
-|---|---|:---:|
-| folder | Folder resource. | |
-| folder_name | Folder name. | |
-
diff --git a/modules/__sandbox/playground/main.tf b/modules/__sandbox/playground/main.tf
deleted file mode 100644
index 233ec2a788..0000000000
--- a/modules/__sandbox/playground/main.tf
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-locals {
- iam_billing_pairs = {
- for pair in setproduct(var.billing_roles, var.administrators) :
- join("-", pair) => { role = pair.0, member = pair.1 }
- }
- iam_organization_pairs = {
- for pair in setproduct(var.organization_roles, var.administrators) :
- join("-", pair) => { role = pair.0, member = pair.1 }
- }
-}
-
-resource "google_folder" "folder" {
- provider = google-beta
- display_name = var.name
- parent = var.parent
-}
-
-resource "google_folder_iam_binding" "authoritative" {
- for_each = toset(var.folder_roles)
- provider = google-beta
- folder = google_folder.folder.name
- role = each.value
- members = var.administrators
-}
-
-resource "google_billing_account_iam_member" "non_authoritative" {
- for_each = local.iam_billing_pairs
- billing_account_id = var.billing_account
- role = each.value.role
- member = each.value.member
-}
-
-resource "google_organization_iam_member" "non_authoritative" {
- for_each = local.iam_organization_pairs
- org_id = var.organization_id
- role = each.value.role
- member = each.value.member
-}
diff --git a/modules/__sandbox/playground/variables.tf b/modules/__sandbox/playground/variables.tf
deleted file mode 100644
index e28f811590..0000000000
--- a/modules/__sandbox/playground/variables.tf
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-variable "administrators" {
- description = "List of IAM-style identities that will manage the playground."
- type = list(string)
- default = []
-}
-
-variable "billing_account" {
- description = "Billing account id on which ot assign billing roles."
- type = string
-}
-
-variable "name" {
- description = "Playground folder name."
- type = string
-}
-
-variable "billing_roles" {
- description = "List of IAM roles granted to administrators on the billing account."
- type = list(string)
- default = [
- "roles/billing.user"
- ]
-}
-
-variable "folder_roles" {
- description = "List of IAM roles granted to administrators on folder."
- type = list(string)
- default = [
- "roles/resourcemanager.folderAdmin",
- "roles/resourcemanager.projectCreator",
- "roles/resourcemanager.projectIamAdmin",
- "roles/compute.xpnAdmin"
- ]
-}
-
-variable "organization_id" {
- description = "Top-level organization id on which to apply roles, format is the numeric id."
- type = number
-}
-
-variable "organization_roles" {
- description = "List of IAM roles granted to administrators on the organization."
- type = list(string)
- default = [
- "roles/browser",
- "roles/resourcemanager.organizationViewer"
- ]
-}
-
-variable "parent" {
- description = "Parent organization or folder, in organizations/nnn or folders/nnn format."
- type = string
-}