TDC 2015
          Matando um Monolítico Django: de Pluggable Apps aos Microservices
          
          Bernardo Fontes
          
          São Paulo/SP
          25 de Julho de 2015
        
        
        
        
        
          ## Roteiro
          - ### Nosso Cenário
          - ### Microservices
          - ### Domain Driven Design
          - ### Implantação
          - ### Dúvidas
        
        
        
          ## Nosso Cenário
          ### Sistema para um empresa de **medicina do trabalho** realizar atendimentos de **medicina ocupacional** por todo o Brasil e fornecer uma **análise inteligente** sobre o perfil dos colaboradores de uma empresa.
        
        
            Maaaaaaas...
            Tudo começou só com uma filinha:
          
        
        
            Hoje em dia...
            
          
        
        
            ## Histórico do Projeto
            - ### + de **5 anos**
            - ### + de **10 devs** passaram
            - ### Python e **Django 1.4**
            - ### Hoje: 4 devs e **únicos Pythonistas**
            - ### Empresa com **12 devs**
        
        
            ## Histórico do Projeto
            - ### Em **Outubro de 2014**
            - ### 3 devs
            - ### Django com **uma única app**
            - ### 3k LOC de **models.py**
            - ### ~3k LOC de **views.py**
            - ### ~2,5k LOC de **services.py**
        
        
            ## Menos de 50% de coverage
            
        
        
            ## Infra do Projeto
            - ### 9 filials == 9 máquinas
            - ### 1 ambiente de clientes
            - ### 1 única máquina do banco
            - ### **11 máquinas**
        
        
            ## $ fab all_hosts deploy
            
        
        
            ## Cliente
            - ### **Novos módulos** no sistema
            - ### **Melhorias** nos antigos
            - ### Correção de **bugs**
        
        
            
        
        
            ### Estudamos
            
        
        
            Estudamos
            
        
        
            ## Microservices
            Um estilo arquitetural para o desenvolvimento de **serviços enxutos** em que cada um possa ser **executado em um processo póprio** e se comunicando através de mecanismos de fácil implementação como o protocolo HTTP. Esses serviços são **construídos focando capacidades de negócio** e devem poder ser **deployados independentemente** através de um processo automatizado. A **necessidade de gestão centralizada deve ser mínima** para os serviços visando viabilizar a independência entre eles.
        
        
            ## Componentes por Serviços
            - ### Componentes: unidades de software **independentes**
            - ### Serviços: **componentes externos** com comunicação por API
        
        
            ## Foco em Capacidades do Negócio
            - ### Serviços limitados ao **contexto do problema**
            - ### Equipe precisa saber só do **contexto específico** e não mais do todo
        
        
            ## Pensamento em ~~Projeto~~ Produto
            - ### Fim da **Lei de Conway**
            - ### Domínio menor == entendimento mais simples
            - ### Entendimento mais simples == equipe responsável por **todo o serviço**
            - ### Deploy independentemente
        
        
            ## Smart Endpoints & Dumb Pipes
            - ### Foco em **coesão** e **desacoplamento**
            - ### Chamadas por métodos em memória viram **chamadas ao serviço**
            - ### HTTP >> **síncrono**
            - ### Mensageria >> **assíncrono**
        
        
            ## Governança Descentralizada
            - ### **Decisões específicas** para o microservice
            - ### **Right tool** for the job
            - ### **Independência** no processo de desenvolvimento da equipe
        
        
            ## Armazenamento de Dados Descentralizados
            - ### Dados sendo visualizados de **acordo com o contexto**
            - ### **Perde-se** a gestão de transações automática
        
        
            ## Design orientado a Falhas
            - ### **Falhas de comunicação** são sempre reais
            - ### A comunicação deve ser sempre **desenvolvida pensando o cenário de falha**
            - ### Código **mais estável**
        
        
            ## Conclusões do que poderíamos
            - ### Desenvolver em **outras tecnologias**
            - ### Ter rotinas de **deploy mais simples**
            - ### Envolver **mais devs** no projeto
            - ### Entregar **mais rápido**
            - ### Gerar software de **maior qualidade**
            - ### Focar em entregar **mais valor** para o cliente
        
        
            ## Nossa reação
            
        
        
            ## E começamos...
            - 
        
        
            ## Trade-offs que Encontramos
            - ### Aumento da **complexidade operacional**
              - ### Ambiente de dev mais burocrático (resolvemos com o Docker)
              - ### Diferentes processos de deploy
            - ### Problemas para garantir a **consistência dos dados**
            - ### Todos os overheads de **comunicação em sistemas distribuídos**
        
        
            ## Killer Problems
            - ### Nossas regras de negócios estavam **completamente acopladas** entre os módulos services.py, models.py e views.py
            - ### A **modelagem acoplada** do nosso banco de dados
            - ### Impossível de mudar
        
        
            
        
        
            
        
        
            ## Solução
            - 
        
        
            Então estudamos mais...
            
        
        
            ## Focos no Mindset
            - ### Definição dos **Bounded Context** dos serviços
            - ### Utilização de **Domain Objects**
            - ### Criação de **Use Cases** orquestrando a troca de mensagem entre os objetos
            - ### Implementação de **Anti-Corruption Layer** nos serviços
        
        
            ## Implementação em Etapas
        
        
            ## 1º Caso: Isolar lógica de um domínio (sem microservice)
            - Models continuaram no legado
            - Nova app somente lidando com um tipo de atendimento
            - Já existiam testes
            - Migramos as views, services, urls e testes isolando a app
            - Ponto de acoplamento: import dos models do legado
            - Objetivo: aprender a isolar a lógica
        
        
            ```python
            # urls.py do legado
            (r'^clientes/absenteismo/', include('abs_homolog.urls', namespace='abs_homolog'))
            # urls.py da app
            urlpatterns = patterns('abs_homolog.views',
                url(r'^graficos/consolidado/$', 'absenteeism_report', name='absenteeism_consolidated'),
                url(r'^graficos/cat/$', 'absenteeism_cat_report', name='absenteeism_cat_chart'),
            )
            ```
        
        
            ## 2º Caso: Nova funcionalidade com baixo acoplamento (sem microservice)
            - Models, views, forms e urls próprios em nova app
            - Utilização de Signals para não mexer no meio legado
            - Integração através de import em listeners no legado
            - Ponto de acoplamento: único import para o model legado de Paciente
            - Objetivo: aprender a organizar o código para ser consumido por clientes externos
        
        
            ```python
            # models.py do legado
            created_medical_file = Signal(providing_args=['medical_file'])
            created_medical_file.connect(
                receivers.populate_medical_file_reports,
                dispatch_uid='medical_file_creation'
            )
            # views.py do legado
            created_medical_file.send(
                sender='atendimento_ocupacional', medical_file=ficha
            )
            # listener.py no legado
            def populate_medical_file_reports(sender, medical_file, *args, **kwargs):
                from epidemiologic.models import EpidemiologicRecord
                EpidemiologicRecord.objects.create_entry(
                    **medical_file.get_report_data()
                )
            ```
        
        
            ## 3º Caso: Nova funcionalidade apenas acoplando o banco (microservice interno)
            - App plugável do Django (estratégias de settings)
            - Criação de UC para encapsular lógica de domínio
            - Utilização de Signals para não mexer no meio legado
            - Integração através de disparo de mensagens via RabbitMq
                - Disparo encapsulado em objetos de fronteira
                - Tarefas fazendo apenas a limpeza de dados e delegando aos UC
            - Ponto de acoplamento: apenas utilizaram o mesmo banco de dados
            - Objetivo: implementar um microservice sem o overhead dois ambientes
        
        
            ```python
            def populate_patient_peridocal_info(sender, patient, *args, **kwargs):
                from brmed_site.utils import PeriodicalDatesPatientProxy
                patient_proxy = PeriodicalDatesPatientProxy(patient)
                patient_proxy.send()
            class PeriodicalDatesPatientProxy(BaseProxy):
                def __init__(self, patient):
                    super(PeriodicalDatesPatientProxy, self).__init__()
                    self.patient = patient
                def send(self):
                    expiration_days = self.patient.days_until_due_date
                    if not isinstance(expiration_days, int):
                      return
                    kwargs = {
                      'patient_id': self.patient.id
                      'last_exam_date': self.patient.get_data_ultimo_exame().isoformat(),
                      'expiration_days': expiration_days,
                    }
                    return self.send_task(settings.UPDATE_PERIODICAL_LAST_EXAM_DATE, kwargs=kwargs)
            # use_cases.py no microservice chamada pela task
            class UpdatePeriodicalDatesExamInfo(object):
                @classmethod
                def execute(cls, patient_id, last_exam_date, expiration_days):
                    entry = PeriodicalDates.objects.get_or_prepare(patient_id=patient_id)
                    entry.update_last_exam_date(last_exam_date)
                    entry.set_expiration_date(expiration_days)
                    entry.save()
            ```
        
        
            ## 4º Caso: Nova funcionalidade agnóstica (microservice interno)
            - App plugável do Django (estratégias de settings)
            - Criação de UC para encapsular lógica de domínio
            - Utilização de Signals para não mexer no meio legado
            - Integração através de disparo de mensagens via RabbitMq
            - **Modelos sem semântica alguma do domínio do legado**
            - **Banco de dados próprio (máquina própria pro banco)**
            - Objetivo: ser possível deployar isoladamente no futuro
        
        
        ```python
        class ReportsRouter(object):
          def db_for_read(self, model, **hints):
            if model._meta.app_label == 'reports':
              return settings.REPORTS_DB_KEY
            return None
          def db_for_write(self, model, **hints):
            if model._meta.app_label == 'reports':
              return settings.REPORTS_DB_KEY
            return None
          def allow_relation(self, obj1, obj2, **hints):
            if obj1._meta.app_label == 'reports' and obj2._meta.app_label == 'reports':
              return True
            return None
          def allow_syncdb(self, db, model):
            if model._meta.app_label == 'south':
              return True
            if db == settings.REPORTS_DB_KEY:
              return model._meta.app_label == 'reports'
            elif model._meta.app_label == 'reports':
              return False
            return None
        # no settings.py
        DATABASE_ROUTERS = ['reports.router.ReportsRouter']
        ```
        
        
            ## 5º Caso: Autenticação como Microservice
            - App SSO em Sinatra (Ruby)
            - **Todo** o ambiente isolado
            - No legado mudamos o Middleware e Backends de autenticação
            - Objetivo: validar participação de outros devs no projeto
        
        
            ## 6º Caso: Novo módulo em Microservice
            - **App Flask** de ambiente isolado
            - Utilização de comunicação por RabbitMq **e** API Rest
            - Tivemos que desenvolver uma API para expor o legado
            - Objetivo: implementar microservices por completo
        
        
            ## Conclusões
            - ### Tem **muitos** trade-offs para serem pensados
            - ### Não usamos microservices para tudo
               - ### Foco em **modularização**
            - ### Diminuímos o **tempo de entrega**
            - ### Diminuímos a **barreira de entrada** no projeto
        
        
            III Encontro PythOnRio
            
        
        
            11ª PythonBrasil