From 248fd379427a534f8d2a9e4effb898384166ec1c Mon Sep 17 00:00:00 2001 From: Paulo Nonato Date: Mon, 20 Apr 2026 16:48:32 -0300 Subject: [PATCH] Add Quantum LocalStack OpenTofu project --- .gitignore | 13 +++ README.md | 94 +++++++++++++++ build/.gitkeep | 1 + examples/quantum-message.json | 14 +++ lambda/handler.py | 48 ++++++++ main.tf | 207 ++++++++++++++++++++++++++++++++++ outputs.tf | 34 ++++++ provider.tf | 22 ++++ variables.tf | 23 ++++ versions.tf | 14 +++ 10 files changed, 470 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build/.gitkeep create mode 100644 examples/quantum-message.json create mode 100644 lambda/handler.py create mode 100644 main.tf create mode 100644 outputs.tf create mode 100644 provider.tf create mode 100644 variables.tf create mode 100644 versions.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..974be46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.terraform/ +.terraform.lock.hcl +terraform.tfstate +terraform.tfstate.* +*.tfplan +*.zip +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..82d4b71 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# aws-localstack + +Projeto OpenTofu para provisionar recursos AWS simulados no LocalStack da aplicacao ficticia Quantum. + +Endpoint LocalStack: + +```text +https://localstack.paulononato.com.br +``` + +## Recursos + +- S3 bucket para artefatos da aplicacao Quantum. +- SQS fila principal e DLQ. +- Lambda Python para processar eventos. +- IAM role e policies ficticias para a Lambda. +- CloudWatch Log Group. +- Secrets Manager com credenciais simuladas. +- Event source mapping SQS -> Lambda. + +## Pre-requisitos + +- OpenTofu instalado. +- AWS CLI opcional para testes. +- Acesso ao endpoint LocalStack. + +Credenciais usadas pelo LocalStack: + +```bash +export AWS_ACCESS_KEY_ID=test +export AWS_SECRET_ACCESS_KEY=test +export AWS_DEFAULT_REGION=us-east-1 +``` + +No PowerShell: + +```powershell +$env:AWS_ACCESS_KEY_ID="test" +$env:AWS_SECRET_ACCESS_KEY="test" +$env:AWS_DEFAULT_REGION="us-east-1" +``` + +## Como usar + +Inicializar: + +```bash +tofu init +``` + +Planejar: + +```bash +tofu plan +``` + +Aplicar: + +```bash +tofu apply +``` + +Destruir: + +```bash +tofu destroy +``` + +## Testes rapidos + +Listar buckets: + +```bash +aws --endpoint-url https://localstack.paulononato.com.br s3 ls +``` + +Enviar mensagem para a fila Quantum: + +```bash +aws --endpoint-url https://localstack.paulononato.com.br sqs send-message \ + --queue-url "$(tofu output -raw quantum_queue_url)" \ + --message-body '{"event":"quantum.order.created","orderId":"QTM-1001"}' +``` + +Ver segredo: + +```bash +aws --endpoint-url https://localstack.paulononato.com.br secretsmanager get-secret-value \ + --secret-id "$(tofu output -raw quantum_secret_name)" +``` + +## Observacao sobre RDS + +RDS nao esta incluido na edicao Community do LocalStack provisionada no servidor. Este projeto evita RDS e usa apenas os servicos disponiveis na stack atual. diff --git a/build/.gitkeep b/build/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/build/.gitkeep @@ -0,0 +1 @@ + diff --git a/examples/quantum-message.json b/examples/quantum-message.json new file mode 100644 index 0000000..0e67c4e --- /dev/null +++ b/examples/quantum-message.json @@ -0,0 +1,14 @@ +{ + "event": "quantum.order.created", + "orderId": "QTM-1001", + "customer": { + "id": "CUS-9001", + "name": "Ada Lovelace" + }, + "items": [ + { + "sku": "QBIT-001", + "quantity": 2 + } + ] +} diff --git a/lambda/handler.py b/lambda/handler.py new file mode 100644 index 0000000..d4a28fc --- /dev/null +++ b/lambda/handler.py @@ -0,0 +1,48 @@ +import json +import os + + +def lambda_handler(event, context): + app_name = os.environ.get("APP_NAME", "Quantum") + bucket_name = os.environ.get("BUCKET_NAME", "unknown") + secret_name = os.environ.get("SECRET_NAME", "unknown") + + records = event.get("Records", []) + + processed = [] + for record in records: + body = record.get("body", "{}") + + try: + payload = json.loads(body) + except json.JSONDecodeError: + payload = {"raw": body} + + processed.append( + { + "messageId": record.get("messageId"), + "payload": payload, + } + ) + + print( + json.dumps( + { + "app": app_name, + "bucket": bucket_name, + "secret": secret_name, + "processedCount": len(processed), + "records": processed, + } + ) + ) + + return { + "statusCode": 200, + "body": json.dumps( + { + "message": "Quantum event batch processed", + "processedCount": len(processed), + } + ), + } diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..30f1954 --- /dev/null +++ b/main.tf @@ -0,0 +1,207 @@ +locals { + name_prefix = "${var.project_name}-${var.environment}" + + common_tags = { + Application = "Quantum" + Environment = var.environment + ManagedBy = "OpenTofu" + Runtime = "LocalStack" + } +} + +data "archive_file" "quantum_lambda" { + type = "zip" + source_file = "${path.module}/lambda/handler.py" + output_path = "${path.module}/build/quantum_lambda.zip" +} + +resource "aws_s3_bucket" "quantum_artifacts" { + bucket = "${local.name_prefix}-artifacts" + force_destroy = true + + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-artifacts" + }) +} + +resource "aws_s3_bucket_versioning" "quantum_artifacts" { + bucket = aws_s3_bucket.quantum_artifacts.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_object" "sample_config" { + bucket = aws_s3_bucket.quantum_artifacts.id + key = "config/quantum-dev.json" + content_type = "application/json" + + content = jsonencode({ + appName = "Quantum" + environment = var.environment + featureToggle = "simulated-localstack" + owner = "platform-team" + }) + + tags = local.common_tags +} + +resource "aws_sqs_queue" "quantum_dlq" { + name = "${local.name_prefix}-events-dlq" + message_retention_seconds = 1209600 + + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-events-dlq" + }) +} + +resource "aws_sqs_queue" "quantum_events" { + name = "${local.name_prefix}-events" + visibility_timeout_seconds = 45 + message_retention_seconds = 345600 + + redrive_policy = jsonencode({ + deadLetterTargetArn = aws_sqs_queue.quantum_dlq.arn + maxReceiveCount = 3 + }) + + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-events" + }) +} + +resource "aws_secretsmanager_secret" "quantum_app" { + name = "${local.name_prefix}/app" + recovery_window_in_days = 0 + + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-app-secret" + }) +} + +resource "aws_secretsmanager_secret_version" "quantum_app" { + secret_id = aws_secretsmanager_secret.quantum_app.id + + secret_string = jsonencode({ + databaseUrl = "postgres://quantum_user:fake_password@quantum-db.local:5432/quantum" + apiKey = "qtm_dev_fake_123456" + jwtSecret = "localstack-only-secret" + }) +} + +resource "aws_iam_role" "quantum_lambda" { + name = "${local.name_prefix}-lambda-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = local.common_tags +} + +resource "aws_iam_policy" "quantum_lambda" { + name = "${local.name_prefix}-lambda-policy" + description = "Permissoes simuladas da Lambda Quantum no LocalStack." + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes" + ] + Resource = aws_sqs_queue.quantum_events.arn + }, + { + Effect = "Allow" + Action = [ + "secretsmanager:GetSecretValue" + ] + Resource = aws_secretsmanager_secret.quantum_app.arn + }, + { + Effect = "Allow" + Action = [ + "s3:GetObject", + "s3:PutObject", + "s3:ListBucket" + ] + Resource = [ + aws_s3_bucket.quantum_artifacts.arn, + "${aws_s3_bucket.quantum_artifacts.arn}/*" + ] + } + ] + }) + + tags = local.common_tags +} + +resource "aws_iam_role_policy_attachment" "quantum_lambda" { + role = aws_iam_role.quantum_lambda.name + policy_arn = aws_iam_policy.quantum_lambda.arn +} + +resource "aws_cloudwatch_log_group" "quantum_lambda" { + name = "/aws/lambda/${local.name_prefix}-processor" + retention_in_days = 14 + + tags = local.common_tags +} + +resource "aws_lambda_function" "quantum_processor" { + function_name = "${local.name_prefix}-processor" + description = "Processador ficticio de eventos da aplicacao Quantum." + role = aws_iam_role.quantum_lambda.arn + runtime = "python3.11" + handler = "handler.lambda_handler" + filename = data.archive_file.quantum_lambda.output_path + source_code_hash = data.archive_file.quantum_lambda.output_base64sha256 + timeout = 30 + memory_size = 256 + + environment { + variables = { + APP_NAME = "Quantum" + BUCKET_NAME = aws_s3_bucket.quantum_artifacts.bucket + SECRET_NAME = aws_secretsmanager_secret.quantum_app.name + QUEUE_URL = aws_sqs_queue.quantum_events.url + } + } + + depends_on = [ + aws_cloudwatch_log_group.quantum_lambda, + aws_iam_role_policy_attachment.quantum_lambda + ] + + tags = local.common_tags +} + +resource "aws_lambda_event_source_mapping" "quantum_events" { + event_source_arn = aws_sqs_queue.quantum_events.arn + function_name = aws_lambda_function.quantum_processor.arn + batch_size = 5 + enabled = true +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..6d95a29 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,34 @@ +output "localstack_endpoint" { + description = "Endpoint LocalStack usado pelo provider." + value = var.localstack_endpoint +} + +output "quantum_bucket_name" { + description = "Bucket S3 da aplicacao Quantum." + value = aws_s3_bucket.quantum_artifacts.bucket +} + +output "quantum_queue_url" { + description = "URL da fila SQS principal." + value = aws_sqs_queue.quantum_events.url +} + +output "quantum_dlq_url" { + description = "URL da fila DLQ." + value = aws_sqs_queue.quantum_dlq.url +} + +output "quantum_lambda_name" { + description = "Nome da Lambda processadora." + value = aws_lambda_function.quantum_processor.function_name +} + +output "quantum_log_group_name" { + description = "Log Group CloudWatch da Lambda." + value = aws_cloudwatch_log_group.quantum_lambda.name +} + +output "quantum_secret_name" { + description = "Nome do segredo no Secrets Manager." + value = aws_secretsmanager_secret.quantum_app.name +} diff --git a/provider.tf b/provider.tf new file mode 100644 index 0000000..1aa5a1d --- /dev/null +++ b/provider.tf @@ -0,0 +1,22 @@ +provider "aws" { + region = var.aws_region + access_key = "test" + secret_key = "test" + s3_use_path_style = true + skip_credentials_validation = true + skip_metadata_api_check = true + skip_requesting_account_id = true + + endpoints { + apigateway = var.localstack_endpoint + cloudformation = var.localstack_endpoint + cloudwatch = var.localstack_endpoint + iam = var.localstack_endpoint + lambda = var.localstack_endpoint + logs = var.localstack_endpoint + s3 = var.localstack_endpoint + secretsmanager = var.localstack_endpoint + sqs = var.localstack_endpoint + sts = var.localstack_endpoint + } +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..079704c --- /dev/null +++ b/variables.tf @@ -0,0 +1,23 @@ +variable "aws_region" { + description = "Regiao AWS simulada no LocalStack." + type = string + default = "us-east-1" +} + +variable "localstack_endpoint" { + description = "Endpoint HTTPS do LocalStack." + type = string + default = "https://localstack.paulononato.com.br" +} + +variable "project_name" { + description = "Nome curto do projeto ficticio." + type = string + default = "quantum" +} + +variable "environment" { + description = "Ambiente ficticio da aplicacao." + type = string + default = "dev" +} diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..fe40e1d --- /dev/null +++ b/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + archive = { + source = "hashicorp/archive" + version = "~> 2.4" + } + } +}