Aprendiendo AWS: Lambda usando VSCode y Python

Continuando con la serie sobre AWS, en este caso he decidido jugar un poco con una de las plataformas del momento, Lambda. Para ello he construido una API con Python para leer y escribir datos de una base de datos, en local y con diferentes capas de test.

¿Qué es Lambda?

Lambda es un producto de AWS que permite ejecutar trozos de código (funciones) de manera aislada de otras aplicaciones. Esto aporta una capa extra de abstracción con todo lo bueno y malo que ello implica, y además únicamente se factura por el tiempo en el que la función se está ejecutando.

Se puede interactuar con Lambda utilizando el editor en la nube que ofrecen, subir el código a S3 usando un formulario, o utilizar sistemas como SAM local para automatizar todo el proceso, y este último es el que se ha usado en este artículo.

El objetivo

Para este pequeño proyecto, el plan consistía en crear al menos un par de funciones que expusieran una API REST, escribir en una base de datos, definir todo lo posible mediante código y configuración y que el despliegue se realizara utilizando la consola.

El ejemplo es un banco con cinco funciones, crear, depositar, retirar, ver saldo, y hacer una transferencia.

Escribiendo la primera función

El lenguaje utilizado en este caso ha sido Python, así que parte del reto ha sido aprender a escribir utilizando la sintaxis del lenguaje de la serpiente:

def setup(event, context):
    payload = json.loads(event['body’])
    account = payload['id']
    bank = Bank()
    bank.save_account({"id": account, "balance": 0})
    return {
        "statusCode": 201
    }

En Lambda, y para el caso particular de Python, se define una función como un método que acepta dos parámetros, event que contiene los datos que se le pasan a la función, y context que contiene información relativa a la ejecución de la misma.

En este ejemplo se puede ver los valores que se pasan a esta función dentro de event

{'httpMethod': 'PUT', 'body': '{"id": "test_alice"}', 'resource': '/account', 'requestContext': {'resourcePath': '/account', 'httpMethod': 'PUT', 'stage': 'prod', 'identity': {'sourceIp': '127.0.0.1:61479'}}, 'queryStringParameters': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'keep-alive', 'Content-Length': '20', 'Host': 'localhost', 'User-Agent': 'python-requests/2.18.4', 'X-Forwarded-Port': '', 'X-Forwarded-Proto': ''}, 'pathParameters': None, 'stageVariables': None, 'path': '/account', 'isBase64Encoded': False}

Para poder continuar, es necesario interpretar los parámetros. En este caso, el valor body es un JSON con la información de la cuenta, y para ello se crea un objeto a partir de la cadena de texto utilizando json.loads(event['body’])

Esto permite interactuar con los datos que se pasan a la función, que en este caso lo que hace es guardar la nueva cuenta en la clase Bank creada para abstraer toda la información relativa a la base de datos.

Finalmente, la función ha de devolver un objeto que contenga, al menos un campo con el estado HTTP en statusCode, que en este caso es 201 (Created).

Utilizando la misma lógica se puede hacer las operaciones de depósito, retirada, saldo y transferencia, aunque para ello puede resultar interesante abstraer la función que recibe el evento de la lógica de la función, usando una función que se llamará dispatch:

def dispatch(event, context):
    requestContext = event["requestContext"]
    resourcePath = requestContext["resourcePath"]
    method = requestContext["httpMethod"]

    if(method == "GET"):
        payload = event['queryStringParameters']
    else:
        payload = json.loads(event['body'])
    try:

        if(resourcePath == "/account"):
            if(method == "PUT"):
                setup(payload)
                return {
                    "statusCode": 201
                }

            elif(method == "GET"):
                return {
                    "statusCode": 200,
                    "body": get_balance(payload)
                }
        ...

    except Exception:
        return {
            "statusCode": 500
        }

Esta función permite leer los parámetros, interpretar la ruta y llamar a la función específica de la misma, así como hacer un pequeño tratamiento de errores.

Integrando con DynamoDB

Una vez que están todas las funciones definidas, el siguiente paso es escribir la clase Bank que será la que escriba información en la base de datos.
Mantener estas responsabilidades separadas permite, en primer lugar, una mejor capacidad de pruebas, y en segundo lugar, hace al código de la función independiente del proveedor de bases de datos.

import boto3
import os
import uuid

class Bank:

    def __init__(self):
        endpoint = os.environ.get("ENDPOINT")
        if(endpoint is None):
            self.dynamodb = boto3.resource('dynamodb')
        else:
            self.dynamodb = boto3.resource('dynamodb', endpoint_url=endpoint)

        self.accounts = self.dynamodb.Table("Accounts")
        self.transactions = self.dynamodb.Table("Transactions")

    def get_account(self, account_name):
        try:
            result = self.accounts.get_item(Key={'id': account_name})
            return result['Item']
        except:
            return None

    def log_transaction(self, transaction):
        transaction["id"] = str(uuid.uuid1())
        self.transactions.put_item(Item=transaction)

    def save_account(self, account):
        self.accounts.put_item(Item=account)

El único detalle destacable de esta clase, es que comprueba si existe un endpoint para DynamoDB definido, esto permitirá utilizar la misma clase para los tests.

Probando la función

Una vez escrito el código de las funciones y la conexión con la base de datos, el siguiente paso es probar que todo funciona como se espera.

Configurando la base de datos

Para probar la interacción con la base de datos se puede, o bien configurarla utilizando el sdk de aws y el comando aws configure o bien utilizando una base de datos local, que podemos arrancar utilizando el siguiente comando:

java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar --inMemory

Esto proporciona una base de datos de DynamoDB en local lista para utilizar. La creación de tablas, en este ejemplo, se realiza como parte de los test unitarios:

table = dynamodb.create_table(
    TableName="Accounts",
    KeySchema=[
        {
            'AttributeName': 'id',
            'KeyType': 'HASH'
        }],
    AttributeDefinitions=[
        {
                'AttributeName': 'id',
                'AttributeType': 'S'
        }],
    ProvisionedThroughput={
        'ReadCapacityUnits': 5,
        'WriteCapacityUnits': 5
})
            table.meta.client.get_waiter('table_exists').wait(TableName='Accounts')

Probando las funciones

Con la base de datos funcionando y las tablas creadas, llega el momento de escribir las diferentes pruebas, como esta, que comprueba que un ingreso ha sido efectivo:

...

def test_deposit(self):
    function.setup({'id': "Bob"})
    function.deposit({'id': "Bob", "amount": 10.0})
    self.assertEqual(function.get_balance({'id': "Bob"}), 10)

Para ejecutar los tests es tan sencillo como escribir python function-tests.pydesde la consola de comandos.

Configurando Lambda

Para convertir el código de las funciones en una API que se pueda consultar vía HTTP se ha recurrido a SAM (Serverless Application Model) local, un conjunto de herramientas para definir, probar y desplegar funciones Lambda.

La plantilla

Una plantilla de SAM no es más que un subconjunto de una plantilla de CloudFormation, que en un artículo anterior comentaba que es una manera de automatizar la creación y actualización de recursos dentro de AWS.

Para este ejemplo la plantilla tiene el siguiente aspecto:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: 'A simple banking app built on lambdas'

Resources:
  Bank:
    Type: AWS::Serverless::Function
    Properties:
      Handler: function.dispatch
      CodeUri: .
      Runtime: python3.6
      Events:
        CreateAccount:
          Type: Api
          Properties:
            Path: '/account/new'
            Method: put
        GetBalance:
          Type: Api
          Properties:
            Path: '/account'
            Method: get
        Deposit:
          Type: Api
          Properties:
            Path: '/account/deposit'
            Method: post
        Transfer:
          Type: Api
          Properties:
            Path: '/account/transfer'
            Method: post
        Withdraw:
          Type: Api
          Properties:
            Path: '/account/withdraw'
            Method: post
      Policies:
        - Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - 'dynamodb:PutItem'
                - 'dynamodb:GetItem'
              Resource: 'arn:aws:dynamodb:*:*:*'

En esta plantilla se define:
– El handler, o función que responderá a la llamada
– Los eventos que dispararán la llamada (varios eventos pueden disparar el mismo handler)
– Las políticas que se aplican (en esta caso necesitamos permisos de lectura y escritura para la base de datos).

Validando y probando la plantilla con SAM

Una vez definidos todos los valores para la plantilla, se puede ejecutar SAM para comprobar que la plantilla es válida:

sam validate

El cliente SAM permite, además, probar la definición de la plantilla de manera local:
sam local start-api

Este comando inicializa un servidor en el puerto 3000 con el que se pueden probar las funciones desde un cliente HTTP (en este ejemplo he utilizado Postman) y ver como la API responde a las diferentes llamadas.

Pruebas de integración

Para comprobar que todo funciona bien antes de subir la plantilla a AWS, he creado un conjunto de tests un tanto diferentes, esta vez para comprobar que los datos se transforman correctamente, llamando al cliente de localhost.

def test_deposit(self):
    r = requests.get('http://localhost:3000/account?id=test_bob')
    initial_bob_balance = float(r.text)

    r = requests.post('http://localhost:3000/account/deposit', data =  json.dumps({'id':'test_bob', 'amount': 100}))
    self.assertEqual(200, r.status_code)

    r = requests.get('http://localhost:3000/account?id=test_bob')
    new_balance = float(r.text)

    self.assertEqual(new_balance, initial_bob_balance + 100)

Para ejecutar los tests es tan sencillo como escribir python integration-tests.pydesde la consola de comandos.

Desplegando la plantilla

Finalmente queda empaquetar la función y desplegarla en la consola de AWS. En primer lugar se empaqueta la función en un fichero zip, se guarda en una carpeta (bucket) de S3 y se actualizan las referencias en el fichero que se ha denominado package.yaml:

sam package --template-file template.yaml --s3-bucket my-bucket --output-template-file packaged.yaml

En segundo lugar se invoca a CloudFormation para crear todos los recursos definidos en el paquete, incluyendo el código fuente y el conjunto de permisos:

sam deploy --template-file packaged.yaml --stack-name my-stack-name --capabilities CAPABILITY_IAM

Una vez se haya desplegado el paquete, se puede ir a la consola de AWS y ver cómo se ha desplegado la función:

Captura de pantalla 2018-05-12 a las 21.00.03

Al hacer click, se puede ver además los diferentes endpoints que API Gateway ha creado. así como las diferentes opciones:

Captura de pantalla 2018-05-13 a las 9.54.07

Con esto la función creada está lista para ser ejecutada.

En resumen

Eh este artículo se ha definido una función Lambda de manera local, utilizando tantas capas de abstracción como hayan sido necesarias. Se han creado diferentes tipos de test para probar los diferentes puntos de integración, y finalmente se ha empaquetado y desplegado la función desde la línea de comandos.

El código está disponible en GitHub.

Sin embargo, se han quedado algunas cosas fuera del alcance del artículo, y quedan como sugerencia al lector para continuar experimentando:

  • Inyección de dependencias y Mocking para hacer tests unitarios
  • Integración con un sistema como Jenkins para ejecutar tests
  • Crear la base de datos utilizando la plantilla de SAM
  • Uso de frameworks como Serverless que agregan una capa de abstracción incluso mayor, aislando de las implementaciones de AWS, Azure, Google Cloud o IBM.

¿Y tú, has hecho alguna prueba con Lambdas u otra plataforma Serverless?

Herramientas utilizadas

  • SAM local para probar la función en local, así como para desplegarla en AWS.
  • Visual Studio Code para editar el código y para interactuar con la API de SAM local usando su consola integrada.
  • DynamoDB Local para emular una base de datos de DynamoDB.
  • Boto 3 el SDK oficial de AWS para Python.
  • Postman para probar las diferentes APIs tanto locales como remotas.

Más documentación y ejemplos

Aprendiendo AWS: Nube privada Virtual o VPC

Desde hace ya unos días estoy empezando a preparar el examen de certificación de AWS Solutions Architect, y eso me permite aprender sobre los diferentes componentes que forman la plataforma cloud de Amazon. En este artículo veremos uno de los pilares de estos componentes, la nube privada virtual o VPC.

Hasta hace relativamente poco, mi idea era que en cualquier proveedor de nube, tenías máquinas virtuales y servicios, y podías realizar ciertos ajustes para detectar desde donde se puede acceder a tu instancia, y poco más. Uno de los componentes al que había prestado poco o nulo interés era el concepto de VPC (llamado también Virtual Network en Azure).

Básicamente una VPC nos permite crear nuestra propia topología de red en la nube como si tuviéramos nuestro propio CPD. Con topología nos referimos de la posibilidad de crear una red, diferentes subredes, decidir cómo se conectan entre ellas, cómo se conectan a internet, y a qué recursos tienen acceso.

Esto también nos permite tener diferentes recursos en diferentes subredes, y poder aplicar directivas de seguridad y control de acceso a nivel de subred, en vez de tener que aplicarlas a recursos individuales.

Cuando gestionamos redes privadas en AWS podemos hacer uso de diferentes componentes, entre los que destacan:

  • Subnets: Permiten aislar varios recursos dentro de la misma VPC (i.e. Servidor Web, Base de datos)
  • Internet Gateway: Necesaria para que una VPC pueda acceder a internet.
  • Elastic IP Address: Necesaria para poder servir tráfico a internet desde nuestra VPC. Podemos tener direcciones IP elásticas asociadas a una VPC o a un recurso específico (una instancia de EC2, por ejemplo).
  • Route Tables: Permiten definir una tabla de origen y destino, y direccionar el tráfico que proviene de una subred tanto a la red interna o al Internet Gateway, para dar acceso a internet.
  • Security Groups: Asociado a una instancia, permite establecer que rango de IPs puede acceder a puertos específicos. Por ejemplo, un servidor web puede permitir tráfico entrante para todo internet en el puerto 80, pero sin embargo limitar el tráfico de conexiones SSH (puerto 22) a máquinas específicas.
  • NAT Gateway / NAT Instance: Permiten que una subred tenga acceso a internet y limitar el acceso a la misma desde internet. Se puede configurar como servicio o manualmente utilizando una máquina virtual dedicada.

Recursos

Con la vista en 2018, se aceptan sugerencias

Un año más es tiempo de echar la vista atrás, repasar qué ha sido del blog en 2017, revisar retos y plantear nuevos para 2018.

Este año que acaba de terminar ha resultado más exigente de lo esperado para mí, tanto profesional como personalmente, una de las más claras consecuencias de ello ha sido que he descuidado el blog en ese período.

Por otra parte, a principios del pasado año cometí el que creo que es un error (voy iterando de un lado a otro del espectro) de poner objetivos ambiguos, con lo cual 365 días después no tengo del todo claro que estén cumplidos o no.

Dichos objetivos eran:

  • Seguir leyendo, estudiando y compartir las notas de lo leído.
  • Seguir corriendo y moviéndome, este trabajo nos hace sedentarios.
  • Seguir aprendiendo abstracciones, y hablar de ellas en el blog.
  • Mejorar la fluidez con herramientas de UNIX, comandos, pipes, python para scripts, etc.
  • Contribuir de vuelta a la comunidad vía charlas o artículos.

En general he leído algo (22 libros, algunos de ellos re-leidos) pero poca literatura técnica, he acudido a menos carreras que el pasado año aunque las marcas han mejorado un poco, no he podido sacar tiempo para sentarme a estudiar abstracciones y/o herramientas, más allá del uso diario.

Por otra parte, este año mi presencia en la comunidad se ha reducido a participar en un tres o cuatro eventos como ponente y moderar algunas discusiones como parte de open spaces. En el blog, me he centrado un poco más en productividad personal, y algo de desarrollo full stack.

Para 2018 quisiera volver a poner objetivos “a la antigua”. es decir, específicos, medibles, alcanzables (al menos en teoría), relevantes y limitados en el tiempo (SMART), así que aquí está mi lista:

  • Prepararme y presentarme al examen de certificación AWS Solutions Architect.
  • Crear un proyecto personal en un nuevo lenguaje.
  • Leer, al menos, 6 libros técnicos NUEVOS.
  • Hacer un MOOC con Coursera o EdX.
  • Publicar un artículo al mes.

Salvo el primer objetivo, los demás siguen siendo algo ambiguos, pero, como bien describe el título, se aceptan sugerencias.

Feliz año nuevo, querido lector.

Otros tipos de inyección de dependencias

No es la primera vez que hablo en este blog sobre inyección de dependencias, el patrón que nos permite pasar todas las dependencias que una clase necesita en el constructor en vez de como argumentos en métodos o utilizar clases estáticas.

En el caso de Java, donde últimamente paso la mayoría de mi tiempo, contamos con Spring y Guice como las maneras más conocidas de inyectar dependencias, veamos un ejemplo de Spring.

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ClassWithDependencies {
    private final DependentClass dependentInstance;

    public void doSomethingWithDependency(){
        dependentInstance.doSomething();
    }
}

@Component
public class DependentClass {
    public void doSomething(){
        System.out.println("Doing something")
    }
}

En este ejemplo, con las anotaciones @Component de Spring y @RequiredArgsConstructor de Lombok (un pre-procesador que nos permite agregar setters, getters y constructores a Java) podemos definir ClassWithDependencies como una clase que recibe DependentClass como parámetro del constructor, y luego operar sobre ella.

En artículos anteriores hemos hablado de otros sistemas como el de ASP.net vNext o incluso crear el nuestro propio. Creando un motor de inyección de dependencias con C#

Es importante destacar que en este ejemplo concreto, la inyección construye una nueva instancia de la clase cada vez que se inyecta, siendo la más sencilla de las opciones de inyección.

Una instancia por ejecución (Singleton)

Sin embargo, podemos personalizar aún más el tipo de inyección que hacemos. Volviendo a nuestro ejemplo anterior, supongamos que DependentClass contiene información de la máquina (memoria, procesador, etc) en la que se ejecuta, y por tanto no cambiará a lo largo de la vida de nuestra aplicación.

En este caso, una nueva instancia de cada clase sería innecesario o incluso costoso, dependiendo del tipo de inyección, para lo cual, en la definición del componente, especificamos qué tipo de inyección queremos

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ClassWithDependencies {
    private final MachineContext machineContext;

    public void doSomethingWithDependency(){
        machineContext.getProcessor();
    }
}

@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class MachineContext {
    public String getProcessor() {
        return "Intel";
    }
}

Comparemos esto con definir un patrón singleton de manera manual:

public class ClassWithDependencies {
    private final MachineContext dependentInstance;

    public void doSomethingWithDependency(){
        MachineContext.getInstance().getProcessor();
    }
}

public class MachineContext {
    private static MachineContext instance;

    public String getProcessor() {
        return "Intel";
    }

    private MachineContext() {
    }

    public static MachineContext getInstance(){
        if(instance == null){
            instance = new MachineContext();
        }
        return instance;
    }
}

La principal diferencia de usar Singleton en inyección respecto al singleton estándar que podemos ver en el ejemplo anterior es con inyección de dependencias tenemos las ventajas de singleton y mantenemos la capacidad de probar nuestro sistema, ya que en una prueba de ClassWithDependencies podríamos simular o utilizar un mock de MachineContext (por ejemplo si queremos probar cómo se comporta nuestra aplicación en distintos contextos).

Una instancia por petición (Request)

Una variante de Singleton especialmente para web, ocurre cuando el contexto depende de la petición o la sesión. Para este ejemplo, tenemos una función que se dedica a llevar un registro de las excepciones, y queremos, para cada excepción, registrar además el Id del usuario al que le ocurrió la excepción.

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class GlobalExceptionHandler {
    private final Request request;

    public void handleException(Exception ex){
        logger.error(request.getUserId(), ex.getStacktrace())
        machineContext.getProcessor();
    }
}

@Component
@Getter
@Setter
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class Request {
    private String userId;
}

Este formato nos permite rellenar la clase Request en la capa del controlador de de nuestra aplicación, y en la otra directamente preguntar por el estado, sin necesidad de tener que pasar el objeto request a lo largo de toda la cadena de llamadas hasta la captura de esta excepción.

Con ello, evitamos que nuestras clases tengan una referencia al objeto únicamente por tener que pasarlo a la siguiente capa, se simplifica el número de parámetros que manejamos y podemos referirnos al contexto siempre que lo necesitemos.

Conclusiones

En este artículo hemos repasado inyección de dependencias de manera tradicional y además hemos visto las ventajas de utilizar construcciones como Singleton y Request.

Y tú, usas estas construcciones cuando desarrollas aplicaciones?

En Pamplona Software Craftsmanship 2017

Este fin de semana he tenido la oportunidad de participar en un evento diferente orientado a profesionales de la industria del software en la ciudad de Pamplona, en Navarra, en el que se combinaban dos aproximaciones:

  • Por una parte, una conferencia tradicional, en la que los ponentes y las sesiones se conocen de antemano.
  • Por otra, un open space, en el que las sesiones se proponen, presentan y votan por parte de los asistentes.

Durante dos días, más de una centena de desarrolladores, ingenieros de software, programadores, agilitas, craftsmen, o profesionales de la industria, independientemente de la etiqueta que tengamos en nuestro lugar de trabajo o en nuestro CV, hemos debatido sobre la profesión, lo que nos motiva a hacer lo que hacemos, los problemas a los que nos enfrentamos y las soluciones a las que hemos llegado.

Con unas 40 sesiones entre open space y ponencias, la temática era muy variada, y pude tomar bastantes notas, entre otras cosas, sobre:

  • Cómo con TDD podemos aprender a trabajar con un lenguaje como Elixir.
  • Cuales son las responsabilidades del arquitecto del software, como todos somos arquitectos en cierta manera y cómo conectar con los responsables de negocio en nuestra empresa o nuestros clientes.
  • Cómo algunos han conseguido llevar soluciones de entrega continua a sectores como la fabricación de vehículos.
  • En qué aspectos de la experiencia de usuario deberíamos fijarnos, como mínimo, a la hora de desarrollar sistemas de información en general e interfaces de usuario en particular.
  • Cómo aprender a enseñar, de qué maneras podemos ayudar, especialmente a los que están empezando, a que entiendan y usen buenas prácticas desde el principio.
  • Por qué perdemos la motivación por la comunidad o por mejorar como profesionales, y cómo podemos recuperarla.
  • Cómo nos organizamos, tanto profesionalmente en el caso de ser freelance como personalmente.
  • Qué libros, charlas o eventos, han influido en nuestra manera de trabajar, nos han enseñado nuevas maneras de ver problemas o nos han inspirado.

Tuve además, la oportunidad de facilitar dos debates, algo que supuso otra experiencia nueva para mí por la cantidad de conversaciones que surgen, cómo se ramifican los temas y la necesidad de ser capaces de reconducir la conversación cuando se aleja demasiado del objetivo del debate.

El formato del evento y los tiempos, con descansos entre todas las sesiones, permitían un amplio margen para conversaciones informales, y eso invitaba a acercarte e iniciar una conversación con gente con quien tal vez no habrías interactuado en una conferencia “tradicional”, ya sea por la cantidad de asistentes como por esa separación entre “ponente” y “asistente”.

De manera paralela, como no solamente de software vive el craftsman, el segundo día por la mañana el tiempo me dio un respiro, me puse mis zapatillas y salí a correr un par de millas alrededor de la ciudad, algo que estoy intentando convertir en una tradición cuando voy a un evento.

Para mí fue toda una experiencia, el viernes comenzó en una sala donde, salvo excepciones, estaba rodeado de desconocidos, y vuelvo el domingo sintiéndome parte de otra comunidad, con muchísima gente que se enfrenta a un montón de problemas interesantes y con quien he compartido experiencias con una cerveza, un café, una copa de vino y algo de comer.

Desde mi humilde blog, no quería perder la oportunidad de agradecer:

  • A la organización por el impagable trabajo que han hecho a lo largo de todos estos meses desde que se convoca hasta que sucede.
  • A todos con los que compartí experiencias, de los que aprendí trucos, y que escucharon mis batallitas,
  • Finalmente a mi familia, por entender la importancia de participar en estos eventos.

Nos vemos en la siguiente!

Materiales de la charla sobre desarrollo Full Stack

El pasado 17 de marzo estuve con los chicos del DotNetClub de la UAM hablando sobre desarrollo Full Stack en la Escuela Politécnica Superior.

Desde un punto de vista personal (y sesgado) discutimos la definición de stack, la importancia de conocer las diferentes partes del mismo, los diferentes modos de ver stacks, las piezas móviles con las que nos podemos encontrar, así como diferentes arquitecturas, utilizando comida italiana como ejemplo, raviolli, lasagna, risotto, pizza, panetone…

Las slides de la charla las tienes disponibles en SlideShare y a continuación.

Gracias por asistir!