commit 6e1e327bed6827ede0ea7ae07d1b53c01fefd5ec Author: Dsupanta Date: Sun Sep 6 18:39:37 2026 -0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e83b8a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Environment files +.env +.env.* + +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json +yarn.lock + +# Python +venv/ +.venv/ +__pycache__/ +*.py[cod] + +# Build artifacts +dist/ +build/ +.out/ + +# Serverless / SAM +.serverless/ +.aws-sam/ + +# AWS and credentials +aws-exports.js +aws-exports.ts +credentials +aws-credentials +*.pem +*.key + +# Terraform +.terraform/ +*.tfstate + +# Editor/OS +.vscode/ +.idea/ +.DS_Store + +# Logs +logs/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..64f8008 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +iot-lambda-dashboardApi +======================== + +Resumen +------ +Proyecto serverless simple que expone un endpoint GET /readings protegido por Cognito y API Gateway HTTP API. +La Lambda (Go) realiza un Scan sobre una tabla DynamoDB existente y devuelve los items en JSON. + +Estructura +--------- +``` +iot-lambda-dashboardApi/ +├── template.yml # CloudFormation template +├── go.mod +├── main.go # Handler y lógica simple (versión inicial sin capas) +└── README.md +``` + +Requisitos +--------- +- Go (>=1.20) para compilar el binario +- AWS CLI configurado con credenciales y permisos para crear recursos CloudFormation +- S3 bucket para alojar el paquete zip de la Lambda +- Tener desplegado el stack `iot-cognito` (o disponer de UserPoolId y AppClientId) +- Una tabla DynamoDB existente con los items de lecturas + +Variables/Parámetros del template +-------------------------------- +El template CloudFormation requiere los siguientes parámetros al desplegar: +- TableName: nombre de la tabla DynamoDB existente +- LambdaS3Bucket: bucket S3 que contiene el zip desplegable +- LambdaS3Key: key (ruta) al objeto zip con el binario +- CognitoUserPoolId: User Pool ID (puede obtenerse desde el stack iot-cognito export) +- CognitoUserPoolClientId: App Client ID (puede obtenerse desde el stack iot-cognito export) + +Construir y empaquetar la Lambda (localmente) +------------------------------------------- +1. Compilar para Linux (runtime go1.x en Lambda): + +```bash +GOOS=linux GOARCH=amd64 go build -o main main.go +``` + +2. Empaquetar en zip: + +```bash +zip deployment.zip main +``` + +3. Subir a S3: + +```bash +aws s3 cp deployment.zip s3:///path/deployment.zip +``` + +Despliegue +--------- +Ejemplo de despliegue usando `aws cloudformation deploy` pasando los parámetros necesarios: + +```bash +aws cloudformation deploy \ + --template-file template.yml \ + --stack-name iot-lambda-dashboardApi \ + --parameter-overrides \ + TableName= \ + LambdaS3Bucket= \ + LambdaS3Key= \ + CognitoUserPoolId= \ + CognitoUserPoolClientId= +``` + +Notas: +- En proyectos integrados, obtener Cognito values desde el stack `iot-cognito` con `aws cloudformation list-exports` y pasar sus valores aquí. + +Probar el endpoint +------------------ +1. Obtener un JWT (id/access token) desde Cognito (por ejemplo usando el flujo de la SPA o directamente desde la consola si se tienen credenciales). +2. Llamar al endpoint: + +```bash +curl -H "Authorization: Bearer " https://.execute-api..amazonaws.com/readings +``` + +Respuesta esperada +------------------ +JSON con la forma: + +```json +{ + "data": [ + { + "deviceId": "ARDUINO-01", + "timestamp": "2026-09-02T18:00:00Z", + "temperature": 24.7, + "humidity": 58.2 + } + ] +} +``` + +Consideraciones pedagógicas +--------------------------- +- Esta implementación inicial es intencionalmente simple: toda la lógica está en `main.go` y utiliza `Scan` en DynamoDB. +- Posteriormente los alumnos deben refactorizar: separar responsabilidades, mejorar acceso a datos, evitar Scan, añadir paginación y filtros, tests, etc. + +Errores y códigos de estado +-------------------------- +- 200 -> éxito +- 400 -> petición inválida (no implementado ampliamente en esta versión) +- 401 -> no autenticado (manejado por API Gateway/Cognito Authorizer) +- 500 -> error interno del servidor + +No crear tabla DynamoDB desde este template +------------------------------------------ +La tabla DynamoDB debe existir previamente y su nombre se pasa mediante el parámetro TableName. + +Contacto +-------- +Proyecto educativo — implementación simple para fines didácticos. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..82df090 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/example/iot-lambda-dashboardApi + +go 1.20 + +require ( + github.com/aws/aws-lambda-go v1.47.0 + github.com/aws/aws-sdk-go-v2 v1.26.0 + github.com/aws/aws-sdk-go-v2/config v1.17.0 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.21.0 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.0 +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..6cf1e15 --- /dev/null +++ b/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" +) + +// Reading represents a single IoT measurement item returned to clients +type Reading struct { + DeviceID string `json:"deviceId" dynamodbav:"deviceId"` + Timestamp string `json:"timestamp" dynamodbav:"timestamp"` + Temperature float64 `json:"temperature" dynamodbav:"temperature"` + Humidity float64 `json:"humidity" dynamodbav:"humidity"` +} + +type ResponseBody struct { + Data []Reading `json:"data"` +} + +func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { + tableName := os.Getenv("TABLE_NAME") + if tableName == "" { + log.Print("TABLE_NAME environment variable not set") + return events.APIGatewayV2HTTPResponse{StatusCode: http.StatusInternalServerError}, nil + } + + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + log.Printf("failed to load AWS config: %v", err) + return events.APIGatewayV2HTTPResponse{StatusCode: http.StatusInternalServerError}, nil + } + + db := dynamodb.NewFromConfig(cfg) + + // Simple Scan as requested for the initial version + scanInput := &dynamodb.ScanInput{ + TableName: &tableName, + } + + scanOutput, err := db.Scan(ctx, scanInput) + if err != nil { + log.Printf("dynamodb scan error: %v", err) + return events.APIGatewayV2HTTPResponse{StatusCode: http.StatusInternalServerError}, nil + } + + readings := make([]Reading, 0, len(scanOutput.Items)) + for _, item := range scanOutput.Items { + var r Reading + err := attributevalue.UnmarshalMap(item, &r) + if err != nil { + // skip items that fail to unmarshal but log them + log.Printf("failed to unmarshal item: %v", err) + continue + } + readings = append(readings, r) + } + + body := ResponseBody{Data: readings} + b, err := json.Marshal(body) + if err != nil { + log.Printf("failed to marshal response: %v", err) + return events.APIGatewayV2HTTPResponse{StatusCode: http.StatusInternalServerError}, nil + } + + return events.APIGatewayV2HTTPResponse{ + StatusCode: http.StatusOK, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + Body: string(b), + }, nil +} + +func main() { + lambda.Start(handler) +} diff --git a/template.yml b/template.yml new file mode 100644 index 0000000..9e63cea --- /dev/null +++ b/template.yml @@ -0,0 +1,134 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: >- + Serverless API for IoT Dashboard: Lambda (Go) + API Gateway HTTP API + Cognito JWT Authorizer. + +Parameters: + TableName: + Type: String + Description: DynamoDB table name that contains IoT readings (existing table) + LambdaS3Bucket: + Type: String + Description: S3 bucket that contains the Lambda deployment zip + LambdaS3Key: + Type: String + Description: S3 key (object name) for the Lambda deployment zip + CognitoUserPoolId: + Type: String + Description: Cognito User Pool ID (provide via ImportValue from iot-cognito or parameter) + CognitoUserPoolClientId: + Type: String + Description: Cognito App Client ID (provide via ImportValue from iot-cognito or parameter) + +Resources: + IotLambdaExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "iot-lambda-dashboardApi-ExecRole-${AWS::StackName}" + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: IotLambdaDynamoDBRead + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - dynamodb:Scan + - dynamodb:Query + - dynamodb:GetItem + Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${TableName}" + + IotLambdaFunction: + Type: AWS::Lambda::Function + Properties: + FunctionName: iot-lambda-dashboardApi + Runtime: go1.x + Handler: main + Role: !GetAtt IotLambdaExecutionRole.Arn + Code: + S3Bucket: !Ref LambdaS3Bucket + S3Key: !Ref LambdaS3Key + Environment: + Variables: + TABLE_NAME: !Ref TableName + Timeout: 30 + MemorySize: 128 + + IotHttpApi: + Type: AWS::ApiGatewayV2::Api + Properties: + Name: iot-lambda-dashboardApi-http + ProtocolType: HTTP + + IotHttpApiAuthorizer: + Type: AWS::ApiGatewayV2::Authorizer + Properties: + ApiId: !Ref IotHttpApi + Name: CognitoJwtAuthorizer + AuthorizerType: JWT + IdentitySource: + - "$request.header.Authorization" + JwtConfiguration: + Issuer: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPoolId}" + Audience: + - !Ref CognitoUserPoolClientId + + LambdaIntegration: + Type: AWS::ApiGatewayV2::Integration + Properties: + ApiId: !Ref IotHttpApi + IntegrationType: AWS_PROXY + IntegrationUri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${IotLambdaFunction.Arn}/invocations" + PayloadFormatVersion: "2.0" + + ReadingsRoute: + Type: AWS::ApiGatewayV2::Route + Properties: + ApiId: !Ref IotHttpApi + RouteKey: "GET /readings" + AuthorizationType: JWT + AuthorizerId: !Ref IotHttpApiAuthorizer + Target: !Sub "integrations/${LambdaIntegration}" + + ApiStage: + Type: AWS::ApiGatewayV2::Stage + Properties: + ApiId: !Ref IotHttpApi + StageName: $default + AutoDeploy: true + + LambdaInvokePermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !Ref IotLambdaFunction + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${IotHttpApi}/*/GET/readings" + +Outputs: + ApiEndpoint: + Description: HTTP API endpoint URL + Value: !Sub "https://${IotHttpApi}.execute-api.${AWS::Region}.amazonaws.com/" + Export: + Name: !Sub "${AWS::StackName}-ApiEndpoint" + + LambdaFunctionArn: + Description: Lambda function ARN + Value: !GetAtt IotLambdaFunction.Arn + Export: + Name: !Sub "${AWS::StackName}-LambdaFunctionArn" + + TableNameOutput: + Description: DynamoDB table name used by the Lambda + Value: !Ref TableName + Export: + Name: !Sub "${AWS::StackName}-TableName"