Initial commit

This commit is contained in:
Dsupanta
2026-09-06 18:39:37 -03:00
commit 6e1e327bed
5 changed files with 398 additions and 0 deletions
+47
View File
@@ -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
+119
View File
@@ -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://<your-bucket>/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=<DDB_TABLE_NAME> \
LambdaS3Bucket=<S3_BUCKET_NAME> \
LambdaS3Key=<S3_KEY> \
CognitoUserPoolId=<COGNITO_USER_POOL_ID> \
CognitoUserPoolClientId=<COGNITO_APP_CLIENT_ID>
```
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 <JWT_TOKEN>" https://<api-id>.execute-api.<region>.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.
+11
View File
@@ -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
)
+87
View File
@@ -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)
}
+134
View File
@@ -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"