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