Lucas Simon

Web Developer. lucassrod@gmail.com

Série API em Flask - Parte 15 - Automatizando o processo de deploy com Fabric

Eae beleza galera. O objetivo deste artigo é aprender a utilizar o Fabric para para automatizar essa tarefas repetitivas e não termos que ficar entrando no servidor e fazendo todos os passos manuais para deploy. Vem comigo.

Para entendimento, segue os capítulos que serão abordados.

O repositório com todo o código fonte esta aqui. Os capítulos estão em branches.

O que vamos automatizar

Para o propósito deste artigo e da necessidade de não ter que fazer todo o processo manual, vou utilizar o Fabric para cumprir os seguintes passos:

  1. Conectar no(s) servidor(es) remoto(s)

  2. Entrar no diretório do aplicativo e executar um git pull && git checkout

  3. Instalar as bibliotecas do aplicativo pip install

  4. Executar migrations. Se tiver é claro.

  5. Reiniciar os serviços do supervisor e nginx

Configurações no servidor

root@teste-devops:~# groupadd fabric

Adicionando o usuário ao grupo criado:

root@teste-devops:~# usermod -a -G fabric apiflask

Entendendo o sudoers

Antes de abrir o arquivo /etc/sudoers é preciso dar permissão de escrita. Por padrão ele é setado somente com a permissão de leitura.

root@teste-devops:~# chmod 600 /etc/sudoers

Em seguida podemos abrir o arquivo, vim /etc/sudoers e adicionar as seguintes configurações.

# ....

# User privilege specification
root	ALL=(ALL:ALL) ALL

# Allow members of group sudo to execute any command
%sudo	ALL=(ALL:ALL) ALL


%fabric ALL=(ALL) NOPASSWD: /usr/bin/supervisorctl, /usr/sbin/service

A sintaxe para configurar os privlégios são:

user/group host=users:groups tags commands
  • user/group - usuário ou grupo a qual estamos setando a permissão de sudo.

  • host - lista de hosts que podem executar o sudo.

  • users:groups - lista de usuarios e grupos.

  • tags – lista de configurações como NOPASSWD.

  • commands – comandos que aquele usuário ou grupo podem executar.

No nosso caso temos:

%fabric ALL=(ALL) NOPASSWD: /usr/bin/supervisorctl, /usr/sbin/service

O % representa que estamos indicando um grupo seguido do nome do grupo fabric criado anteriormente. O ALL=(ALL) representa todos hosts, usuários e grupo de usuários. O NOPASSWD: representa que não será necessário se autenticar através de senha, ja que, configuramos nosso servidor para se autenticar via SSH e nem criamos uma senha para ele. E por úlimo temos limitamos os comandos, /usr/bin/supervisorctl, /usr/sbin/service que aquele grupo pode executar com o sudo.

Depois disso basta logar em outro terminal com o usuário apiflask e testar as configurações.

O Fabric

De acordo com a definição do http://www.fabfile.org/ o Fabric é uma biblioteca desenvolvida para executar comandos shell remotamente através do SSH.

Ele não chega a ser uma ferramenta de configuração completa, com controle de estados de pacotes como o Ansible porém conseguimos criar tarefas que automatiza o processo de um servidor remoto. Basta usar de acordo com a necessidade.

fabric1 x fabric2 x fabric3

O Fabric 1.x foi desenvolvido em cima da versão do Python 2.7 e após mais de 10 anos de uso e estabilidade do Python 3.x a biblioteca Fabric foi reescrita com a versão 3.x do python e por ter tido grandes mudanças deu-se o nome do pacote de fabric2.

Ja o Fabric3 é um fork do Fabric1x a qual a equipe mantenedora do fabric original não faz parte. Logo as próximas versões estarão no pacote fabric2.

Para mais detalhes

Criando o fabfile

Portanto vamos instalar o fabric, pip install fabric==2.4.0, com o virtualenv ativado é claro.

A partir de agora na raíz do projeto criamos nosso arquivo fabfile.py e vamos criar nossa primeira task.

# -*- coding: utf-8 -*-

from fabric import Connection
from invoke import task

def whoami(c):
    """
    Print the envirioment and user
    """
    c.run('whoami')

@task
def uname(c):
    """ Prints information about the host. """
    c.run('uname -a')
    whoami(c)

O parâmetro c da função whoami e da task uname correspondem ao contexto atual a qual o fabric se encontra.

Por exemplo ao executarmos o comando abaixo ele irá pegar os dados minha maquina, no caso o contexto atual.

$ fab uname
Linux notebook 4.15.0-39-generic #42-Ubuntu SMP Tue Oct 23 15:48:01 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
lucas

Porém ao executar o mesmo comando com a opção --hosts, temos uma outra saída de resultados:

$ fab --hosts apiflask@165.227.124.70 uname
Linux teste-devops 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64 GNU/Linux
apiflask

Alguns comandos básicos são:

  • fab: Para exibir um help do comando

  • fab --hosts ou -H: Lista de hosts separados por virgula

  • fab --list ou -l: Exibe a lista de tasks, possuem o decorator @task do fabfile.py

Exemplo:

$ fab -l
Available tasks:

  deploy   Deploy the code to context
  status   Status supervisor and webserver
  stop     Stopping supervisor
  uname    Prints information about the host.

Sem mais delongas, vamos ao arquivo fabfile.py completo. Observe bem os comentários em cada função ou task.

  • c.run: Executa um comando shell. Utilizo ele para chamar o git, ou pip por exemplo.

  • with c.cd: Altero para o diretório especificado.

  • c.sudo: Executo um comando como sudo.

# -*- coding: utf-8 -*-

from fabric import Connection
from invoke import task

# Crio constantes referente as confgurações realizadas no servidor
SITE_DIR = '~/sites/flask-api-users'
VENV = '~/venvs/'
PYTHON_BIN = VENV + 'bin/python'
PIP_BIN = VENV + 'bin/pip'


def whoami(c):
    """
    Print the envirioment and user
    """

    # Executo o comando whoami
    c.run('whoami')


def git(conn, cmd):
    """
    Create a method to execute git on the server
    """
    # Altero para o diretorio SITE_DIR e executo o comando git
    with conn.cd('{}'.format(SITE_DIR)):
        conn.run('git {}'.format(cmd))


def checkout(conn, branch='master', tag=None):
    """
    Run git checkout to branch or tag
    """
    if tag:
        git(conn, 'checkout {}'.format(tag))

    else:
        git(conn, 'checkout {}'.format(branch))


def pull(conn):
    """
    Run git pull command on repository
    """
    git(conn, "pull")


def fetch(conn):
    """
    Run git pull command on repository
    """
    git(conn, "fetch")


def sudo(conn, cmd):
    conn.sudo(cmd)


def install_requirements(conn, env='prod'):
    """
    Install all requirements by the server
    """
    # Crio um comando para executar o pip de acordo
    cmd = "{} install -r requirements/{}.txt".format(PIP_BIN, env)
    with conn.cd('{}'.format(SITE_DIR)):
        conn.run(cmd)


@task
def uname(c):
    """ Prints information about the host. """
    c.run('uname -a')
    whoami(c)


def restart(conn):
    """
    Restart supervisor and webserver
    """
    # restarto o supervisor e o nginx
    conn.sudo("supervisorctl restart api_users:gunicorn_api_users")
    conn.sudo("service nginx restart")


@task
def status(c):
    """
    Status supervisor and webserver
    """
    c.sudo("supervisorctl status api_users:gunicorn_api_users")
    c.sudo("service nginx status")


@task
def stop(c):
    """
    Stopping supervisor
    """
    c.sudo("supervisorctl stop api_users:gunicorn_api_users")


@task
def deploy(c, tag=None):
    """
    Deploy the code to context
    """

    # 1 - Atualizo meu repo com um git pull
    fetch(c)

    # 2 - Altero para branch master ou para uma tag
    if tag:
        checkout(c, tag)
    else:
        checkout(c)

    # 3 - Instalo os requirements, por padrão, de prod.txt
    install_requirements(c)

    # Restarto o serviço.
    restart(c)

Resultado

Vamos ver nosso fab deploy em ação:

Do a request in container