Desplegando python en AWS

presentacion online en https://monobot.github.io/pyaws_pybirras/#/

Héctor Alvarez


twitter: @monobotblog

email: monobot.soft@gmail.com Backend developer en cabaana

AWS no necesita presentaciones

Es el mayor proveedor de servicios cloud actualmente

Dispone de un enorme abanico de servicios, y cada año lanza varios.

Facturó alrededor de 6.300 millones de euros en 2015

cloud share 2016
cloud share 2016

Primeros pasos

Alta en aws

Hay que poner la tarjeta de credito, aunque para las primeras pruebas y durante un año hay muchos servicios que son gratis, cuando los servicios no sean gratis normalmente avisan

Tienen herramientas para consultar los costes previstos

Durante el proceso de alta descargar un .pem que deberás conservar, si lo pierdes no se puede recuperar hay que generar otro

¿Es barato?

Lo intentaremos debatir entre todos al final

aws cli

AWS dispone de una herramienta para la línea de comandos que admite (CASI) TODA la funcionalidad disponible en la "consola web"

Así que en breve puedes hacer lo que quieras desde la línea de comando (si sabes como hacerlo)

aws-cli está escrito en python (obviamente)

aws cli (ejemplos)

obtener ficheros (recursivamente) de un bucket determinado


aws s3 ls s3://mybucketname --recursive
                            

obtener el listado de servidores


aws ec2 describe-instances
                            

IAM

Es una de las herramientas principales de aws, es donde se establecen los permisos a los roles, y éstos (los roles) se pueden aplicar a usuarios o a servidores.

S3

S3 es el webservice para almacenaje y entrega de ficheros estáticos.

Aseguran disponibilidaddel 99,99%

Se almacenan en buckets (baldes) que están disponibles en una localización específica, no es lo mismo tener los ficheros en Irlanda que en Hong Kong

Para el caso de disponibilidad mundial también tienen un CDN que ellos llaman cloudfront

También tienen un servicio para ficheros de lento (muy muy lento) acceso glacier

Usar S3 desde django

Como es lógico es posible usar S3 desde django, puedes obviamente hacerlo tu a mano, con cada subida que haya una señal que lo haga por ti

Pero lo lógico es que cada vez que un usuario o servicio suba un fichero no lo haga al servidor (piensa en el caso de múltiples servidores) sino que lo haga directamente al bucket

Configurar como se suben lo ficheros media


class MediaStorage(S3BotoStorage):
    location = settings.MEDIAFILES_LOCATION
                            

Configurar el settings


AWS_STORAGE_BUCKET_NAME = 'BUCKET_NAME'
AWS_ACCESS_KEY_ID = 'xxxxxxxxxxxxxxxxxxxx'
AWS_SECRET_ACCESS_KEY = 'yyyyyyyyyyyyyyyyyyyyyyy'

MEDIAFILES_LOCATION = 'media'
MEDIA_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, MEDIAFILES_LOCATION)
DEFAULT_FILE_STORAGE = 'custom_storages.MediaStorage'
                            

Lo mismo se puede (y debe) hacer con los estáticos ¡para que tu servidor no sirva ningún fichero!

Para una descripción paso a paso y completa ver el tutorial

SQS

Es el servicio de colas, asegura solo y al menos una entrega de la tarea, si devuelve error se volverá a enviar

Para sqs amazon puedes usar el boto de amazon, pero tendrías que hacertelo tu a mano, yo recomiendo usar sqjobs

No hay tiempo de explicarlo entero, básicamente configuras una serie de tareas, y tienes un broker que se encarga de encolar las tareas y worker/s que se encarga/n de ejecutarlas a medida que llegan.

definir el broker


def sqs_broker():
    return create_sqs_broker(
        access_key=settings.SQJOBS_SQS_ACCESS_KEY,
        secret_key=settings.SQJOBS_SQS_SECRET_KEY,
        region_name='eu-west-1'
    )
                            

el mixin de las tareas


class LogJobMixin(Job):
    default_queue_name = settings.SQS_QUEUE

    def pre_run(self, *args, **kwargs):
        # que hacer antes de ejecutarse

    def post_run(self, *args, **kwargs):
        # que hacer después de ejecutarse

    def on_failure(self):
        # que hacer si falla

    def on_retry(self):
        # que hacer al reintentar
                            

definir una tarea:


class ChangePasswordEmail(LogJobMixin):
    name = 'ChangePasswordEmail'

    def run(self, *args, **kwargs):
        changePasswordEmail(*args, **kwargs)
                            

encolar una tarea:


from mysqs.brokers import sqs_broker
from mysqs import ChangePasswordEmail

broker = sqs_broker()

broker.add_job(
    ChangePasswordEmail,
    emp.user.id,
    schema_name=schema_name,
)
                            

Los argumentos que se envían a sqs deben ser serializables directamente, no puedes por tanto enviar modelos de django

Ya solo nos queda ver como corren los workers

convenientemente tenemos una tarea de django ya configurada


manage.py sqjobs sqs worker QUEUE_NAME
                            

EC2, VPC, ELB y ROUTE53

Son diferentes servicios pero están estrechamente relacionados

EC2

Los los servidores a los cuales puedes acceder mediante ssh

Existe un abanico muy amplio de servidores, generalistas, cpu, memoria, ancho de banda, etc...

Los servidores deben tener asociado al menos una seccion de disco virtual (normalmente ssd)

Regiones y zonas de disponibilidad

La nube de amazon se divide en regiones (uswest, useast, euwest, eueast, etc...) y cada region se divide al menos en 3 zonas de disponibilidad (availability zones)

debes tener claro donde localizas el servidor para que esté lo mas cerca posible de tu clientes

salvo contadas excepciones los servicios de amazon están localizados

VPC (Virtual Private Cloud)

Necesita gateway (un servidor ec2 dedicado al enrutado), la red virtual da un nivel de aislamiento necesario cuando tratas mas de una máquina para que el acceso interno sea fácil y posible

Es importante hablar ahora de IAM, es muy importante asignar a los servidores los roles que les permitan acceder al resto de servicios.

ELB (Elastic Load Balancer)

Se encarga de balancear la carga entre los diferentes servidores, de tal forma que éstos estén mas o menos al mismo nivel de trabajo.

El ELB se puede asignar a un grow group, es decir una configuración de EC2 que dependiendo de una serie de requisitos establecidos por nosotros aumenta automáticamente el número de servidores.

ROUTE53

El servicio de enrutado de amazon

Lo fantástico que tiene es que permite enrutar a una máquina específica o a un ELB, podemos hacer cosas como el 50% de la carga la pones aquí y el resto allá, o que la carga la reparta al servidor mas cercano, o por paises, etc...

cloud share 2016

Como pueden ver es un lio poder interactuar con los diferentes servidores y además a priori no sabemos las ips o cuantos servidores hay en un grow group por ejemplo, por ello nos tenemos que valer algun truco


def describe_instances(stage):
    aws_instances = local('aws ec2 describe-instances', capture=True)
    aws_dict = json.loads(aws_instances.stdout)
    # print aws_dict
    webserver_stage = {'hosts': [], 'user': ''}
    websocket_stage = {'hosts': [], 'user': ''}
    for res in aws_dict['Reservations']:
        for inst in res['Instances']:
            same_stage = False
            is_webserver = False
            is_websocket = False
            inst_ip = inst['PublicIpAddress']

            for tag in inst['Tags']:
                if tag['Key'] == 'Stage':
                    same_stage = (
                        tag['Key'] == 'Stage' and tag['Value'] == stage
                    )
                elif tag['Key'] == 'Use':
                    is_webserver = (
                        tag['Key'] == 'Use' and tag['Value'] == 'Webserver'
                    )
                    is_websocket = (
                        tag['Key'] == 'Use' and tag['Value'] == 'Websockets'
                    )
                elif tag['Key'] == 'username':
                    username = tag['Value']

            if same_stage:
                if is_webserver:
                    webserver_stage['hosts'].append(str(inst_ip))
                    webserver_stage['user'] = username
                elif is_websocket:
                    websocket_stage['hosts'].append(str(inst_ip))
                    websocket_stage['user'] = username

    return webserver_stage, websocket_stage


def dev():
    env.host_type = 'development'
    env.webserver_hosts, env.websocket_host = describe_instances(env.host_type)

    env.key_filename = get_pempath('mykey.pem')
    env.DB_ENDPOINT = 'yourendpoint.rds.amazonaws.com'
    env.DB_NAME = 'dev'
                            


def deploy():
    print(('this command will pull {red}{servername}\'s{black} '
        'current branch to the latest version and restart the services ...'
        ).format(
            red=ShellFormat.WARNING,
            servername=env.host_type.upper(),
            black=ShellFormat.ENDC,
            )
        )
    resp = raw_input('Type the server\'s name to confirm: ')

    if resp == env.host_type:
        env.migrate = True

        websocket_host = env.websocket_host != {'hosts': [], 'user': ''}
        if websocket_host:
            print 'WEBSOCKET SERVER: migrate the database and deploy'
            execute(real_deploy,
                    env.websocket_host['user'],
                    hosts=env.websocket_host['hosts'],
                    )

            print 'WEBSERVERS: just deploy'
            env.migrate = False
            execute(real_deploy,
                    env.webserver_hosts['user'],
                    hosts=env.webserver_hosts['hosts'],
                    )
        else:
            print 'WEBSERVERS: migrate the database and deploy'
            execute(real_deploy,
                    env.webserver_hosts['user'],
                    hosts=env.webserver_hosts['hosts'],
                    )
    else:
        print('... exiting')
                            


def real_deploy(user):
    env.user = user
    with cd(env.mydir):
        run('git stash')
        run('git pull')
        if env.migrate:
            run(
                ('{python} manage.py migrate_schemas '
                    '--settings=special.settings.{host_type}'
                    ).format(
                        python=env.python_int,
                        host_type=env.host_type
                        )
                )

        control_service = 'supervisorctl'
        if env.host_type in ['development', 'gold']:
            control_service = 'circusctl'

        run('sudo {} reload'.format(control_service))
        sleep(3)
        run('sudo {} status'.format(control_service))
                            

Ahora

¿Es barato?

¿Ustedes que opinan?