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:

Diagrama de aplicacion serverless en AWS

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 y KeySchema 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 atributo ProvisionedThroughput 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 para uploadFile 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 atributo Environment ******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 atributo Role 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 un Resource ******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 tipo AWS_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

mensaje en consola de un deploy exitoso del stack

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)