Luego de la primer parte de este articulo, donde hicimos un repaso de las distintas maneras de generar un template de CloudFormation, en esta etapa vamos a entrar de lleno a la creación del mismo y usando como guía un caso de uso simple
Diseño de la solución
Caso de uso
Para hacerlo un poco mas didáctico vamos a plantear un caso de uso que podría ser igual (o muy parecido) a lo que nos pueden pedir solucionar en nuestro día a día
En este caso vamos a crear una pequeña API Rest con 2 endpoints distintos, uno para hacer un upload de un archivo a S3, guardar el nombre del archivo y otros datos del archivo en una base de datos DynamoDB y un segundo endpoint donde poder consultar esa información.
En el siguiente diagrama vamos a poder ver la conexión entre los distintos servicios:
Servicios involucrados
Para entender mejor el diagrama anterior vamos a hacer una breve descripción de los servicios que aparecen en el mismo
- Amazon API Gateway: El servicio de API Gateway nos permite crear, publicar y monitorear APIs REST o WebSocket. Al ser parte de los servicios sin servidor de AWS no vamos a necesitar gestionar ningún entorno por nosotros mismos.
- AWS Lambda: Lambda es el servicio de computo serverless de AWS, con este servicio vamos a poder ejecutar nuestro código escrito en JavaScript, Python, Go o Java entre otros como así también nos da la posibilidad de ejecutar nuestras propias imágenes de docker.
- Amazon DynamoDB: DynamoDB es una base de datos NoSQL serverless para aplicaciones de cualquier escala y al igual que otros servicios serverless vamos a pagar solo por el espacio u operaciones que realicemos evitando los gastos fijos de un servidor propio 24/7.
- Amazon Simple Storage Service (Amazon S3): S3 es el servicio de storage de objetos de AWS, donde podremos subir una cantidad ilimitada de archivos cuyo tamaño máximo puede ser de hasta 5TB cada uno.
- AWS Identity and Access Management (IAM): Si bien no lo pusimos en nuestro diagrama, IAM es un servicio transversal a todo el ecosistema de AWS y nos va a permitir gestionar los permisos y accesos de nuestros usuarios o aplicaciones, como sera el caso de nuestras funciones Lambda.
Implementación
A partir de ahora vamos a iniciar la creación de nuestro template con un archivo vacío (template.yaml) y vamos a ir agregando de a poco todas las partes necesarias para implementar la solución.
Pre-requisitos
Antes de empezar es necesario que tengas creado un bucket de S3 en tu cuenta para usarlo como source del código de la lambdas, te recomiendo ponerle de nombre cloudformation-code-artifacts-{AccountID}
, donde {AccountID}
es el numero de tu cuenta de AWS pero podes optar por cualquier otro nombre.
Una vez creado el bucket es necesario subir los archivos .zip con el código de las lambdas que vas a encontrar en este link.
Parameters
Primero vamos a declarar unos parámetros a los cuales vamos a estar haciendo referencia en nuestro template:
- SourceCodeBucketName: Este parámetro indica el nombre del bucket donde subimos los archivos .zip de las lambdas y que configuramos en los pre-requisitos
- UploadedFilesBucketName: Nombre del bucket donde se van a guardar los archivos que subamos mediante nuestra API. Podemos reemplazar la variable
{AccountID}
con el numero de tu cuenta de AWS o seleccionar cualquier otro string que queramos, recordando que los nombres de buckets deben ser únicos a nivel global, independientemente de si existen o no en nuestra cuenta. - DynamoDBTable: Nombre de la tabla donde guardaremos la información de los archivos subidos mediante nuestra API. Puede ser modificado sin problema.
- APIGatewayName: Nombre para identificar a nuestra API. Puede ser modificado sin problema.
Parameters:
SourceCodeBucketName:
Type: "String"
Default: "cloudformation-code-artifacts-{AccountID}"
UploadedFilesBucketName:
Type: "String"
Default: "uploaded-files-{AccountID}"
DynamoDBTable:
Type: "String"
Default: "UploadedFilesInfo"
APIGatewayName:
Type: "String"
Default: "FilesServiceAPI"
Resources
- S3: En esta sección vamos a crear el bucket donde se guardan los archivos que subimos, para indicar el
BucketName
hacemos referencia al parámetro que configuramos en la sección anterior
Resources:
UploadedFilesBucket:
Type: "AWS::S3::Bucket"
Properties:
BucketName: !Ref UploadedFilesBucketName
- DynamoDB: Al igual que con el bucket S3, el nombre de la tabla lo tomamos del parámetro declarado anteriormente. Con los atributos
AttributeDefinitions
yKeySchema
vamos a indicar como se llama nuestro campo, de que tipo es y luego indicarle que es la Key de nuestra tabla. Finalmente con el atributoProvisionedThroughput
le indicamos la capacidad de lectura y escritura de nuestra tabla.
FilesInfoTable:
Type: "AWS::DynamoDB::Table"
Properties:
TableName: !Ref DynamoDBTable
AttributeDefinitions:
- AttributeName: "filename"
AttributeType: "S"
KeySchema:
- AttributeName: "filename"
KeyType: "HASH"
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
- IAM: Ahora vamos a crear dos roles, uno para cada lambda para tener un mejor control sobre los permisos de cada una y seguir el principio de mínimos privilegios en nuestros servicios. Uno sera para nuestra lambda de
getFilesInfo
, donde solo tendrá acceso a la tabla que creamos, y otro parauploadFile
que tendrá acceso a nuestra tabla y al bucket de S3 para guardar los archivos.
GetFilesInfoFunctionRole:
DependsOn:
- FilesInfoTable
Type: "AWS::IAM::Role"
Properties:
RoleName: "GetFilesInfoFunctionRole"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: "GetFilesInfo-LambdaPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "dynamodb:*"
Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${DynamoDBTable}"
- Effect: "Allow"
Action:
- "logs:*"
Resource: "*"
UploadFunctionRole:
DependsOn:
- FilesInfoTable
Type: "AWS::IAM::Role"
Properties:
RoleName: "UploadFunctionRole"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: "UploadFunction-LambdaPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "s3:*"
Resource:
- !Sub "arn:aws:s3:::${UploadedFilesBucketName}"
- !Sub "arn:aws:s3:::${UploadedFilesBucketName}/*"
- Effect: "Allow"
Action:
- "dynamodb:*"
Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${DynamoDBTable}"
- Effect: "Allow"
Action:
- "logs:*"
Resource: "*"
- Lambda: Luego vamos a crear nuestras funciones lambda indicándoles en el atributo
Code
el bucket (S3Bucket
) y el nombre del archivo (S3Key
). Con el atributoEnvironment
******vamos a pasar como variable de entorno la información sobre la tabla de DynamoDB y el bucket de S3 donde guardar los archivos. Finalmente, dentro del atributoRole
le indicamos a cada función cual es el role de IAM que le corresponde
GetFilesInfoFunction:
DependsOn:
- GetFilesInfoFunctionRole
Type: "AWS::Lambda::Function"
Properties:
FunctionName: "GetFilesInfo"
Description: "Obtiene la informacion de los archivos subidos almacenada en DynamoDB"
PackageType: Zip
Code: { S3Bucket: !Ref SourceCodeBucketName, S3Key: getFilesInfo.zip }
Handler: index.handler
Runtime: nodejs20.x
Environment:
Variables:
BUCKET_NAME: !Ref UploadedFilesBucketName
REGION: !Ref AWS::Region
TABLE_NAME: !Ref DynamoDBTable
MemorySize: 128
Timeout: 30
Role: !GetAtt GetFilesInfoFunctionRole.Arn
UploadFileFunction:
DependsOn:
- UploadFunctionRole
Type: "AWS::Lambda::Function"
Properties:
FunctionName: "UploadFile"
Description: "Sube el archivo a S3 y guarda la informacion del mismo en DynamoDB"
PackageType: "Zip"
Code: { S3Bucket: !Ref SourceCodeBucketName, S3Key: uploadFile.zip }
Handler: index.handler
Runtime: nodejs20.x
Environment:
Variables:
BUCKET_NAME: !Ref UploadedFilesBucketName
REGION: !Ref AWS::Region
TABLE_NAME: !Ref DynamoDBTable
MemorySize: 128
Timeout: 30
Role: !GetAtt UploadFunctionRole.Arn
- API Gateway: En esta sección vamos a arrancar nuevamente con un rol de IAM ya que necesitamos hacer referencia al ARN de nuestras lambdas y es por eso que necesitamos que antes de poder crear este rol estén creadas nuestras funciones. Para poder crear esa dependencia usamos el atributo
DependsOn
lo cual va a generar que nuestro template se vaya desplegando por etapas. Luego de esto vamos a crear nuestra API, a la cual le asignaremos unResource
******para nuestro path/files
, a continuación le indicamos que para los métodos GET y POST debe usar nuestras lambdas mediante una integración del tipoAWS_PROXY
y por ultimo generamos el despliegue y el stage “dev”
ApiGatewayIamRole:
DependsOn:
- GetFilesInfoFunction
- UploadFileFunction
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: ""
Effect: "Allow"
Principal:
Service:
- "apigateway.amazonaws.com"
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: LambdaAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: "lambda:*"
Resource:
- !GetAtt GetFilesInfoFunction.Arn
- !GetAtt UploadFileFunction.Arn
ApiGatewayRestApi:
DependsOn:
- GetFilesInfoFunction
- UploadFileFunction
Type: "AWS::ApiGateway::RestApi"
Properties:
Name: !Ref APIGatewayName
Description: "API Gateway"
EndpointConfiguration:
Types:
- "REGIONAL"
ApiGatewayFilesResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId: !GetAtt ApiGatewayRestApi.RootResourceId
PathPart: "files"
RestApiId: !Ref ApiGatewayRestApi
GETMethod:
DependsOn:
- ApiGatewayRestApi
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: NONE
HttpMethod: "GET"
Integration:
Type: "AWS_PROXY"
IntegrationHttpMethod: "POST"
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetFilesInfoFunction.Arn}/invocations"
Credentials: !GetAtt ApiGatewayIamRole.Arn
ResourceId: !Ref ApiGatewayFilesResource
RestApiId: !Ref ApiGatewayRestApi
POSTMethod:
DependsOn:
- ApiGatewayRestApi
Type: "AWS::ApiGateway::Method"
Properties:
AuthorizationType: NONE
HttpMethod: "POST"
Integration:
Type: "AWS_PROXY"
IntegrationHttpMethod: "POST"
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${UploadFileFunction.Arn}/invocations"
Credentials: !GetAtt ApiGatewayIamRole.Arn
ResourceId: !Ref ApiGatewayFilesResource
RestApiId: !Ref ApiGatewayRestApi
ApiGatewayDeployment:
DependsOn:
- GETMethod
- POSTMethod
Type: "AWS::ApiGateway::Deployment"
Properties:
RestApiId: !Ref ApiGatewayRestApi
ApiGatewayStage:
DependsOn:
- GETMethod
- POSTMethod
Type: "AWS::ApiGateway::Stage"
Properties:
StageName: "dev"
Description: "Dev Stage"
RestApiId: !Ref ApiGatewayRestApi
DeploymentId: !Ref ApiGatewayDeployment
Outputs
Para finalizar nuestro template vamos a agregar dentro de la sección de outputs
un parámetro para poder obtener fácilmente la URL de nuestro API Gateway
Outputs:
ApiGatewayUrl:
Description: "URL API Gateway"
Value: !Sub "https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/${ApiGatewayStage}/"
Deploy del template
Para deployar los recursos que declaramos en nuestro template debemos usar la aplicación de linea de comandos (CLI) de AWS, en caso de no tenerla instalada podes seguir la guía de la documentación oficial.
Dentro de la consola, debemos ubicarnos en la carpeta donde creamos nuestro archivo template.yaml
y ejecutar el siguiente comando y, de no existir errores, veremos el mensaje confirmando la creación de nuestro stack
aws cloudformation deploy --template-file template.yaml --stack-name cloudformation-files-service-api --capabilities CAPABILITY_NAMED_IAM
Una vez deployado podemos obtener la URL de nuestro API Gateway mediante el siguiente comando.
aws cloudformation describe-stacks --query 'Stacks[?StackName==`cloudformation-files-service-api`][].Outputs[?OutputKey==`ApiGatewayUrl`].OutputValue' --output text
Probando nuestro API Gateway
Para revisar que todo este funcionando correctamente podemos enviar algunas peticiones a nuestra API mediante postman o cualquier otro método que nos resulte familiar.
-
Para subir un archivo vamos a enviar una petición POST a nuestro endpoint (
https://{APIGATEWAYID}.execute-api.us-east-1.amazonaws.com/dev/files
) con el siguiente objeto JSON en el body{ "filename": "archivo.pdf", "contentType": "application/pdf", "author": "Martin Lubo", "data": "JVBERi0xLjcNCiW1tbW1DQoxIDAgb2JqDQo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFIvTGFuZyhlbikgL1N0cnVjdFRyZWVSb290IDE1IDAgUi9NYXJrSW5mbzw8L01hcmtlZCB0cnVlPj4vTWV0YWRhdGEgMjcgMCBSL1ZpZXdlclByZWZlcmVuY2VzIDI4IDAgUj4+DQplbmRvYmoNCjIgMCBvYmoNCjw8L1R5cGUvUGFnZXMvQ291bnQgMS9LaWRzWyAzIDAgUl0gPj4NCmVuZG9iag0KMyAwIG9iag0KPDwvVHlwZS9QYWdlL1BhcmVudCAyIDAgUi9SZXNvdXJjZXM8PC9Gb250PDwvRjEgNSAwIFIvRjIgMTIgMCBSPj4vRXh0R1N0YXRlPDwvR1MxMCAxMCAwIFIvR1MxMSAxMSAwIFI+Pi9Qcm9jU2V0Wy9QREYvVGV4dC9JbWFnZUIvSW1hZ2VDL0ltYWdlSV0gPj4vTWVkaWFCb3hbIDAgMCA2MTIgNzkyXSAvQ29udGVudHMgNCAwIFIvR3JvdXA8PC9UeXBlL0dyb3VwL1MvVHJhbnNwYXJlbmN5L0NTL0RldmljZVJHQj4+L1RhYnMvUy9TdHJ1Y3RQYXJlbnRzIDA+Pg0KZW5kb2JqDQo0IDAgb2JqDQo8PC9GaWx0ZXIvRmxhdGVEZWNvZGUvTGVuZ3RoIDI5OD4+DQpzdHJlYW0NCnicrZFNa8MwDIbvBv8HHdtBHElOYgeCoY3TsrEeRjJ2KDuU0fW0sI//D1ObFjZoe9jig6wv6/FrQ9q+b3qoqnRV30bA9H7T72Cy7ZPHdhoCzGMNH1qhwf0qiQGhEOtKhs+tVk830Gs177RKFwREhnPoXrUi6UMgcGzEYGkyhu5NupYtIey+ZCTshpCO4VKrdYXo5yGxtkKys0Cl7D4PCWUV2lgiFbPA+6TlQH7IYR2Rco/Y+JAUUiN7qBEVIWEnjovDhNNIORWeobvTqpGLP2j1H4WUZaa0PzUelJ302LgYk+WusdBnopjFYT4IRduMCC+84Ss6x3xThybLz7MgkbHWeuheBuzYaPlOfxnN5I7khtAu4t/IfI7s8SJ5PYHpLxA0qxq+Ad/BuJQNCmVuZHN0cmVhbQ0KZW5kb2JqDQo1IDAgb2JqDQo8PC9UeXBlL0ZvbnQvU3VidHlwZS9UeXBlMC9CYXNlRm9udC9CQ0RFRUUrQXB0b3MvRW5jb2RpbmcvSWRlbnRpdHktSC9EZXNjZW5kYW50Rm9udHMgNiAwIFIvVG9Vbmljb2RlIDIzIDAgUj4+DQplbmRvYmoNCjYgMCBvYmoNClsgNyAwIFJdIA0KZW5kb2JqDQo3IDAgb2JqDQo8PC9CYXNlRm9udC9CQ0RFRUUrQXB0b3MvU3VidHlwZS9DSURGb250VHlwZTIvVHlwZS9Gb250L0NJRFRvR0lETWFwL0lkZW50aXR5L0RXIDEwMDAvQ0lEU3lzdGVtSW5mbyA4IDAgUi9Gb250RGVzY3JpcHRvciA5IDAgUi9XIDI1IDAgUj4+DQplbmRvYmoNCjggMCBvYmoNCjw8L09yZGVyaW5nKElkZW50aXR5KSAvUmVnaXN0cnkoQWRvYmUpIC9TdXBwbGVtZW50IDA+Pg0KZW5kb2JqDQo5IDAgb2JqDQo8PC9UeXBlL0ZvbnREZXNjcmlwdG9yL0ZvbnROYW1lL0JDREVFRStBcHRvcy9GbGFncyAzMi9JdGFsaWNBbmdsZSAwL0FzY2VudCA5MzkvRGVzY2VudCAtMjgyL0NhcEhlaWdodCA5MzkvQXZnV2lkdGggNTYxL01heFdpZHRoIDE2ODIvRm9udFdlaWdodCA0MDAvWEhlaWdodCAyNTAvU3RlbVYgNTYvRm9udEJCb3hbIC01MDAgLTI4MiAxMTgyIDkzOV0gL0ZvbnRGaWxlMiAyNCAwIFI+Pg0KZW5kb2JqDQoxMCAwIG9iag0KPDwvVHlwZS9FeHRHU3RhdGUvQk0vTm9ybWFsL2NhIDE+Pg0KZW5kb2JqDQoxMSAwIG9iag0KPDwvVHlwZS9FeHRHU3RhdGUvQk0vTm9ybWFsL0NBIDE+Pg0KZW5kb2JqDQoxMiAwIG9iag0KPDwvVHlwZS9Gb250L1N1YnR5cGUvVHJ1ZVR5cGUvTmFtZS9GMi9CYXNlRm9udC9CQ0RGRUUrQXB0b3MvRW5jb2RpbmcvV2luQW5zaUVuY29kaW5nL0ZvbnREZXNjcmlwdG9yIDEzIDAgUi9GaXJzdENoYXIgMzIvTGFzdENoYXIgMzIvV2lkdGhzIDI2IDAgUj4+DQplbmRvYmoNCjEzIDAgb2JqDQo8PC9UeXBlL0ZvbnREZXNjcmlwdG9yL0Zv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ICAyMDVbIDUzMV0gIDIzMlsgNTI1XSAgMjc1WyA1NTFdICAyNzhbIDIzOV0gIDMwNlsgNTUxXSAgMzE0WyA1NTJdICAzNDRbIDMzNF0gIDM2MlsgNTU5XSAgMzgxWyA0NTJdICAzODlbIDQ1Ml0gIDk4NVsgMjAzXSAgOTkxWyAyODZdICA5OTNbIDI4Nl0gIDEwMjFbIDI5M10gXSANCmVuZG9iag0KMjYgMCBvYmoNClsgMjAzXSANCmVuZG9iag0KMjcgMCBvYmoNCjw8L1R5cGUvTWV0YWRhdGEvU3VidHlwZS9YTUwvTGVuZ3RoIDMwODc+Pg0Kc3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IjMuMS03MDEiPgo8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgo8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiAgeG1sbnM6cGRmPSJodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvIj4KPHBkZjpQcm9kdWNlcj5NaWNyb3NvZnTCriBXb3JkIGZvciBNaWNyb3NvZnQgMzY1PC9wZGY6UHJvZHVjZXI+PC9yZGY6RGVzY3JpcHRpb24+CjxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iPgo8ZGM6Y3JlYXRvcj48cmRmOlNlcT48cmRmOmxpPk1hcnRpbiBMdWJvPC9yZGY6bGk+PC9yZGY6U2VxPjwvZGM6Y3JlYXRvcj48L3JkZjpEZXNjcmlwdGlvbj4KPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+Cjx4bXA6Q3JlYXRvclRvb2w+TWljcm9zb2Z0wq4gV29yZCBmb3IgTWljcm9zb2Z0IDM2NTwveG1wOkNyZWF0b3JUb29sPjx4bXA6Q3JlYXRlRGF0ZT4yMDI0LTA1LTEyVDE2OjUyOjExLTAzOjAwPC94bXA6Q3JlYXRlRGF0ZT48eG1wOk1vZGlmeURhdGU+MjAyNC0wNS0xMlQxNjo1MjoxMS0wMzowMDwveG1wOk1vZGlmeURhdGU+PC9yZGY6RGVzY3JpcHRpb24+CjxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyI+Cjx4bXBNTTpEb2N1bWVudElEPnV1aWQ6REExNUU0OTktMjczMC00ODcyLUFGN0ItNDk3MkQyQkY5RUFBPC94bXBNTTpEb2N1bWVudElEPjx4bXBNTTpJbnN0YW5jZUlEPnV1aWQ6REExNUU0OTktMjczMC00ODcyLUFGN0ItNDk3MkQyQkY5RUFBPC94bXBNTTpJbnN0YW5jZUlEPjwvcmRmOkRlc2NyaXB0aW9uPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKPC9yZGY6UkRGPjwveDp4bXBtZXRhPjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ0KZW5kb2JqDQoyOCAwIG9iag0KPDwvRGlzcGxheURvY1RpdGxlIHRydWU+Pg0KZW5kb2JqDQoyOSAwIG9iag0KPDwvVHlwZS9YUmVmL1NpemUgMjkvV1sgMSA0IDJdIC9Sb290IDEgMCBSL0luZm8gMTQgMCBSL0lEWzw5OUU0MTVEQTMwMjc3MjQ4QUY3QjQ5NzJEMkJGOUVBQT48OTlFNDE1REEzMDI3NzI0OEFGN0I0OTcyRDJCRjlFQUE+XSAvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAxMDY+Pg0Kc3RyZWFtDQp4nC3Myw1AQBAG4H+fNiIRF11QgINKXNWgBUW46YiTOEtcVbDW/OYwX+YJpIhRpVwBHys5BfUIZiSXYBVZCJtuE3wgPeEs2wGdftbQxBBLHFHk3/TpLtxyns9CUQrNJLQDYdUdwAvgCQ+hDQplbmRzdHJlYW0NCmVuZG9iag0KeHJlZg0KMCAzMA0KMDAwMDAwMDAxNSA2NTUzNSBmDQowMDAwMDAwMDE3IDAwMDAwIG4NCjAwMDAwMDAxNjMgMDAwMDAgbg0KMDAwMDAwMDIxOSAwMDAwMCBuDQowMDAwMDAwNDk3IDAwMDAwIG4NCjAwMDAwMDA4NjkgMDAwMDAgbg0KMDAwMDAwMDk5NyAwMDAwMCBuDQowMDAwMDAxMDI1IDAwMDAwIG4NCjAwMDAwMDExODAgMDAwMDAgbg0KMDAwMDAwMTI1MyAwMDAwMCBuDQowMDAwMDAxNDkwIDAwMDAwIG4NCjAwMDAwMDE1NDQgMDAwMDAgbg0KMDAwMDAwMTU5OCAwMDAwMCBuDQowMDAwMDAxNzY1IDAwMDAwIG4NCjAwMDAwMDIwMDMgMDAwMDAgbg0KMDAwMDAwMDAxNiA2NTUzNSBmDQowMDAwMDAwMDE3IDY1NTM1IGYNCjAwMDAwMDAwMTggNjU1MzUgZg0KMDAwMDAwMDAxOSA2NTUzNSBmDQowMDAwMDAwMDIwIDY1NTM1IGYNCjAwMDAwMDAwMjEgNjU1MzUgZg0KMDAwMDAwMDAyMiA2NTUzNSBmDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDI2OTYgMDAwMDAgbg0KMDAwMDAwMzA4NyAwMDAwMCBuDQowMDAwMDExNjM5IDAwMDAwIG4NCjAwMDAwMTE4NjggMDAwMDAgbg0KMDAwMDAxMTg5NSAwMDAwMCBuDQowMDAwMDE1MDY1IDAwMDAwIG4NCjAwMDAwMTUxMTAgMDAwMDAgbg0KdHJhaWxlcg0KPDwvU2l6ZSAzMC9Sb290IDEgMCBSL0luZm8gMTQgMCBSL0lEWzw5OUU0MTVEQTMwMjc3MjQ4QUY3QjQ5NzJEMkJGOUVBQT48OTlFNDE1REEzMDI3NzI0OEFGN0I0OTcyRDJCRjlFQUE+XSA+Pg0Kc3RhcnR4cmVmDQoxNTQxNw0KJSVFT0YNCnhyZWYNCjAgMA0KdHJhaWxlcg0KPDwvU2l6ZSAzMC9Sb290IDEgMCBSL0luZm8gMTQgMCBSL0lEWzw5OUU0MTVEQTMwMjc3MjQ4QUY3QjQ5NzJEMkJGOUVBQT48OTlFNDE1REEzMDI3NzI0OEFGN0I0OTcyRDJCRjlFQUE+XSAvUHJldiAxNTQxNy9YUmVmU3RtIDE1MTEwPj4NCnN0YXJ0eHJlZg0KMTYxNzQNCiUlRU9G" }
En caso de ejecutarse correctamente vamos a recibir el siguiente mensaje como respuesta
{ "msg": "File uploaded successfully" }
-
Para obtener la información de los archivos subidos debemos enviar una petición get a nuestro endpoint (
https://{APIGATEWAYID}.execute-api.us-east-1.amazonaws.com/dev/files
) y en caso de estar todo bien recibiremos un array de objetos con la información de los archivos subidos[ { "filename": { "S": "archivo.pdf" }, "contentType": { "S": "application/pdf" }, "author": { "S": "Martin Lubo" }, }, ]
Eliminar el deploy
Por ultimo, en caso que necesitemos eliminar nuestro stack podemos hacerlo tanto desde la consola web como desde la linea de comando utilizando el siguiente comando
aws cloudformation delete-stack --stack-name cloudformation-files-service-api
En ambos casos es importante que, antes de ejecutar este paso, eliminemos todo el contenido del bucket de destino para los archivos subidos para evitar errores, ya que AWS no permite eliminar un bucket que contenga archivos.
En los próximos artículos estaremos deployando esta misma solución pero utilizando AWS Serverless Application Model (AWS SAM) y AWS Cloud Development Kit (AWS CDK)