Initial commit
This commit is contained in:
+47
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
@@ -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"
|
||||||
Reference in New Issue
Block a user