eletrotupi / tcc/ commit / c901c35

thesis: expand architecture and technology rationales

Pedro Lucas Porcellis porcellis@eletrotupi.com · 2 days ago c901c35af9c84b6a29e5185545ced1129f33b8f4 Browse files
Parents 31d5e0a
Changed files
3
Additions
+200
Deletions
-19
thesis/chapters/architecture.tex
1 1
\chapter{Arquitetura e Funcionamento do Sistema}
2 2

3 3
Como delineado as tecnologias no capítulo anterior, aqui se apresenta a
4 4
arquitetura, demonstrando como todas as tecnologias apresentadas acima se
5 5
conectam e conversam entre si.
10 10
\label{fig:arch}
11 11
\includegraphics[width=0.9\linewidth]{arch-v2.png}
12 12
\source{Elaborado pelo autor (2026)}
13 13
\end{figure}
14 14

15 +
Três planos compõem a arquitetura representada na Figura~\ref{fig:arch} ---
16 +
cliente, \textit{back-end} e infraestrutura ---, lidos da esquerda para a
17 +
direita. O aplicativo móvel comunica-se exclusivamente com a API por
18 +
HTTPS/JSON e mantém no dispositivo apenas o estado efêmero da interface e um
19 +
cache local dos dados já consultados; validação, autorização e regras de
20 +
domínio ficam concentradas no servidor. A API é a única porta de entrada para
21 +
os dados: acessa o PostgreSQL por meio do Prisma, grava avatares no
22 +
armazenamento de objetos e delega o trabalho que não precisa bloquear a
23 +
resposta ao usuário a filas no Valkey, consumidas por \textit{workers}
24 +
independentes. São esses \textit{workers} que enviam e-mails e notificações e
25 +
recalculam os indicadores, mantendo a API livre de tarefas de longa duração.
26 +

27 +
Essa organização tem como principal força a separação de responsabilidades em
28 +
torno de um contrato único: como o cliente só conhece a API, mudanças internas
29 +
no banco, nas filas ou nos serviços externos não o afetam, e a autenticação por
30 +
JWT permite que a API permaneça sem estado, reiniciável sem perda de sessão. O
31 +
desacoplamento por filas também torna o registro de um humor uma operação
32 +
curta, independente do custo de recalcular indicadores ou disparar
33 +
notificações. Em contrapartida, a topologia é deliberadamente monolítica e
34 +
vertical: todos os componentes são implantados em um único servidor por meio do
35 +
Docker Compose, sem replicação horizontal da API ou separação física entre
36 +
banco, fila e \textit{workers}. Isso significa que o trabalho pesado dos
37 +
\textit{workers} compete por CPU e memória com o atendimento das requisições, e
38 +
que a disponibilidade do conjunto equivale à disponibilidade desse único
39 +
\textit{host}. Trata-se de um \textit{trade-off} coerente com o porte do
40 +
projeto, mas que delimita seu principal limite de escalabilidade.
15 41

16 42
Este capítulo detalha o funcionamento de algumas funcionalidades
17 43
desenvolvidas durante o projeto. Apenas algumas funcionalidades mais
18 44
chaves são abordadas neste capítulo, como o registro de humor, o vínculo
19 45
de um gatilho, os indicadores, o registro de atividades e, por fim, a
409 435
complementa, mas não substitui, a exportação integral: para uma visão
410 436
completa de todos os tipos de dado, o usuário ainda recorre ao
411 437
\textit{zip} de CSV.
412 438

413 439
\FloatBarrier
thesis/chapters/tech.tex modified+174 -19
thesis/chapters/tech.tex
1 1
\chapter{Tecnologias}
2 2

3 3
Este capítulo apresenta as principais tecnologias empregadas no desenvolvimento
4 4
do aplicativo, organizadas de acordo com a camada da arquitetura em que atuam:
5 5
\textit{front-end}, \textit{back-end} e infraestrutura/persistência de dados.
111 111
export type CreateMoodInput = z.infer<typeof CreateMoodSchema>;
112 112
\end{lstlisting}
113 113

114 114
\sourcecode{Elaborado pelo autor (2026)}
115 115

116 -
Aqui, é possível observar alguns elementos, como a extração direta dos
117 -
\textit{enums} mapeados pelo Prisma, citado anteriormente nas linhas 1--5. Em
118 -
seguida, são criados os \textit{schemas} de validação: um valida os componentes
116 +
Aqui, é possível observar alguns elementos. O primeiro é o uso de
117 +
\texttt{z.nativeEnum} para reaproveitar diretamente os \textit{enums}
118 +
declarados no esquema do Prisma (linhas 1--5), em vez de repetir as listas de
119 +
valores no código. Como esses enums são a mesma fonte de verdade que o banco
120 +
impõe, a validação aceita exatamente o conjunto de valores que pode ser
121 +
persistido, e qualquer valor fora dele é rejeitado na borda da API antes de
122 +
chegar à camada de persistência. Esse padrão se repete de forma extensiva nos
123 +
demais esquemas, que reutilizam enumerações de gatilho, tipo de ação de
124 +
cuidado, periodicidade de medicamento e tipo de indicador. Em seguida, são
125 +
criados os \textit{schemas} de validação: um valida os componentes
119 126
de humor de forma aninhada dentro do objeto recebido, e o outro valida os dados
120 127
do próprio registro --- os níveis aceitos de ansiedade, estresse e energia; a
121 128
conversão do campo \texttt{moment} para um objeto de data a partir da entrada
122 129
recebida; a exigência de ao menos um componente de humor selecionado; entre
123 130
outros. Por fim, esses dados são exportados como um \textit{type} do
186 193
O Prisma foi escolhido por permitir a manutenção do esquema do banco de dados
187 194
de forma versionada e sincronizada com o código da aplicação, reduzindo a
188 195
chance de divergência entre a estrutura do banco e as entidades manipuladas
189 196
pelo back-end, além de prover checagem de tipos em tempo de compilação para
190 197
as consultas realizadas.
198 +

199 +
O Código~\ref{lst:prisma-schema} reproduz um trecho representativo desse
200 +
esquema, o modelo de registro de humor. Além dos campos e de seus tipos, o
201 +
trecho evidencia a convenção de nomenclatura adotada: modelos e campos seguem o
202 +
padrão do TypeScript --- \texttt{PascalCase} para tipos e \texttt{camelCase}
203 +
para propriedades ---, enquanto as tabelas e colunas no PostgreSQL usam
204 +
\texttt{snake\_case}, com nomes de tabela no plural. Os atributos
205 +
\texttt{@map} e \texttt{@@map} fazem essa tradução, de modo que o código da
206 +
aplicação manipula \texttt{selectedMood} ou \texttt{anxietyLevel} sem que os
207 +
nomes físicos das colunas vazem para a API. O trecho também mostra a declaração
208 +
explícita das relações e o uso de enumerações para valores fechados. Esses
209 +
\textit{enums} não se limitam ao código: na migração, o Prisma os materializa
210 +
como tipos enumerados nativos do PostgreSQL, de modo que a restrição aos
211 +
valores previstos é imposta também pelo banco. O esquema declara enumerações
212 +
para humores, componentes e intensidade, categorias de gatilho, tipos de ação
213 +
de cuidado e periodicidade de medicamentos, entre outras. A partir desse único
214 +
arquivo, o Prisma gera o cliente tipado consumido pela API e as migrações SQL
215 +
versionadas no repositório, de modo que alterações no modelo passam por revisão
216 +
e são aplicadas de forma reproduzível em qualquer ambiente.
217 +

218 +
\begin{lstlisting}[caption={Trecho do esquema Prisma para o registro de humor}, label={lst:prisma-schema}]
219 +
model Mood {
220 +
  id           Int            @id @default(autoincrement())
221 +
  annotation   String?        @db.Text
222 +
  moment       DateTime       @default(now())
223 +
  selectedMood BaseMoodOption @default(GOOD) @map("selected_mood")
224 +
  anxietyLevel Int            @map("anxiety_level")
225 +
  stressLevel  Int            @map("stress_level")
226 +
  energyLevel  Int            @map("energy_level")
227 +
  userId       Int            @map("user_id")
228 +
  user         User           @relation(fields: [userId], references: [id])
229 +

230 +
  moodComponents MoodComponent[]
231 +
  triggerLinks   TriggerMoodLink[]
232 +
  careActions    CareAction[]
233 +

234 +
  @@map("moods")
235 +
}
236 +
\end{lstlisting}
237 +

238 +
\sourcecode{Elaborado pelo autor (2026)}
191 239

192 240
\subsection{VictoriaLogs e Vector}
193 241

194 242
Observabilidade, no contexto de sistemas distribuídos, designa a capacidade de
195 243
compreender o estado interno de uma aplicação a partir dos sinais que ela emite
390 438
GitHub Actions é uma plataforma de integração e entrega contínuas (CI/CD)
391 439
nativa do GitHub, que permite a definição de fluxos de trabalho automatizados
392 440
--- disparados por eventos como \textit{push} ou \textit{pull request} ---
393 441
descritos de forma declarativa em arquivos YAML \citep{githubactions:2026}.
394 442

395 -
No projeto, o fluxo de trabalho de CI/CD é dividido em três etapas
396 -
sequenciais e dependentes entre si. A primeira etapa (\texttt{test}) sobe um
397 -
serviço efêmero de PostgreSQL e executa a suíte de testes automatizados da
398 -
API a cada \textit{push} ou \textit{pull request} que modifique arquivos do
443 +
No projeto, o fluxo de trabalho de CI/CD é dividido em três etapas sequenciais
444 +
e dependentes entre si, como visto na
445 +
Figura~\ref{fig:screenshot-github-actions}. A primeira etapa (\texttt{test})
446 +
sobe um serviço efêmero de PostgreSQL e executa a suíte de testes automatizados
447 +
da API a cada \textit{push} ou \textit{pull request} que modifique arquivos do
399 448
diretório da API.
400 449

401 450
A segunda etapa (\texttt{build-and-push}), condicionada ao
402 451
sucesso da primeira e restrita a eventos de \textit{push}, constrói a imagem
403 452
Docker de produção da API e a publica no GitHub Container Registry (GHCR),
409 458
\textit{branch} \texttt{master}, conecta-se ao servidor de produção via SSH e
410 459
realiza a atualização do serviço da API por meio da atualização da tag da
411 460
imagem no arquivo \texttt{.env} e da recriação do contêiner correspondente
412 461
via Docker Compose.
413 462

463 +
\begin{figure}[H]
464 +
\centering
465 +
\caption{Github Actions rodando os testes e realizando \textit{deploy}}
466 +
\label{fig:screenshot-github-actions}
467 +
\includegraphics[width=\linewidth]{screenshot-github-actions.png}
468 +
\source{Elaborado pelo autor (2026)}
469 +
\end{figure}
470 +

414 471
Esse processo elimina a necessidade de intervenção manual para a maior
415 472
parte do ciclo de implantação, restringindo a ação humana direta sobre o
416 473
servidor de produção a situações excepcionais, o que reduz a superfície de
417 474
erro humano e aumenta a confiabilidade do processo de entrega de novas
418 475
versões da aplicação.
423 480
descrever, de forma declarativa e idempotente, o estado desejado de um ou
424 481
mais servidores, dispensando a necessidade de um agente instalado na máquina
425 482
gerenciada, uma vez que se comunica com ela via SSH \citep{ansible:2026}.
426 483

427 484
No escopo deste projeto, o papel do Ansible é ortogonal ao do Docker Compose
428 -
e ao do GitHub Actions: enquanto estes automatizam, respectivamente, a
429 -
orquestração dos serviços da aplicação e o ciclo de integração e entrega
430 -
contínuas, o Ansible é responsável exclusivamente pelo provisionamento
431 -
inicial do servidor de produção, executado uma única vez --- ou sempre que um
432 -
novo servidor precisar ser provisionado. Esse provisionamento consiste em:
433 -
(1) criar o usuário dedicado à aplicação; (2) adicionar a chave pública SSH
434 -
utilizada pelo GitHub Actions para se autenticar no servidor durante a etapa
435 -
de implantação; (3) criar os diretórios da aplicação com as permissões
436 -
adequadas; e (4) instalar o Docker no servidor. A cópia do arquivo de
437 -
variáveis de ambiente de produção (\texttt{.env.production}), por conter
438 -
segredos e credenciais sensíveis, é deliberadamente mantida como uma etapa
439 -
manual, não automatizada, e não é versionada no repositório.
485 +
e ao do GitHub Actions: enquanto estes automatizam, respectivamente,
486 +
a orquestração dos serviços da aplicação e o ciclo de integração e entrega
487 +
contínuas, o Ansible é responsável exclusivamente pelo provisionamento inicial
488 +
do servidor de produção, executado uma única vez --- ou sempre que um novo
489 +
servidor precisar ser provisionado. Esse provisionamento consiste em: (1) criar
490 +
o usuário dedicado à aplicação; (2) adicionar a chave pública SSH utilizada
491 +
pelo GitHub Actions para se autenticar no servidor durante a etapa de
492 +
implantação; (3) criar os diretórios da aplicação com as permissões adequadas;
493 +
e (4) instalar o Docker no servidor. A cópia do arquivo de variáveis de
494 +
ambiente de produção (\texttt{.env.production}), por conter segredos
495 +
e credenciais sensíveis, é deliberadamente mantida como uma etapa manual, não
496 +
automatizada, e não é versionada no repositório. O Ansible, trabalha de forma
497 +
modularizada, portanto permite que possamos organizar as tarefas a serem feitas
498 +
em ``perfis'', da qual podem ter uma serie de tarefas para serem executadas em
499 +
sequencia, \textit{templates}, etc. O Código~\ref{lst:ansible-setup} demonstra
500 +
a lista de tarefas iniciais, em qual máquinas devem rodar, através da diretiva
501 +
\textit{hosts}, o método de troca de permissão (através da diretiva
502 +
\textit{become}) e tarefas posteriores.
503 +

504 +
\begin{lstlisting}[caption={Provisionamento utilizando um \textit{playbook} de configuração}, label={lst:ansible-setup}]
505 +
- name: Base host setup
506 +
  hosts: vps
507 +
  become: yes
508 +
  become_method: doas
509 +

510 +
  roles:
511 +
    - bootstrap
512 +
    - docker
513 +
    - appuser
514 +

515 +
  post_tasks:
516 +
    - name: Confirm deploy user can reach Docker
517 +
      ansible.builtin.command: docker info
518 +
      become_user: "{{ deploy_user }}"
519 +
      changed_when: false
520 +
      register: docker_check
521 +

522 +
    - name: Show Docker status
523 +
      ansible.builtin.debug:
524 +
        msg: "Docker is reachable by {{ deploy_user }}"
525 +
      when: docker_check.rc == 0
526 +
\end{lstlisting}
527 +

528 +
\sourcecode{Elaborado pelo autor (2026)}
529 +

530 +
O Código~\ref{lst:ansible-setup-tasks} mostra o conteúdo de um desses papéis, o
531 +
de Docker. Cada tarefa descreve um estado desejado --- o pacote instalado, o
532 +
diretório existente com determinada permissão, o arquivo de configuração do
533 +
\textit{daemon} com seu conteúdo, o serviço habilitado --- em vez de uma
534 +
sequência de comandos imperativos. É essa forma declarativa que permite ao
535 +
Ansible inspecionar o estado atual e agir apenas sobre o que diverge; a tarefa
536 +
de rotação de \textit{logs}, por exemplo, vale para todos os contêineres, e não
537 +
apenas para aqueles que declaram um bloco de \textit{logging} próprio no arquivo
538 +
de composição.
539 +

540 +
\begin{lstlisting}[caption={Tarefas de provisionamento utilizando um \textit{playbook}}, label={lst:ansible-setup-tasks}]
541 +
- name: Install Docker and Compose plugin
542 +
  community.general.apk:
543 +
    name:
544 +
      - docker
545 +
      - docker-cli-compose
546 +
    state: present
547 +

548 +
- name: Ensure /etc/docker exists
549 +
  ansible.builtin.file:
550 +
    path: /etc/docker
551 +
    state: directory
552 +
    owner: root
553 +
    group: root
554 +
    mode: "0755"
555 +

556 +
# Bound every container's json-file logs globally, not just the ones that set
557 +
# an explicit logging block in a compose file
558 +
- name: Configure the Docker daemon log rotation
559 +
  ansible.builtin.copy:
560 +
    dest: /etc/docker/daemon.json
561 +
    content: |
562 +
      {
563 +
        "log-driver": "json-file",
564 +
        "log-opts": { "max-size": "10m", "max-file": "3" }
565 +
      }
566 +
    owner: root
567 +
    group: root
568 +
    mode: "0644"
569 +
  notify: restart docker
570 +

571 +
- name: Enable Docker service
572 +
  ansible.builtin.service:
573 +
    name: docker
574 +
    enabled: yes
575 +
    state: started
576 +

577 +
\end{lstlisting}
578 +

579 +
\sourcecode{Elaborado pelo autor (2026)}
580 +

581 +
Automatizar esse provisionamento com o Ansible, em vez de repetir os mesmos
582 +
passos manualmente por SSH, não economiza apenas comandos: troca um
583 +
procedimento implícito, que existiria apenas na memória de quem o executa, por
584 +
um artefato versionado e revisável. Por ser idempotente, o \textit{playbook}
585 +
pode ser reexecutado após uma falha parcial ou sobre um servidor já configurado
586 +
sem duplicar usuários, diretórios ou configurações; como consequência, o
587 +
processo se torna auditável e reproduzível por terceiros.
588 +
Em um projeto de código aberto, é isso que permite que outra pessoa reconstrua
589 +
a infraestrutura do zero --- ou a audite antes de confiar nela --- sem acesso
590 +
prévio ao servidor de produção, além de reduzir a chance de erro humano e de
591 +
divergência entre ambientes. A divisão em papéis (\textit{roles}) reforça essa
592 +
propriedade ao separar responsabilidades --- usuário da aplicação, Docker e
593 +
configuração base ---, de modo que cada parte pode ser alterada e testada
594 +
isoladamente.
440 595

441 596
A separação entre o provisionamento inicial (\textit{Ansible}), a orquestração de
442 597
execução (\textit{Docker Compose}) e a automação do ciclo de
443 598
entrega (\textit{GitHub Actions}) reflete uma decisão arquitetural:
444 599
cada ferramenta atua em uma camada distinta e bem delimitada do processo
451 606
ao documentar e automatizar o processo de provisionamento, o projeto
452 607
favorece um modelo mais alinhado aos princípios de privacidade e de
453 608
autonomia sobre os dados dos usuários, permitindo que instituições ou
454 609
indivíduos preocupados com a confidencialidade de dados sensíveis de
455 610
saúde mental hospedem e controlem sua própria instância da aplicação.
/dev/null → thesis/images/screenshot-github-actions.png
Binary file