DEV Community 👩‍💻👨‍💻

camilo cabrales
camilo cabrales

Posted on

IaC - Como crear un API Rest con: CloudFormation o SAM

En este post vamos a crear la siguiente infraestructura utilizando algunas de las herramienta de IaC que tenemos a nuestra disposición en AWS.

Infraestructura

Empecemos con la plantilla para CloudFormation:

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  MetodoGET:
    Type: String
    Default: GET
  MetodoPOST:
    Type: String
    Default: POST
  ClientePath:
    Type: String
    Default: cliente
  SucursalPath:
    Type: String
    Default: sucursal
  Stage:
    Type: String
    Default: dev
  RoleFullAccessDynamoDB:
    Type: String
    Default: arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
  RoleFullAccessCloudWatch:
    Type: String
    Default: arn:aws:iam::aws:policy/CloudWatchFullAccess

Resources:
  RoleFuncionLambda:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
        - !Ref RoleFullAccessDynamoDB
        - !Ref RoleFullAccessCloudWatch
  APITienda:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: APITienda
      EndpointConfiguration:
        Types:
          - REGIONAL
  RecursoCliente:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref  APITienda
      PathPart: !Ref ClientePath
      ParentId: !GetAtt APITienda.RootResourceId
  RecursoSucursal:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref  APITienda
      PathPart: !Ref SucursalPath
      ParentId: !GetAtt APITienda.RootResourceId
  MetodoObtenerClientes:
    Type: AWS::ApiGateway::Method
    Properties:
      AuthorizationType: NONE
      HttpMethod: !Ref MetodoGET
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub
          - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
          - lambdaArn: !GetAtt ObtenerClienteLambda.Arn
      ResourceId: !GetAtt RecursoCliente.ResourceId
      RestApiId: !Ref APITienda
  MetodoRegistrarCliente:
    Type: AWS::ApiGateway::Method
    Properties:
      AuthorizationType: NONE
      HttpMethod: !Ref MetodoPOST
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub
          - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
          - lambdaArn: !GetAtt RegistrarClienteLambda.Arn
      ResourceId: !GetAtt RecursoCliente.ResourceId
      RestApiId: !Ref APITienda
  MetodoObtenerSucursales:
    Type: AWS::ApiGateway::Method
    Properties:
      AuthorizationType: NONE
      HttpMethod: !Ref MetodoGET
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub
          - arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
          - lambdaArn: !GetAtt ObtenerSucursalesLambda.Arn
      ResourceId: !GetAtt RecursoSucursal.ResourceId
      RestApiId: !Ref APITienda
  Despliegue:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - MetodoObtenerClientes
      - MetodoRegistrarCliente
      - MetodoObtenerSucursales
    Properties: 
      RestApiId: !Ref APITienda
      StageName: !Ref Stage
  RegistrarClienteLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: "Agrega clientes a la base de datos"
      FunctionName: "RegistarCliente"
      Handler: index.lambda_handler
      MemorySize: 128
      Runtime: python3.8
      Timeout: 3
      Code: 
        ZipFile: |
          import json
          import boto3 
          from boto3.dynamodb.conditions import Key

          dynamodb = boto3.resource("dynamodb")
          table = dynamodb.Table("Tiendas")

          def lambda_handler(event, context):
            data = json.loads(event["body"])
            idCliente= data["id"]
            nombre = data["nombre"]
            apellido = data["apellido"]
            table.put_item(Item={
              "Id": "Cliente",
              "Filtro":"Id#"+idCliente,
              "Nombre":nombre,
              "Apellido":apellido
            })
            return {
              'statusCode': 200,
              'body': json.dumps(idCliente)
            }
      Role: !GetAtt  RoleFuncionLambda.Arn
  ObtenerClienteLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: "Obtiene los clientes"
      FunctionName: "ObtenerCliente"
      Handler: index.lambda_handler
      MemorySize: 128
      Runtime: python3.8
      Timeout: 3
      Code: 
        ZipFile: |
          import json
          import boto3 
          from boto3.dynamodb.conditions import Key

          dynamodb = boto3.resource("dynamodb")
          table = dynamodb.Table("Tiendas")

          def lambda_handler(event, context):

            id = event["queryStringParameters"]["id"]
            cliente = table.query(KeyConditionExpression=Key('Id').eq("Cliente") & Key('Filtro').eq('Id#'+id),                   
                                ProjectionExpression="Id,Filtro,Nombre,Apellido")
            return {
              'statusCode': 200,
              'body': json.dumps(cliente)
            }
      Role: !GetAtt  RoleFuncionLambda.Arn
  ObtenerSucursalesLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: "Obtiene las sucursales"
      FunctionName: "ObtenerSucursales"
      Handler: index.lambda_handler
      MemorySize: 128
      Runtime: python3.8
      Timeout: 3
      Code: 
        ZipFile: |
          import json
          import boto3 
          from boto3.dynamodb.conditions import Key

          dynamodb = boto3.resource("dynamodb")
          table = dynamodb.Table("Tiendas")

          def lambda_handler(event, context):

            sucursales = table.query(KeyConditionExpression=Key('Id').eq("Sucursal"),                   
                                  ProjectionExpression="Id,Filtro,Direccion")
            return {
              'statusCode': 200,
              'body': json.dumps(sucursales)
            }
      Role: !GetAtt  RoleFuncionLambda.Arn
  PermisoObtenerClientes:
    Type: AWS::Lambda::Permission
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt ObtenerClienteLambda.Arn
      Principal: "apigateway.amazonaws.com"
      SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${APITienda}/*/${MetodoGET}/${ClientePath}"
  PermisoRegistrarCliente:
    Type: AWS::Lambda::Permission
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !Ref RegistrarClienteLambda
      Principal: "apigateway.amazonaws.com"
      SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${APITienda}/*/${MetodoPOST}/${ClientePath}"
  PermisoObtenerSucursales:
    Type: AWS::Lambda::Permission
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !Ref ObtenerSucursalesLambda
      Principal: "apigateway.amazonaws.com"
      SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${APITienda}/*/${MetodoGET}/${SucursalPath}"
  Tabla:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: Tiendas
      BillingMode: PROVISIONED
      ProvisionedThroughput: 
        ReadCapacityUnits: "5"
        WriteCapacityUnits: "5"
      AttributeDefinitions:
        -
          AttributeName: "Id"
          AttributeType: "S"
        -
          AttributeName: "Filtro"
          AttributeType: "S"
      KeySchema:
        -
          AttributeName: "Id"
          KeyType: "HASH"
        -
          AttributeName: "Filtro"
          KeyType: "RANGE"
Outputs:
  ApiEndPoint:
    Description: "EndPoint Api"
    Value: !Sub "https://${APITienda}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/"
  ClienteEndPoint:
    Description: "EndPoint Metodos Cliente"
    Value: !Sub "https://${APITienda}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/${ClientePath}"
  SucursalEndPoint:
    Description: "EndPoint Metodo Sucursal"
    Value: !Sub "https://${APITienda}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/${SucursalPath}"
Enter fullscreen mode Exit fullscreen mode

apirest.yml

Las etiquetas que utilizamos para crear los recursos son las siguientes:

Infraestructura CloudFormation

Para poder desplegar nuestra infraestructura debemos asegurarnos de tener instalada la CLI (Instalar CLI) en nuestro equipo y haber configurado nuestras credenciales(comando aws configure).

Vamos a validar que nuestra plantilla este construida de manera correcta. Ubicados en la carpeta donde se encuentra la plantilla vamos a ejecutar el siguiente comando:

aws cloudformation validate-template --template-body file://apirest.yml

Si la plantilla esta bien construida el comando nos va a retornar la lista de parámetros que tiene nuestra plantilla.

Parameters

Una vez sabemos que nuestra plantilla es correcta vamos a desplegarla utilizando el siguiente comando

aws cloudformation deploy --template-file apirest.yml --stack-name apiresIaC --capabilities CAPABILITY_IAM

que recibe como parámetros el nombre de la plantilla y el nombre del stack que vamos a crear. Adicionalmente le enviamos el parámetro --capabilities CAPABILITY_IAM ya que vamos hacer uso de IAM.

Una vez que ejecutamos el comando vamos a ver lo siguiente:

Deploy

Lo que nos indica que nuestros recursos fueron desplegados con exito. Podemos revisar todo el proceso y el resultado del despliegue de la infraestructura desde la consola de AWS. Para este buscamos el servicio CloudFormation y damos click en Stacks, donde vamos a ver el stack que creamos con el comando deploy.

Deploy

Si damos click en el nombre del stack creado vamos a poder ver todo el detalle de los eventos del proceso de creación, los recursos creados, los parámetros, las salidas, el template y los cambios que se han realizado sobre la plantilla. Damos click en Outputs para ver las rutas de los servicios desplegados.

Detail Stack

Para probar nuestros servicios podemos utilizar PostMan o en mi caso estoy utilizando la extension REST Client para Visual Studio Code. Puedo probar los servicios de la siguiente manera:

POST   https://5d24uhg9db.execute-api.us-east-1.amazonaws.com/dev/cliente HTTP/1.1
content-type: application/json

{
    "id": "1",
    "nombre": "Lisa",
    "apellido": "Simpson"
}

###

GET   https://5d24uhg9db.execute-api.us-east-1.amazonaws.com/dev/cliente?id=1 HTTP/1.1
content-type: application/json

###

GET   https://5d24uhg9db.execute-api.us-east-1.amazonaws.com/dev/sucursal HTTP/1.1
content-type: application/json
Enter fullscreen mode Exit fullscreen mode

Con esto finalizamos el despliegue y las pruebas de nuestro API Rest creado con CloudFormation.

Ahora vamos a crear la misma infraestructura utilizando SAM(Serverless Application Model). SAM nos permite desplegar aplicaciones sin servidor en AWS a diferencia de CloudFormation que nos permite desplegar todo tipo de recursos.

Las etiquetas que utilizamos para crear los recursos son las siguientes:

Infraestructura SAM

La plantilla que utilizamos es la siguiente:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
  Stage:
    Type: String
    Default: dev
  RoleFullAccessDynamoDB:
    Type: String
    Default: arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
  RoleFullAccessCloudWatch:
    Type: String
    Default: arn:aws:iam::aws:policy/CloudWatchFullAccess
  ClientePath:
    Type: String
    Default: cliente
  SucursalPath:
    Type: String
    Default: sucursal
Resources:
  RoleFuncionLambda:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
        - !Ref RoleFullAccessDynamoDB
        - !Ref RoleFullAccessCloudWatch
  APITienda:
    Type: AWS::Serverless::Api
    Properties:
      Name: APITienda
      StageName: !Ref Stage

  RegistrarClienteLambda:
    Type: AWS::Serverless::Function
    Properties:
      InlineCode: |
          import json
          import boto3 
          from boto3.dynamodb.conditions import Key

          dynamodb = boto3.resource("dynamodb")
          table = dynamodb.Table("Tiendas")

          def lambda_handler(event, context):
            data = json.loads(event["body"])
            idCliente= data["id"]
            nombre = data["nombre"]
            apellido = data["apellido"]
            table.put_item(Item={
              "Id": "Cliente",
              "Filtro":"Id#"+idCliente,
              "Nombre":nombre,
              "Apellido":apellido
            })
            return {
              'statusCode': 200,
              'body': json.dumps(idCliente)
            }
      Handler: index.lambda_handler
      Runtime: python3.7
      PackageType: Zip
      FunctionName: RegistrarCliente
      Role: !GetAtt RoleFuncionLambda.Arn
      Events:
        RegistrarCliente:
          Type: Api
          Properties:
            RestApiId: !Ref APITienda
            Path: /cliente
            Method: POST
  ObtenerClienteLambda:
    Type: AWS::Serverless::Function
    Properties:
      InlineCode: |
          import json
          import boto3 
          from boto3.dynamodb.conditions import Key

          dynamodb = boto3.resource("dynamodb")
          table = dynamodb.Table("Tiendas")

          def lambda_handler(event, context):

            id = event["queryStringParameters"]["id"]
            cliente = table.query(KeyConditionExpression=Key('Id').eq("Cliente") & Key('Filtro').eq('Id#'+id),                   
                                ProjectionExpression="Id,Filtro,Nombre,Apellido")
            return {
              'statusCode': 200,
              'body': json.dumps(cliente)
            }
      Handler: index.lambda_handler
      Runtime: python3.7
      PackageType: Zip
      FunctionName: ObtenerCliente
      Role: !GetAtt RoleFuncionLambda.Arn
      Events:
        ObtenerClientes:
          Type: Api
          Properties:
            RestApiId: !Ref APITienda
            Path: /cliente
            Method: GET
  SucursalesLambda:
    Type: AWS::Serverless::Function
    Properties:
      InlineCode: |
          import json
          import boto3 
          from boto3.dynamodb.conditions import Key

          dynamodb = boto3.resource("dynamodb")
          table = dynamodb.Table("Tiendas")

          def lambda_handler(event, context):

            sucursales = table.query(KeyConditionExpression=Key('Id').eq("Sucursal"),                   
                                  ProjectionExpression="Id,Filtro,Direccion")
            print(sucursales["Items"])
            return {
              'statusCode': 200,
              'body': json.dumps(sucursales)
            }
      Handler: index.lambda_handler
      Runtime: python3.7
      PackageType: Zip
      FunctionName: ObtenerSucursales
      Role: !GetAtt RoleFuncionLambda.Arn
      Events:
        ObtenerSucursales:
          Type: Api
          Properties:
            RestApiId: !Ref APITienda
            Path: /sucursal
            Method: GET
  Tabla:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: Tiendas
      BillingMode: PROVISIONED
      ProvisionedThroughput: 
        ReadCapacityUnits: "5"
        WriteCapacityUnits: "5"
      AttributeDefinitions:
        -
          AttributeName: "Id"
          AttributeType: "S"
        -
          AttributeName: "Filtro"
          AttributeType: "S"
      KeySchema:
        -
          AttributeName: "Id"
          KeyType: "HASH"
        -
          AttributeName: "Filtro"
          KeyType: "RANGE"
Outputs:
  ApiEndPoint:
    Description: "EndPoint Api"
    Value: !Sub "https://${APITienda}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/"
  ClienteEndPoint:
    Description: "EndPoint Metodos Cliente"
    Value: !Sub "https://${APITienda}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/${ClientePath}"
  SucursalEndPoint:
    Description: "EndPoint Metodo Sucursal"
    Value: !Sub "https://${APITienda}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/${SucursalPath}"
Enter fullscreen mode Exit fullscreen mode

apirestsam.yml

Algo que diferencia a las plantillas SAM de las plantillas de CloudFormation es la siguiente linea que identifica la plantilla como SAM:

Transform: AWS::Serverless-2016-10-31

SAM utiliza algunas etiquetas diferentes para crear los recursos como:

AWS::Serverless::Api : Nos sirve para crear el API Gateway.

AWS::Serverless::Function: Nos sirve para crear la función Lambda y la integración con API Gateway (los recursos, los métodos, el stage y el despliegue).

Como podemos observar utilizar SAM para desplegar aplicaciones sin servidor es mucho mas practico que utilizar CloudFormation.

Teniendo nuestra plantilla debemos desplegarla en AWS. Primero debemos tener instalada la SAM CLI en nuestro equipo.

Una vez instalada SAM CLI vamos a validar que nuestra plantilla este construida correctamente con el siguiente comando.

sam validate --template apirestsam.yml

SAM Validate

Como vemos que nuestra plantilla esta correcta vamos a desplegarla con el siguiente comando:

sam deploy --template-file apirestsam.yml --stack-name apirestSAMIaC --capabilities CAPABILITY_IAM

Al ejecutar el comando vamos a ver en detalle como se están creando nuestros recursos. Van a ver algo similar a esto en su terminal:

SAM Deploy

Esto nos indica que nuestra API Rest fue desplegado con éxito.
Como lo hicimos en el despliegue de la plantilla de CloudFormation podemos ir a la consola de AWS y verificar que los recursos se hayan creado.

En este post podemos observar que se pueden utilizar diferentes herramientas para cumplir un mismo objetivo, sin embargo debemos buscar siempre la mas adecuada para cada caso, que en este es SAM, ya que la infraestructura a desplegar es sin servidor.

Como ejercicio pueden implementar la función Lambda para ingresar las sucursales e implementar un método de autenticación para validar el acceso a las métodos del API Gateway.

Me pueden encontrar en:

Camilo Cabrales

Referencias

CloudFormation

SAM

Oldest comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.