From 867f8b708f2715afae4c957ec598ed9987bc29d6 Mon Sep 17 00:00:00 2001 From: maxsonferovante Date: Sat, 5 Sep 2026 09:30:52 -0300 Subject: [PATCH 1/4] feat: add DynamoDB single-table Terraform module with 5 GSIs - Create single_table module with composite key design: - PK/SK as main access pattern - GSI1: UUID-based lookups (Certificate by ID) - GSI2: Email-based access (Orders, Certificates, Participants) - GSI3: Product-based access (Products, Certificates, Orders) - GSI4: Success flag access (successful certificates) - GSI5: City-based access (Participants) - Update dev environment to use single_table module - Replace 4 separate DynamoDB tables with single table design --- terraform/env/dev/main.tf | 34 +---- .../modules/02.dynamodb/single_table/main.tf | 138 ++++++++++++++++++ .../02.dynamodb/single_table/outputs.tf | 14 ++ .../02.dynamodb/single_table/variables.tf | 14 ++ 4 files changed, 171 insertions(+), 29 deletions(-) create mode 100644 terraform/modules/02.dynamodb/single_table/main.tf create mode 100644 terraform/modules/02.dynamodb/single_table/outputs.tf create mode 100644 terraform/modules/02.dynamodb/single_table/variables.tf diff --git a/terraform/env/dev/main.tf b/terraform/env/dev/main.tf index 5d084ef..6a74afe 100644 --- a/terraform/env/dev/main.tf +++ b/terraform/env/dev/main.tf @@ -7,31 +7,10 @@ module "s3" { region = var.aws_region } -# DynamoDB Tables -module "certificates_table" { - source = "../../modules/02.dynamodb/certificates" - table_name = "${var.project_name}-certificates-${var.environment}" - environment = var.environment - project_name = var.project_name -} - -module "orders_table" { - source = "../../modules/02.dynamodb/orders" - table_name = "${var.project_name}-orders-${var.environment}" - environment = var.environment - project_name = var.project_name -} - -module "participants_table" { - source = "../../modules/02.dynamodb/participants" - table_name = "${var.project_name}-participants-${var.environment}" - environment = var.environment - project_name = var.project_name -} - -module "products_table" { - source = "../../modules/02.dynamodb/products" - table_name = "${var.project_name}-products-${var.environment}" +# DynamoDB Single Table +module "dynamodb_single_table" { + source = "../../modules/02.dynamodb/single_table" + table_name = "${var.project_name}-${var.environment}" environment = var.environment project_name = var.project_name } @@ -63,10 +42,7 @@ module "lambda" { notification_queue_arn = module.sqs.notification_queue_arn notification_queue_url = module.sqs.notification_queue_url dynamodb_table_arns = [ - module.certificates_table.table_arn, - module.orders_table.table_arn, - module.participants_table.table_arn, - module.products_table.table_arn + module.dynamodb_single_table.table_arn ] s3_bucket_arn = module.s3.bucket_arn s3_bucket_name = module.s3.bucket_name diff --git a/terraform/modules/02.dynamodb/single_table/main.tf b/terraform/modules/02.dynamodb/single_table/main.tf new file mode 100644 index 0000000..8e25f07 --- /dev/null +++ b/terraform/modules/02.dynamodb/single_table/main.tf @@ -0,0 +1,138 @@ +# Single Table Design - DynamoDB +# Baseado no padrão: https://aws.amazon.com/pt/blogs/compute/creating-a-single-table-design-with-amazon-dynamodb/ +# +# Entity Patterns: +# PK: EntityType#EntityId (ex: ORDER#123, PRODUCT#1, PARTICIPANT#uuid, CERTIFICATE#123) +# SK: EntityType#EntityId (mesmo formato para acesso direto) +# +# GSIs para padrões de acesso: +# GSI1: Acesso por email (participants, orders, certificates) +# GSI2: Acesso por product (orders, certificates) +# GSI3: Acesso por success flag (certificates) +# GSI4: Acesso por city (participants) + +resource "aws_dynamodb_table" "single_table" { + name = var.table_name + billing_mode = "PAY_PER_REQUEST" + hash_key = "PK" + range_key = "SK" + + attribute { + name = "PK" + type = "S" + } + + attribute { + name = "SK" + type = "S" + } + + # Atributos base - entidades + attribute { + name = "GSI1PK" + type = "S" + } + + attribute { + name = "GSI1SK" + type = "S" + } + + attribute { + name = "GSI2PK" + type = "S" + } + + attribute { + name = "GSI2SK" + type = "S" + } + + attribute { + name = "GSI3PK" + type = "S" + } + + attribute { + name = "GSI3SK" + type = "S" + } + + attribute { + name = "GSI4PK" + type = "S" + } + + attribute { + name = "GSI4SK" + type = "S" + } + + attribute { + name = "GSI5PK" + type = "S" + } + + attribute { + name = "GSI5SK" + type = "S" + } + + # EntityType para identificação rápida do tipo + attribute { + name = "EntityType" + type = "S" + } + + # GSI1: CERT# lookups (certificate find_by_id) + global_secondary_index { + name = "GSI1" + hash_key = "GSI1PK" + range_key = "GSI1SK" + projection_type = "ALL" + } + + # GSI2: EMAIL# lookups (email lookups across entities) + global_secondary_index { + name = "GSI2" + hash_key = "GSI2PK" + range_key = "GSI2SK" + projection_type = "ALL" + } + + # GSI3: PRODUCT# lookups (product lookups) + global_secondary_index { + name = "GSI3" + hash_key = "GSI3PK" + range_key = "GSI3SK" + projection_type = "ALL" + } + + # GSI4: SUCCESS# lookups (certificate success flag) + global_secondary_index { + name = "GSI4" + hash_key = "GSI4PK" + range_key = "GSI4SK" + projection_type = "ALL" + } + + # GSI5: CITY# lookups (participant city) + global_secondary_index { + name = "GSI5" + hash_key = "GSI5PK" + range_key = "GSI5SK" + projection_type = "ALL" + } + + server_side_encryption { + enabled = true + } + + tags = { + Name = var.table_name + Environment = var.environment + Project = var.project_name + Design = "SingleTable" + CostCenter = "low-cost" + } +} diff --git a/terraform/modules/02.dynamodb/single_table/outputs.tf b/terraform/modules/02.dynamodb/single_table/outputs.tf new file mode 100644 index 0000000..d2e5d3c --- /dev/null +++ b/terraform/modules/02.dynamodb/single_table/outputs.tf @@ -0,0 +1,14 @@ +output "table_name" { + value = aws_dynamodb_table.single_table.name + description = "Nome da tabela única" +} + +output "table_arn" { + value = aws_dynamodb_table.single_table.arn + description = "ARN da tabela única" +} + +output "table_id" { + value = aws_dynamodb_table.single_table.id + description = "ID da tabela única" +} diff --git a/terraform/modules/02.dynamodb/single_table/variables.tf b/terraform/modules/02.dynamodb/single_table/variables.tf new file mode 100644 index 0000000..eba4420 --- /dev/null +++ b/terraform/modules/02.dynamodb/single_table/variables.tf @@ -0,0 +1,14 @@ +variable "table_name" { + description = "Nome da tabela única" + type = string +} + +variable "environment" { + description = "Ambiente de deploy" + type = string +} + +variable "project_name" { + description = "Nome do projeto" + type = string +} From 79137666ad74e3349c5d247964be69fa8a47f691 Mon Sep 17 00:00:00 2001 From: maxsonferovante Date: Sat, 5 Sep 2026 09:39:31 -0300 Subject: [PATCH 2/4] docs: add DynamoDB single-table design documentation --- README.md | 50 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 464125e..58cb4a1 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,7 @@ A estrutura de pastas do projeto é organizada da seguinte forma: │ ├── api-gateway/ │ ├── bucket_s3/ │ ├── dynamodb/ -│ │ ├── certificates/ -│ │ ├── orders/ -│ │ ├── participants/ -│ │ └── products/ +│ │ └── single_table/ # Single-table design │ ├── lambda/ │ └── sqs/ └── README.md @@ -65,19 +62,48 @@ A seguir estão os serviços da AWS criados por este projeto e suas respectivas - `region`: Região da AWS. - `lifecycle_rule`: Regras de ciclo de vida dos objetos S3. -### DynamoDB +### DynamoDB Single-Table -- **Descrição**: Cria as tabelas do DynamoDB para a aplicação. -- **Módulos**: - - `certificates`: Tabela de certificados. - - `orders`: Tabela de pedidos. - - `participants`: Tabela de participantes. - - `products`: Tabela de produtos. -- **Variáveis (por módulo)**: +- **Descrição**: Cria a tabela DynamoDB com padrão Single-Table Design. +- **Módulo**: `single_table` +- **Variáveis**: - `table_name`: Nome da tabela. - `environment`: Ambiente de deploy. - `project_name`: Nome do projeto. +#### Estrutura da Tabela + +A tabela utiliza um design Single-Table com chaves compostas e 5 Global Secondary Indexes (GSIs). + +| Atributo | Tipo | Descrição | +|----------|------|-----------| +| `PK` | String | Chave de Partição principal | +| `SK` | String | Chave de Ordenação principal | +| `GSI1PK`, `GSI1SK` | String | GSI1 - Certificate por UUID | +| `GSI2PK`, `GSI2SK` | String | GSI2 - Orders, Certificates, Participants por email | +| `GSI3PK`, `GSI3SK` | String | GSI3 - Products por nome, Certificates/Orders por product | +| `GSI4PK`, `GSI4SK` | String | GSI4 - Certificados por status de sucesso | +| `GSI5PK`, `GSI5SK` | String | GSI5 - Participants por cidade | + +#### GSIs (Global Secondary Indexes) + +| GSI | Key Schema | Access Pattern | +|-----|------------|----------------| +| GSI1 | `PK: UUID, SK: CERT#` | Certificate by UUID | +| GSI2 | `PK: email, SK: ENTITY#` | Orders, Certificates, Participants by email | +| GSI3 | `PK: product, SK: ENTITY#` | Products by name, Certificates/Orders by product | +| GSI4 | `PK: SUCCESS#Y/N, SK: CERT#` | Successful/Failed certificates | +| GSI5 | `PK: CITY#name, SK: PART#` | Participants by city | + +#### Entidades Suportadas + +| Entidade | PK | SK | GSIs Utilizados | +|----------|----|----|----------------| +| Certificate | `CERTIFICATE#` | `CERTIFICATE#` | GSI1, GSI2, GSI3, GSI4 | +| Order | `ORDER#` | `ORDER#` | GSI2, GSI3 | +| Product | `PRODUCT#` | `PRODUCT#` | GSI3 | +| Participant | `PARTICIPANT#` | `PARTICIPANT#` | GSI2, GSI5 | + ### Lambda - **Descrição**: Cria as funções Lambda no formato ZIP e a infraestrutura associada. From 258a80fb13b2376322c8ad260a80dc568047b864 Mon Sep 17 00:00:00 2001 From: maxsonferovante Date: Mon, 7 Sep 2026 19:10:41 -0300 Subject: [PATCH 3/4] fix: remove GSI5 and update outputs per PR feedback - Remove GSI5 (city-based access for participants) - Update outputs.tf to reference dynamodb_single_table module - Fix terraform validate errors (8 module reference errors) - Update comments to reflect actual GSI usage --- terraform/env/dev/outputs.tf | 47 +++---------------- .../modules/02.dynamodb/single_table/main.tf | 26 ++-------- 2 files changed, 11 insertions(+), 62 deletions(-) diff --git a/terraform/env/dev/outputs.tf b/terraform/env/dev/outputs.tf index 3d70568..d2cf03e 100644 --- a/terraform/env/dev/outputs.tf +++ b/terraform/env/dev/outputs.tf @@ -9,48 +9,15 @@ output "s3_bucket_arn" { value = module.s3.bucket_arn } -# Informações das tabelas de certificados -output "certificates_table_name" { - description = "Nome da tabela de certificados" - value = module.certificates_table.table_name +# Informações da tabela única DynamoDB (single-table design) +output "dynamodb_single_table_name" { + description = "Nome da tabela DynamoDB única" + value = module.dynamodb_single_table.table_name } -output "certificates_table_arn" { - description = "ARN da tabela de certificados" - value = module.certificates_table.table_arn -} - -# Informações das tabelas de pedidos -output "orders_table_name" { - description = "Nome da tabela de pedidos" - value = module.orders_table.table_name -} - -output "orders_table_arn" { - description = "ARN da tabela de pedidos" - value = module.orders_table.table_arn -} - -# Informações das tabelas de participantes -output "participants_table_name" { - description = "Nome da tabela de participantes" - value = module.participants_table.table_name -} - -output "participants_table_arn" { - description = "ARN da tabela de participantes" - value = module.participants_table.table_arn -} - -# Informações das tabelas de produtos -output "products_table_name" { - description = "Nome da tabela de produtos" - value = module.products_table.table_name -} - -output "products_table_arn" { - description = "ARN da tabela de produtos" - value = module.products_table.table_arn +output "dynamodb_single_table_arn" { + description = "ARN da tabela DynamoDB única" + value = module.dynamodb_single_table.table_arn } # Informações do SQS diff --git a/terraform/modules/02.dynamodb/single_table/main.tf b/terraform/modules/02.dynamodb/single_table/main.tf index 8e25f07..9f7f92a 100644 --- a/terraform/modules/02.dynamodb/single_table/main.tf +++ b/terraform/modules/02.dynamodb/single_table/main.tf @@ -6,10 +6,10 @@ # SK: EntityType#EntityId (mesmo formato para acesso direto) # # GSIs para padrões de acesso: -# GSI1: Acesso por email (participants, orders, certificates) -# GSI2: Acesso por product (orders, certificates) -# GSI3: Acesso por success flag (certificates) -# GSI4: Acesso por city (participants) +# GSI1: Acesso por UUID (certificate by ID) +# GSI2: Acesso por email (orders, certificates, participants) +# GSI3: Acesso por product (products, orders, certificates) +# GSI4: Acesso por success flag (certificates) resource "aws_dynamodb_table" "single_table" { name = var.table_name @@ -68,16 +68,6 @@ resource "aws_dynamodb_table" "single_table" { type = "S" } - attribute { - name = "GSI5PK" - type = "S" - } - - attribute { - name = "GSI5SK" - type = "S" - } - # EntityType para identificação rápida do tipo attribute { name = "EntityType" @@ -116,14 +106,6 @@ resource "aws_dynamodb_table" "single_table" { projection_type = "ALL" } - # GSI5: CITY# lookups (participant city) - global_secondary_index { - name = "GSI5" - hash_key = "GSI5PK" - range_key = "GSI5SK" - projection_type = "ALL" - } - server_side_encryption { enabled = true } From 3d09189f6deb19380d056ffb3fef8fc65a334c34 Mon Sep 17 00:00:00 2001 From: maxsonferovante Date: Mon, 7 Sep 2026 19:11:45 -0300 Subject: [PATCH 4/4] docs: remove GSI5 from single-table documentation --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 58cb4a1..4b051fd 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ A seguir estão os serviços da AWS criados por este projeto e suas respectivas #### Estrutura da Tabela -A tabela utiliza um design Single-Table com chaves compostas e 5 Global Secondary Indexes (GSIs). +A tabela utiliza um design Single-Table com chaves compostas e 4 Global Secondary Indexes (GSIs). | Atributo | Tipo | Descrição | |----------|------|-----------| @@ -83,7 +83,6 @@ A tabela utiliza um design Single-Table com chaves compostas e 5 Global Secondar | `GSI2PK`, `GSI2SK` | String | GSI2 - Orders, Certificates, Participants por email | | `GSI3PK`, `GSI3SK` | String | GSI3 - Products por nome, Certificates/Orders por product | | `GSI4PK`, `GSI4SK` | String | GSI4 - Certificados por status de sucesso | -| `GSI5PK`, `GSI5SK` | String | GSI5 - Participants por cidade | #### GSIs (Global Secondary Indexes) @@ -93,7 +92,6 @@ A tabela utiliza um design Single-Table com chaves compostas e 5 Global Secondar | GSI2 | `PK: email, SK: ENTITY#` | Orders, Certificates, Participants by email | | GSI3 | `PK: product, SK: ENTITY#` | Products by name, Certificates/Orders by product | | GSI4 | `PK: SUCCESS#Y/N, SK: CERT#` | Successful/Failed certificates | -| GSI5 | `PK: CITY#name, SK: PART#` | Participants by city | #### Entidades Suportadas @@ -102,7 +100,7 @@ A tabela utiliza um design Single-Table com chaves compostas e 5 Global Secondar | Certificate | `CERTIFICATE#` | `CERTIFICATE#` | GSI1, GSI2, GSI3, GSI4 | | Order | `ORDER#` | `ORDER#` | GSI2, GSI3 | | Product | `PRODUCT#` | `PRODUCT#` | GSI3 | -| Participant | `PARTICIPANT#` | `PARTICIPANT#` | GSI2, GSI5 | +| Participant | `PARTICIPANT#` | `PARTICIPANT#` | GSI2 | ### Lambda