From 87ff09be76d97ada08fa3483055a4b96d37ea8a3 Mon Sep 17 00:00:00 2001 From: Carl Johnston Date: Wed, 1 Nov 2023 18:57:17 +1100 Subject: [PATCH] feat: Add support for relational database datasources (#58) --- README.md | 14 ++++++-- examples/complete/main.tf | 11 ++++-- iam.tf | 20 ++++++++++- main.tf | 18 +++++++++- variables.tf | 12 +++++++ wrappers/main.tf | 74 ++++++++++++++++++++------------------- 6 files changed, 107 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 73faf7a..d35c303 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,16 @@ module "appsync" { } eventbridge1 = { - type = "AMAZON_EVENTBRIDGE" - event_bus_arn = "aws:arn:events:us-west-1:135367859850:event-bus/eventbridge1" + type = "AMAZON_EVENTBRIDGE" + event_bus_arn = "arn:aws:events:us-west-1:135367859850:event-bus/eventbridge1" + } + + rds1 = { + type = "RELATIONAL_DATABASE" + cluster_arn = "arn:aws:rds:us-west-1:135367859850:cluster:rds1" + secret_arn = "arn:aws:secretsmanager:us-west-1:135367859850:secret:rds-secret1" + database_name = "mydb" + schema = "myschema" } } @@ -198,9 +206,11 @@ No modules. | [name](#input\_name) | Name of GraphQL API | `string` | `""` | no | | [openid\_connect\_config](#input\_openid\_connect\_config) | Nested argument containing OpenID Connect configuration. | `map(string)` | `{}` | no | | [opensearchservice\_allowed\_actions](#input\_opensearchservice\_allowed\_actions) | List of allowed IAM actions for datasources type AMAZON\_OPENSEARCH\_SERVICE | `list(string)` |
[
"es:ESHttpDelete",
"es:ESHttpHead",
"es:ESHttpGet",
"es:ESHttpPost",
"es:ESHttpPut"
]
| no | +| [relational\_database\_allowed\_actions](#input\_relational\_database\_allowed\_actions) | List of allowed IAM actions for datasources type RELATIONAL\_DATABASE | `list(string)` |
[
"rds-data:BatchExecuteStatement",
"rds-data:BeginTransaction",
"rds-data:CommitTransaction",
"rds-data:ExecuteStatement",
"rds-data:RollbackTransaction"
]
| no | | [resolver\_caching\_ttl](#input\_resolver\_caching\_ttl) | Default caching TTL for resolvers when caching is enabled | `number` | `60` | no | | [resolvers](#input\_resolvers) | Map of resolvers to create | `any` | `{}` | no | | [schema](#input\_schema) | The schema definition, in GraphQL schema language format. Terraform cannot perform drift detection of this configuration. | `string` | `""` | no | +| [secrets\_manager\_allowed\_actions](#input\_secrets\_manager\_allowed\_actions) | List of allowed IAM actions for secrets manager datasources type RELATIONAL\_DATABASE | `list(string)` |
[
"secretsmanager:GetSecretValue"
]
| no | | [tags](#input\_tags) | Map of tags to add to all GraphQL resources created by this module | `map(string)` | `{}` | no | | [user\_pool\_config](#input\_user\_pool\_config) | The Amazon Cognito User Pool configuration. | `map(string)` | `{}` | no | | [visibility](#input\_visibility) | The API visibility. Valid values: GLOBAL, PRIVATE. | `string` | `null` | no | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 06c486b..8732051 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -224,9 +224,16 @@ module "appsync" { } eventbridge1 = { - type = "AMAZON_EVENTBRIDGE" + type = "AMAZON_EVENTBRIDGE" + event_bus_arn = "arn:aws:events:us-west-1:135367859850:event-bus/eventbridge1" + } - event_bus_arn = "aws:arn:events:us-west-1:135367859850:event-bus/eventbridge1" + rds1 = { + type = "RELATIONAL_DATABASE" + cluster_arn = "arn:aws:rds:us-west-1:135367859850:cluster:rds1" + secret_arn = "arn:aws:secretsmanager:us-west-1:135367859850:secret:rds-secret1" + database_name = "mydb" + schema = "myschema" } } diff --git a/iam.tf b/iam.tf index 57b1dd4..f53bd86 100644 --- a/iam.tf +++ b/iam.tf @@ -1,7 +1,7 @@ data "aws_partition" "this" {} locals { - service_roles_with_policies = var.create_graphql_api ? { for k, v in var.datasources : k => v if contains(["AWS_LAMBDA", "AMAZON_DYNAMODB", "AMAZON_ELASTICSEARCH", "AMAZON_OPENSEARCH_SERVICE", "AMAZON_EVENTBRIDGE"], v.type) && tobool(lookup(v, "create_service_role", true)) } : {} + service_roles_with_policies = var.create_graphql_api ? { for k, v in var.datasources : k => v if contains(["AWS_LAMBDA", "AMAZON_DYNAMODB", "AMAZON_ELASTICSEARCH", "AMAZON_OPENSEARCH_SERVICE", "AMAZON_EVENTBRIDGE", "RELATIONAL_DATABASE"], v.type) && tobool(lookup(v, "create_service_role", true)) } : {} service_roles_with_policies_lambda = { for k, v in local.service_roles_with_policies : k => merge(v, { @@ -63,12 +63,30 @@ locals { } ) if v.type == "AMAZON_EVENTBRIDGE" } + service_roles_with_policies_relational_database = { for k, v in local.service_roles_with_policies : k => merge(v, + { + policy_statements = { + relational_database = { + effect = "Allow" + actions = lookup(v, "policy_actions", null) == null ? var.relational_database_allowed_actions : v.policy_actions + resources = [v.cluster_arn] + } + secrets_manager = { + effect = "Allow" + actions = lookup(v, "policy_actions", null) == null ? var.secrets_manager_allowed_actions : v.policy_actions + resources = [v.secret_arn] + } + } + } + ) if v.type == "RELATIONAL_DATABASE" } + service_roles_with_specific_policies = merge( local.service_roles_with_policies_lambda, local.service_roles_with_policies_dynamodb, local.service_roles_with_policies_elasticsearch, local.service_roles_with_policies_opensearchservice, local.service_roles_with_policies_eventbridge, + local.service_roles_with_policies_relational_database, ) } diff --git a/main.tf b/main.tf index b837012..6f46acb 100644 --- a/main.tf +++ b/main.tf @@ -144,7 +144,7 @@ resource "aws_appsync_datasource" "this" { name = each.key type = each.value.type description = lookup(each.value, "description", null) - service_role_arn = lookup(each.value, "service_role_arn", tobool(lookup(each.value, "create_service_role", contains(["AWS_LAMBDA", "AMAZON_DYNAMODB", "AMAZON_ELASTICSEARCH", "AMAZON_OPENSEARCH_SERVICE", "AMAZON_EVENTBRIDGE"], each.value.type))) ? aws_iam_role.service_role[each.key].arn : null) + service_role_arn = lookup(each.value, "service_role_arn", tobool(lookup(each.value, "create_service_role", contains(["AWS_LAMBDA", "AMAZON_DYNAMODB", "AMAZON_ELASTICSEARCH", "AMAZON_OPENSEARCH_SERVICE", "AMAZON_EVENTBRIDGE", "RELATIONAL_DATABASE"], each.value.type))) ? aws_iam_role.service_role[each.key].arn : null) dynamic "http_config" { for_each = each.value.type == "HTTP" ? [true] : [] @@ -197,6 +197,22 @@ resource "aws_appsync_datasource" "this" { event_bus_arn = each.value.event_bus_arn } } + + dynamic "relational_database_config" { + for_each = each.value.type == "RELATIONAL_DATABASE" ? [true] : [] + + content { + source_type = lookup(each.value, "source_type", "RDS_HTTP_ENDPOINT") + + http_endpoint_config { + db_cluster_identifier = each.value.cluster_arn + aws_secret_store_arn = each.value.secret_arn + database_name = lookup(each.value, "database_name", null) + region = split(":", each.value.cluster_arn)[3] + schema = lookup(each.value, "schema", null) + } + } + } } # Resolvers diff --git a/variables.tf b/variables.tf index bc65c94..4bc8ef4 100644 --- a/variables.tf +++ b/variables.tf @@ -242,6 +242,18 @@ variable "eventbridge_allowed_actions" { default = ["events:PutEvents"] } +variable "relational_database_allowed_actions" { + description = "List of allowed IAM actions for datasources type RELATIONAL_DATABASE" + type = list(string) + default = ["rds-data:BatchExecuteStatement", "rds-data:BeginTransaction", "rds-data:CommitTransaction", "rds-data:ExecuteStatement", "rds-data:RollbackTransaction"] +} + +variable "secrets_manager_allowed_actions" { + description = "List of allowed IAM actions for secrets manager datasources type RELATIONAL_DATABASE" + type = list(string) + default = ["secretsmanager:GetSecretValue"] +} + variable "iam_permissions_boundary" { description = "ARN for iam permissions boundary" type = string diff --git a/wrappers/main.tf b/wrappers/main.tf index 2208e41..3b126d8 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -3,42 +3,44 @@ module "wrapper" { for_each = var.items - create_graphql_api = try(each.value.create_graphql_api, var.defaults.create_graphql_api, true) - logging_enabled = try(each.value.logging_enabled, var.defaults.logging_enabled, false) - domain_name_association_enabled = try(each.value.domain_name_association_enabled, var.defaults.domain_name_association_enabled, false) - caching_enabled = try(each.value.caching_enabled, var.defaults.caching_enabled, false) - xray_enabled = try(each.value.xray_enabled, var.defaults.xray_enabled, false) - name = try(each.value.name, var.defaults.name, "") - schema = try(each.value.schema, var.defaults.schema, "") - visibility = try(each.value.visibility, var.defaults.visibility, null) - authentication_type = try(each.value.authentication_type, var.defaults.authentication_type, "API_KEY") - create_logs_role = try(each.value.create_logs_role, var.defaults.create_logs_role, true) - logs_role_name = try(each.value.logs_role_name, var.defaults.logs_role_name, null) - log_cloudwatch_logs_role_arn = try(each.value.log_cloudwatch_logs_role_arn, var.defaults.log_cloudwatch_logs_role_arn, null) - log_field_log_level = try(each.value.log_field_log_level, var.defaults.log_field_log_level, null) - log_exclude_verbose_content = try(each.value.log_exclude_verbose_content, var.defaults.log_exclude_verbose_content, false) - lambda_authorizer_config = try(each.value.lambda_authorizer_config, var.defaults.lambda_authorizer_config, {}) - openid_connect_config = try(each.value.openid_connect_config, var.defaults.openid_connect_config, {}) - user_pool_config = try(each.value.user_pool_config, var.defaults.user_pool_config, {}) - additional_authentication_provider = try(each.value.additional_authentication_provider, var.defaults.additional_authentication_provider, {}) - graphql_api_tags = try(each.value.graphql_api_tags, var.defaults.graphql_api_tags, {}) - logs_role_tags = try(each.value.logs_role_tags, var.defaults.logs_role_tags, {}) - tags = try(each.value.tags, var.defaults.tags, {}) - domain_name = try(each.value.domain_name, var.defaults.domain_name, "") - domain_name_description = try(each.value.domain_name_description, var.defaults.domain_name_description, null) - certificate_arn = try(each.value.certificate_arn, var.defaults.certificate_arn, "") - caching_behavior = try(each.value.caching_behavior, var.defaults.caching_behavior, "FULL_REQUEST_CACHING") - cache_type = try(each.value.cache_type, var.defaults.cache_type, "SMALL") - cache_ttl = try(each.value.cache_ttl, var.defaults.cache_ttl, 1) - cache_at_rest_encryption_enabled = try(each.value.cache_at_rest_encryption_enabled, var.defaults.cache_at_rest_encryption_enabled, false) - cache_transit_encryption_enabled = try(each.value.cache_transit_encryption_enabled, var.defaults.cache_transit_encryption_enabled, false) - api_keys = try(each.value.api_keys, var.defaults.api_keys, {}) - lambda_allowed_actions = try(each.value.lambda_allowed_actions, var.defaults.lambda_allowed_actions, ["lambda:invokeFunction"]) - dynamodb_allowed_actions = try(each.value.dynamodb_allowed_actions, var.defaults.dynamodb_allowed_actions, ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:UpdateItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem"]) - elasticsearch_allowed_actions = try(each.value.elasticsearch_allowed_actions, var.defaults.elasticsearch_allowed_actions, ["es:ESHttpDelete", "es:ESHttpHead", "es:ESHttpGet", "es:ESHttpPost", "es:ESHttpPut"]) - opensearchservice_allowed_actions = try(each.value.opensearchservice_allowed_actions, var.defaults.opensearchservice_allowed_actions, ["es:ESHttpDelete", "es:ESHttpHead", "es:ESHttpGet", "es:ESHttpPost", "es:ESHttpPut"]) - eventbridge_allowed_actions = try(each.value.eventbridge_allowed_actions, var.defaults.eventbridge_allowed_actions, ["events:PutEvents"]) - iam_permissions_boundary = try(each.value.iam_permissions_boundary, var.defaults.iam_permissions_boundary, null) + create_graphql_api = try(each.value.create_graphql_api, var.defaults.create_graphql_api, true) + logging_enabled = try(each.value.logging_enabled, var.defaults.logging_enabled, false) + domain_name_association_enabled = try(each.value.domain_name_association_enabled, var.defaults.domain_name_association_enabled, false) + caching_enabled = try(each.value.caching_enabled, var.defaults.caching_enabled, false) + xray_enabled = try(each.value.xray_enabled, var.defaults.xray_enabled, false) + name = try(each.value.name, var.defaults.name, "") + schema = try(each.value.schema, var.defaults.schema, "") + visibility = try(each.value.visibility, var.defaults.visibility, null) + authentication_type = try(each.value.authentication_type, var.defaults.authentication_type, "API_KEY") + create_logs_role = try(each.value.create_logs_role, var.defaults.create_logs_role, true) + logs_role_name = try(each.value.logs_role_name, var.defaults.logs_role_name, null) + log_cloudwatch_logs_role_arn = try(each.value.log_cloudwatch_logs_role_arn, var.defaults.log_cloudwatch_logs_role_arn, null) + log_field_log_level = try(each.value.log_field_log_level, var.defaults.log_field_log_level, null) + log_exclude_verbose_content = try(each.value.log_exclude_verbose_content, var.defaults.log_exclude_verbose_content, false) + lambda_authorizer_config = try(each.value.lambda_authorizer_config, var.defaults.lambda_authorizer_config, {}) + openid_connect_config = try(each.value.openid_connect_config, var.defaults.openid_connect_config, {}) + user_pool_config = try(each.value.user_pool_config, var.defaults.user_pool_config, {}) + additional_authentication_provider = try(each.value.additional_authentication_provider, var.defaults.additional_authentication_provider, {}) + graphql_api_tags = try(each.value.graphql_api_tags, var.defaults.graphql_api_tags, {}) + logs_role_tags = try(each.value.logs_role_tags, var.defaults.logs_role_tags, {}) + tags = try(each.value.tags, var.defaults.tags, {}) + domain_name = try(each.value.domain_name, var.defaults.domain_name, "") + domain_name_description = try(each.value.domain_name_description, var.defaults.domain_name_description, null) + certificate_arn = try(each.value.certificate_arn, var.defaults.certificate_arn, "") + caching_behavior = try(each.value.caching_behavior, var.defaults.caching_behavior, "FULL_REQUEST_CACHING") + cache_type = try(each.value.cache_type, var.defaults.cache_type, "SMALL") + cache_ttl = try(each.value.cache_ttl, var.defaults.cache_ttl, 1) + cache_at_rest_encryption_enabled = try(each.value.cache_at_rest_encryption_enabled, var.defaults.cache_at_rest_encryption_enabled, false) + cache_transit_encryption_enabled = try(each.value.cache_transit_encryption_enabled, var.defaults.cache_transit_encryption_enabled, false) + api_keys = try(each.value.api_keys, var.defaults.api_keys, {}) + lambda_allowed_actions = try(each.value.lambda_allowed_actions, var.defaults.lambda_allowed_actions, ["lambda:invokeFunction"]) + dynamodb_allowed_actions = try(each.value.dynamodb_allowed_actions, var.defaults.dynamodb_allowed_actions, ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:UpdateItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem"]) + elasticsearch_allowed_actions = try(each.value.elasticsearch_allowed_actions, var.defaults.elasticsearch_allowed_actions, ["es:ESHttpDelete", "es:ESHttpHead", "es:ESHttpGet", "es:ESHttpPost", "es:ESHttpPut"]) + opensearchservice_allowed_actions = try(each.value.opensearchservice_allowed_actions, var.defaults.opensearchservice_allowed_actions, ["es:ESHttpDelete", "es:ESHttpHead", "es:ESHttpGet", "es:ESHttpPost", "es:ESHttpPut"]) + eventbridge_allowed_actions = try(each.value.eventbridge_allowed_actions, var.defaults.eventbridge_allowed_actions, ["events:PutEvents"]) + relational_database_allowed_actions = try(each.value.relational_database_allowed_actions, var.defaults.relational_database_allowed_actions, ["rds-data:BatchExecuteStatement", "rds-data:BeginTransaction", "rds-data:CommitTransaction", "rds-data:ExecuteStatement", "rds-data:RollbackTransaction"]) + secrets_manager_allowed_actions = try(each.value.secrets_manager_allowed_actions, var.defaults.secrets_manager_allowed_actions, ["secretsmanager:GetSecretValue"]) + iam_permissions_boundary = try(each.value.iam_permissions_boundary, var.defaults.iam_permissions_boundary, null) direct_lambda_request_template = try(each.value.direct_lambda_request_template, var.defaults.direct_lambda_request_template, <<-EOF { "version" : "2017-02-28",