diff --git a/modules/aws/dynamodb-project-metadata/backplane/README.md b/modules/aws/dynamodb-project-metadata/backplane/README.md new file mode 100644 index 00000000..866de15e --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/backplane/README.md @@ -0,0 +1,42 @@ +## Overview + +The backplane provisions a central DynamoDB table in your AWS management/tooling account and an +IAM role that the meshStack building block runtime can assume via workload identity federation (OIDC). +No long-lived AWS credentials are required. + +## What is provisioned + +| Resource | Purpose | +|---|---| +| `aws_dynamodb_table` | Stores one item per meshStack project tenant (PK: workspace, SK: project) | +| `aws_iam_openid_connect_provider` | Trusts the meshStack OIDC issuer | +| `aws_iam_role` | Grants DynamoDB write access to the building block runtime | +| `aws_iam_role_policy` | Scoped to `PutItem`, `UpdateItem`, `DeleteItem` on the table only | + +## Required permissions + +The AWS principal running this backplane must be able to create IAM resources and DynamoDB tables. +Typically this is run with `AdministratorAccess` in a management/tooling account. + +## Usage + +```hcl +module "backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/aws/dynamodb-project-metadata/backplane?ref=main" + + workload_identity_federation = { + issuer = "" + audience = "" + subjects = ["system:serviceaccount::workspace..buildingblockdefinition."] + } + + table_name = "meshstack-project-metadata" # optional, defaults to this value +} +``` + +## Outputs + +| Output | Description | +|---|---| +| `table_name` | Name of the created DynamoDB table (pass to `aws_dynamodb_table_name` BBD input) | +| `workload_identity_federation_role_arn` | IAM role ARN (pass to `AWS_ROLE_ARN` BBD input) | diff --git a/modules/aws/dynamodb-project-metadata/backplane/main.tf b/modules/aws/dynamodb-project-metadata/backplane/main.tf new file mode 100644 index 00000000..acc7f68e --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/backplane/main.tf @@ -0,0 +1,80 @@ +data "aws_caller_identity" "current" {} + +resource "random_string" "suffix" { + length = 4 + special = false + upper = false +} + +resource "aws_dynamodb_table" "this" { + name = var.table_name + billing_mode = "PAY_PER_REQUEST" + hash_key = "workspace_identifier" + range_key = "project_identifier" + + attribute { + name = "workspace_identifier" + type = "S" + } + + attribute { + name = "project_identifier" + type = "S" + } +} + +resource "aws_iam_openid_connect_provider" "this" { + url = var.workload_identity_federation.issuer + client_id_list = [var.workload_identity_federation.audience] +} + +data "aws_iam_policy_document" "assume_role" { + statement { + effect = "Allow" + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.this.arn] + } + actions = ["sts:AssumeRoleWithWebIdentity"] + + condition { + test = "StringEquals" + variable = "${trimprefix(var.workload_identity_federation.issuer, "https://")}:aud" + values = [var.workload_identity_federation.audience] + } + + condition { + test = "StringLike" + variable = "${trimprefix(var.workload_identity_federation.issuer, "https://")}:sub" + values = var.workload_identity_federation.subjects + } + } +} + +locals { + role_name = "MeshStackDynamoDBWriter-${random_string.suffix.result}" +} + +resource "aws_iam_role" "this" { + name = local.role_name + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +data "aws_iam_policy_document" "dynamodb_write" { + statement { + effect = "Allow" + actions = [ + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:GetItem", + ] + resources = [aws_dynamodb_table.this.arn] + } +} + +resource "aws_iam_role_policy" "dynamodb_write" { + name = "DynamoDBWritePolicy" + role = aws_iam_role.this.id + policy = data.aws_iam_policy_document.dynamodb_write.json +} diff --git a/modules/aws/dynamodb-project-metadata/backplane/outputs.tf b/modules/aws/dynamodb-project-metadata/backplane/outputs.tf new file mode 100644 index 00000000..b9fb0694 --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/backplane/outputs.tf @@ -0,0 +1,25 @@ +output "table_name" { + description = "Name of the DynamoDB table." + value = aws_dynamodb_table.this.name +} + +output "table_arn" { + description = "ARN of the DynamoDB table." + value = aws_dynamodb_table.this.arn +} + +output "table_region" { + description = "AWS region where the DynamoDB table was created." + value = aws_dynamodb_table.this.region +} + +output "table_console_url" { + description = "AWS Console URL for the DynamoDB table." + value = "https://${aws_dynamodb_table.this.region}.console.aws.amazon.com/dynamodbv2/home?region=${aws_dynamodb_table.this.region}#table?name=${aws_dynamodb_table.this.name}" +} + +output "workload_identity_federation_role_arn" { + description = "ARN of the IAM role assumed by the building block runtime via workload identity federation." + # Manually construct ARN to avoid dependency cycle on input workload_identity_federation (which contains the BBD UUID as subject) + value = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${local.role_name}" +} diff --git a/modules/aws/dynamodb-project-metadata/backplane/variables.tf b/modules/aws/dynamodb-project-metadata/backplane/variables.tf new file mode 100644 index 00000000..e7636983 --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/backplane/variables.tf @@ -0,0 +1,17 @@ +variable "workload_identity_federation" { + type = object({ + issuer = string + audience = string + subjects = list(string) + }) + description = <<-EOT + Workload identity federation configuration. Allows the meshStack building block runtime to assume + an IAM role via OIDC without long-lived credentials. + EOT +} + +variable "table_name" { + type = string + default = "meshstack-project-metadata" + description = "Name of the DynamoDB table to create for storing meshStack project metadata." +} diff --git a/modules/aws/dynamodb-project-metadata/backplane/versions.tf b/modules/aws/dynamodb-project-metadata/backplane/versions.tf new file mode 100644 index 00000000..9c03e190 --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/backplane/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.0.0" + } + } +} diff --git a/modules/aws/dynamodb-project-metadata/buildingblock/README.md b/modules/aws/dynamodb-project-metadata/buildingblock/README.md new file mode 100644 index 00000000..d5354db6 --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/buildingblock/README.md @@ -0,0 +1,70 @@ +--- +name: AWS DynamoDB Project Metadata +supportedPlatforms: + - aws +description: Pushes meshStack project metadata (workspace, project, tags, team members) into a central AWS DynamoDB table per AWS organization. +--- + +Automatically syncs meshStack project metadata into a central AWS DynamoDB table whenever a building +block is assigned to a tenant. This eliminates the need for long-lived meshStack API credentials in +your AWS organization — metadata is pushed from meshStack using workload identity federation. + +## When to use it + +Use this building block when you need meshStack project data (tags, team members, workspace/project +identifiers) available inside your AWS organization without polling the meshStack API with static +credentials. Typical use cases: + +- Feeding project ownership and cost-centre tags into AWS Cost Explorer via DynamoDB. +- Driving IAM permission boundaries or SCPs with project-level metadata. +- Providing a CMDB-like inventory of all meshStack tenants in your AWS org. + +## Shared Responsibility + +| Responsibility | Platform Team | Application Team | +|---|:---:|:---:| +| Deploy backplane (DynamoDB table + IAM role) | ✅ | ❌ | +| Assign building block to tenants | ✅ | ❌ | +| Consume DynamoDB data in tooling/pipelines | ✅ | ❌ | +| Keep meshStack project tags up to date | ❌ | ✅ | +| Manage team membership in meshStack | ❌ | ✅ | + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.12.0 | +| [aws](#requirement\_aws) | >= 6.0.0 | +| [meshstack](#requirement\_meshstack) | >= 0.22.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_dynamodb_table_item.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table_item) | resource | +| [meshstack_project.this](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/data-sources/project) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [aws\_dynamodb\_table\_name](#input\_aws\_dynamodb\_table\_name) | Name of the DynamoDB table to write project metadata to. | `string` | n/a | yes | +| [aws\_region](#input\_aws\_region) | AWS region where the DynamoDB table is located. | `string` | n/a | yes | +| [platform\_identifier](#input\_platform\_identifier) | meshStack platform identifier (typically the AWS account ID for AWS tenants). | `string` | n/a | yes | +| [project\_identifier](#input\_project\_identifier) | meshStack project identifier. | `string` | n/a | yes | +| [users](#input\_users) | Project team members with their roles, injected by meshStack. |
list(object({
meshIdentifier = string
username = string
firstName = string
lastName = string
email = string
euid = string
roles = list(string)
}))
| `[]` | no | +| [workspace\_identifier](#input\_workspace\_identifier) | meshStack workspace identifier. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [dynamodb\_item\_key](#output\_dynamodb\_item\_key) | Composite key of the DynamoDB item written for this project (workspace/project). | +| [dynamodb\_item\_url](#output\_dynamodb\_item\_url) | AWS Console URL to view the specific item written for this project. | +| [dynamodb\_table\_name](#output\_dynamodb\_table\_name) | Name of the DynamoDB table the metadata was written to. | + diff --git a/modules/aws/dynamodb-project-metadata/buildingblock/logo.png b/modules/aws/dynamodb-project-metadata/buildingblock/logo.png new file mode 100644 index 00000000..e31ad968 Binary files /dev/null and b/modules/aws/dynamodb-project-metadata/buildingblock/logo.png differ diff --git a/modules/aws/dynamodb-project-metadata/buildingblock/main.tf b/modules/aws/dynamodb-project-metadata/buildingblock/main.tf new file mode 100644 index 00000000..1bbaf6a7 --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/buildingblock/main.tf @@ -0,0 +1,25 @@ +data "meshstack_project" "this" { + metadata = { + name = var.project_identifier + owned_by_workspace = var.workspace_identifier + } +} + +locals { + tags_json = jsonencode(data.meshstack_project.this.spec.tags) + users_json = jsonencode([for u in var.users : { email = u.email, roles = u.roles }]) +} + +resource "aws_dynamodb_table_item" "this" { + table_name = var.aws_dynamodb_table_name + hash_key = "workspace_identifier" + range_key = "project_identifier" + + item = jsonencode({ + workspace_identifier = { S = var.workspace_identifier } + project_identifier = { S = var.project_identifier } + platform_identifier = { S = var.platform_identifier } + tags = { S = local.tags_json } + users = { S = local.users_json } + }) +} diff --git a/modules/aws/dynamodb-project-metadata/buildingblock/outputs.tf b/modules/aws/dynamodb-project-metadata/buildingblock/outputs.tf new file mode 100644 index 00000000..531579b0 --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/buildingblock/outputs.tf @@ -0,0 +1,14 @@ +output "dynamodb_item_key" { + description = "Composite key of the DynamoDB item written for this project (workspace/project)." + value = "${var.workspace_identifier}/${var.project_identifier}" +} + +output "dynamodb_table_name" { + description = "Name of the DynamoDB table the metadata was written to." + value = var.aws_dynamodb_table_name +} + +output "dynamodb_item_url" { + description = "AWS Console URL to view the specific item written for this project." + value = "https://${var.aws_region}.console.aws.amazon.com/dynamodbv2/home?region=${var.aws_region}#item?table=${var.aws_dynamodb_table_name}&itemMode=2&pk=${var.workspace_identifier}&sk=${var.project_identifier}&route=ROUTE_ITEM_EXPLORER" +} diff --git a/modules/aws/dynamodb-project-metadata/buildingblock/provider.tf b/modules/aws/dynamodb-project-metadata/buildingblock/provider.tf new file mode 100644 index 00000000..1365e6c0 --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/buildingblock/provider.tf @@ -0,0 +1,7 @@ +provider "aws" { + region = var.aws_region +} + +provider "meshstack" { + # configured via environment variables (MESHSTACK_ENDPOINT, MESHSTACK_API_KEY) +} diff --git a/modules/aws/dynamodb-project-metadata/buildingblock/variables.tf b/modules/aws/dynamodb-project-metadata/buildingblock/variables.tf new file mode 100644 index 00000000..d6be5670 --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/buildingblock/variables.tf @@ -0,0 +1,38 @@ +variable "workspace_identifier" { + type = string + description = "meshStack workspace identifier." +} + +variable "project_identifier" { + type = string + description = "meshStack project identifier." +} + +variable "platform_identifier" { + type = string + description = "meshStack platform identifier (typically the AWS account ID for AWS tenants)." +} + +variable "users" { + type = list(object({ + meshIdentifier = string + username = string + firstName = string + lastName = string + email = string + euid = string + roles = list(string) + })) + default = [] + description = "Project team members with their roles, injected by meshStack." +} + +variable "aws_region" { + type = string + description = "AWS region where the DynamoDB table is located." +} + +variable "aws_dynamodb_table_name" { + type = string + description = "Name of the DynamoDB table to write project metadata to." +} diff --git a/modules/aws/dynamodb-project-metadata/buildingblock/versions.tf b/modules/aws/dynamodb-project-metadata/buildingblock/versions.tf new file mode 100644 index 00000000..f1524b6d --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/buildingblock/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.12.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0.0" + } + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.22.0" + } + } +} diff --git a/modules/aws/dynamodb-project-metadata/meshstack_integration.tf b/modules/aws/dynamodb-project-metadata/meshstack_integration.tf new file mode 100644 index 00000000..3e65ad56 --- /dev/null +++ b/modules/aws/dynamodb-project-metadata/meshstack_integration.tf @@ -0,0 +1,220 @@ +variable "workspace_identifier" { + type = string + description = "meshStack workspace identifier." +} + +variable "aws_region" { + type = string + description = "AWS region where the DynamoDB table will be created (e.g. 'eu-central-1')." +} + +variable "notification_subscribers" { + type = list(string) + default = [] + description = "List of email addresses to notify on building block lifecycle events." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + tags = optional(map(list(string)), {}) + }) + description = "Shared meshStack context. Tags are optional and propagated to building block definition metadata." + default = { + owning_workspace_identifier = var.workspace_identifier + } +} + +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + const = true + default = {} + description = <<-EOT + `git_ref`: Hub release reference. Set to a tag (e.g. 'v1.2.3') or branch or commit sha of the meshstack-hub repo. + `bbd_draft`: If true, the building block definition version is kept in draft mode. + EOT +} + +output "building_block_definition" { + description = "BBD is consumed in building block compositions." + value = { + uuid = meshstack_building_block_definition.this.metadata.uuid + version_ref = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release + } +} + +data "meshstack_integrations" "integrations" {} + +module "backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/aws/dynamodb-project-metadata/backplane?ref=${var.hub.git_ref}" + + workload_identity_federation = { + issuer = data.meshstack_integrations.integrations.workload_identity_federation.replicator.issuer + audience = data.meshstack_integrations.integrations.workload_identity_federation.replicator.aws.audience + subjects = [ + "${trimsuffix(data.meshstack_integrations.integrations.workload_identity_federation.replicator.subject, ":replicator")}:workspace.${var.meshstack.owning_workspace_identifier}.buildingblockdefinition.${meshstack_building_block_definition.this.metadata.uuid}" + ] + } +} + +resource "meshstack_building_block_definition" "this" { + metadata = { + owned_by_workspace = var.meshstack.owning_workspace_identifier + tags = var.meshstack.tags + } + + spec = { + display_name = "AWS DynamoDB Project Metadata" + description = "Pushes meshStack project metadata (tags, team members) into a central DynamoDB table." + support_url = "mailto:support@example.com" + notification_subscribers = var.notification_subscribers + target_type = "TENANT_LEVEL" + supported_platforms = [{ name = "AWS" }] + + readme = chomp(<<-EOT + Automatically syncs meshStack project metadata into a central AWS DynamoDB table whenever this + building block is assigned to a tenant. This eliminates the need for long-lived meshStack API + credentials in your AWS organization — metadata is pushed from meshStack using workload identity + federation (no static API keys). + + Each tenant gets one DynamoDB item keyed by `workspace_identifier` + `project_identifier`. The + item contains the workspace, project, platform identifier, all project tags, and current team + members with their roles. + + ## When to use it + + Use this building block when you need meshStack project data available inside your AWS + organization without polling the meshStack API with static credentials: + + - Feed project ownership and cost-centre tags into AWS Cost Explorer via DynamoDB. + - Drive IAM permission boundaries or SCPs with project-level metadata. + - Provide a CMDB-like inventory of all meshStack tenants in your AWS org. + + ## Shared Responsibility + + | Responsibility | Platform Team | Application Team | + |---|:---:|:---:| + | Deploy backplane (DynamoDB table + IAM role) | ✅ | ❌ | + | Assign building block to tenants | ✅ | ❌ | + | Consume DynamoDB data in tooling/pipelines | ✅ | ❌ | + | Keep meshStack project tags up to date | ❌ | ✅ | + | Manage team membership in meshStack | ❌ | ✅ | + EOT + ) + } + + version_spec = { + draft = var.hub.bbd_draft + deletion_mode = "DELETE" + + implementation = { + terraform = { + terraform_version = "1.12.0" + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + repository_path = "modules/aws/dynamodb-project-metadata/buildingblock" + ref_name = var.hub.git_ref + use_mesh_http_backend_fallback = true + } + } + + inputs = { + AWS_ROLE_ARN = { + type = "STRING" + display_name = "AWS Role ARN" + description = "ARN of the IAM role assumed by the building block runtime via workload identity federation." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(module.backplane.workload_identity_federation_role_arn) + } + AWS_WEB_IDENTITY_TOKEN_FILE = { + type = "STRING" + display_name = "AWS Web Identity Token File" + description = "Path to the OIDC token file used for workload identity federation." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("/var/run/secrets/workload-identity/aws/token") + } + aws_region = { + type = "STRING" + display_name = "AWS Region" + description = "AWS region where the DynamoDB table is located." + assignment_type = "STATIC" + argument = jsonencode(var.aws_region) + } + aws_dynamodb_table_name = { + type = "STRING" + display_name = "DynamoDB Table Name" + description = "Name of the DynamoDB table to write project metadata to." + assignment_type = "STATIC" + argument = jsonencode(module.backplane.table_name) + } + workspace_identifier = { + type = "STRING" + display_name = "Workspace Identifier" + description = "meshStack workspace identifier. Used as the DynamoDB partition key." + assignment_type = "WORKSPACE_IDENTIFIER" + } + project_identifier = { + type = "STRING" + display_name = "Project Identifier" + description = "meshStack project identifier. Used as the DynamoDB sort key." + assignment_type = "PROJECT_IDENTIFIER" + } + platform_identifier = { + type = "STRING" + display_name = "Platform Identifier" + description = "meshStack platform identifier (AWS account ID for AWS tenants)." + assignment_type = "PLATFORM_TENANT_ID" + } + users = { + type = "CODE" + display_name = "Team Members" + description = "Project team members with their roles, injected by meshStack." + assignment_type = "USER_PERMISSIONS" + } + } + + outputs = { + dynamodb_item_key = { + type = "STRING" + display_name = "DynamoDB Item Key" + description = "Composite key of the written DynamoDB item (workspace/project)." + assignment_type = "SUMMARY" + } + dynamodb_table_name = { + type = "STRING" + display_name = "DynamoDB Table Name" + description = "Name of the DynamoDB table the metadata was written to." + assignment_type = "NONE" + } + dynamodb_item_url = { + type = "STRING" + display_name = "Open in AWS Console" + description = "Direct link to the DynamoDB item in the AWS Console." + assignment_type = "RESOURCE_URL" + } + } + + permissions = [ + "PROJECT_LIST", + ] + } +} + +terraform { + required_version = ">= 1.12.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0.0" + } + meshstack = { + source = "meshcloud/meshstack" + version = "~> 0.22.0" + } + } +}