eletrotupi / tcc/ thesis/chapters/tech.texc901c35af9c84b6a29e5185545ced1129f33b8f4
30.0 KB | 610 lines Raw
1
\chapter{Tecnologias}
2

3
Este capítulo apresenta as principais tecnologias empregadas no desenvolvimento
4
do aplicativo, organizadas de acordo com a camada da arquitetura em que atuam:
5
\textit{front-end}, \textit{back-end} e infraestrutura/persistência de dados.
6

7
\section{Front-end}
8

9
O termo \textit{frontend} refere-se à camada de um sistema responsável pela
10
interação direta com o usuário: a interface visual, a lógica de apresentação e
11
o processamento que ocorre no dispositivo do próprio usuário, seja um navegador
12
ou, como no caso do Nexo, um aplicativo móvel, conforme
13
~\citet{sommerville:2010}. É o frontend que renderiza telas, captura entradas
14
do usuário (como toques e formulários) e comunica-se com o \textit{backend}
15
através de requisições de rede, geralmente utilizando APIs. No caso do Nexo,
16
essa camada é implementada com \textit{React Native} e Expo, permitindo que a
17
mesma base de código seja compilada tanto para Android utilizando bibliotecas
18
em \textit{TypeScript}.
19

20
\subsection{React Native}
21

22
React Native é um \textit{framework} de código aberto, mantido pela Meta, que
23
permite o desenvolvimento de aplicativos móveis multiplataforma a partir de
24
uma única base de código escrita em JavaScript/TypeScript, utilizando
25
componentes que são traduzidos para elementos nativos tanto no iOS quanto no
26
Android \citep{reactnative:2026}.
27

28
A escolha do React Native se justifica pelo escopo reduzido da equipe de
29
desenvolvimento (um único desenvolvedor) e facilidade de conhecimento e
30
manutenção, sem trazer, para este projeto, benefícios que justifiquem esse
31
custo adicional. Além disso, o ecossistema maduro do React Native oferece
32
ampla disponibilidade de bibliotecas para funcionalidades já previstas no
33
aplicativo, como notificações locais e gráficos.
34

35
\subsection{Zustand}
36

37
Zustand é uma biblioteca leve de gerenciamento de estado para aplicações
38
React e React Native, que dispensa a estrutura verbosa de \textit{providers}
39
e \textit{reducers} tipicamente associada a bibliotecas como Redux
40
\citep{zustand:2026}.
41

42
Ela foi escolhida para gerenciar o estado efêmero da aplicação (por exemplo,
43
dados de formulários em preenchimento ou estados de navegação temporários)
44
por sua API minimalista e baixo \textit{overhead} de configuração, adequada a um
45
projeto de escopo individual em que a complexidade adicional de soluções
46
mais robustas de gerenciamento de estado não se justifica.
47

48
\section{Back-end}
49

50
Já o termo \textit{backend} refere-se à camada de um sistema que processa a
51
lógica de negócio, gerencia o acesso e a persistência de dados, e expõe essa
52
funcionalidade a clientes (como o frontend) através de uma interface bem
53
definida, tipicamente uma API HTTP \cite{Fowler_2013}. O \textit{backend} é
54
responsável por validar dados recebidos, aplicar regras de negócio, autenticar
55
usuários, e coordenar o acesso ao banco de dados e a outros serviços
56
auxiliares. No caso do Nexo, essa camada é implementada em Node.js com o
57
framework \textit{Express}, seguindo uma arquitetura em camadas (\textit{Route}
58
$\rightarrow$ \textit{Controller} $\rightarrow$ \textit{Service} $\rightarrow$
59
Prisma) que separa as responsabilidades de validação, lógica de negócio e
60
acesso a dados \cite{Martin_2012}.
61

62
\subsection{Express.js}
63

64
Express.js é um \textit{framework} minimalista para aplicações web em
65
Node.js, amplamente utilizado para a construção de APIs \textit{REST},
66
oferecendo uma camada fina de abstração sobre o roteamento de requisições HTTP e
67
\textit{middlewares} \citep{express:2026}.
68

69
Sua adoção se deve à simplicidade e à flexibilidade não opinativa do
70
\textit{framework}, que permite estruturar a API de acordo com as
71
necessidades específicas do projeto, sem impor convenções rígidas de
72
arquitetura, além de sua ampla documentação e maturidade no ecossistema
73
Node.js.
74

75
\subsection{Zod}
76

77
Zod é uma biblioteca de validação e definição de esquemas de dados para
78
TypeScript, que permite declarar formatos esperados de entrada (como corpos
79
de requisição) e inferir automaticamente os tipos estáticos correspondentes
80
\citep{zod:2026}.
81

82
Zod foi escolhido para validar os dados recebidos pela API antes de seu
83
processamento, garantindo que registros de humor, sono e demais entradas do
84
usuário estejam em conformidade com o formato esperado. A inferência
85
automática de tipos a partir dos esquemas evita a duplicação de definições de
86
tipo entre a camada de validação e o restante do código TypeScript do
87
back-end.
88

89
\begin{lstlisting}[language=typescript, caption={Validação e Inferência usando Zod}, label={lst:zod-inference}]
90
import {
91
  BaseMoodOption,
92
  MoodComponentOption,
93
  IntensityLevel
94
} from '@prisma/client';
95

96
export const MoodComponentSchema = z.object({
97
  component: z.nativeEnum(MoodComponentOption),
98
  intensity: z.nativeEnum(IntensityLevel)
99
});
100

101
export const CreateMoodSchema = z.object({
102
  annotation: z.string().optional(),
103
  moment: z.coerce.date(),
104
  selectedMood: z.nativeEnum(BaseMoodOption),
105
  anxietyLevel: z.number().int().min(0).max(10),
106
  stressLevel: z.number().int().min(0).max(10),
107
  energyLevel: z.number().int().min(0).max(10),
108
  moodComponents: z.array(MoodComponentSchema).min(1)
109
});
110

111
export type CreateMoodInput = z.infer<typeof CreateMoodSchema>;
112
\end{lstlisting}
113

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

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
126
de humor de forma aninhada dentro do objeto recebido, e o outro valida os dados
127
do próprio registro --- os níveis aceitos de ansiedade, estresse e energia; a
128
conversão do campo \texttt{moment} para um objeto de data a partir da entrada
129
recebida; a exigência de ao menos um componente de humor selecionado; entre
130
outros. Por fim, esses dados são exportados como um \textit{type} do
131
TypeScript, usado pelo compilador para a verificação estática de tipos no
132
restante do back-end.
133

134
\subsection{Valkey e BullMQ}
135

136
Valkey é um banco de dados em memória, do tipo chave-valor, criado como
137
\textit{fork} de código aberto do Redis, mantido sob a governança da Linux
138
Foundation \citep{valkey:2026}. BullMQ é uma biblioteca para gerenciamento de
139
filas de tarefas em Node.js, construída sobre bancos de dados compatíveis com
140
o protocolo Redis \citep{bullmq:2026}.
141

142
A combinação das duas ferramentas foi adotada para o processamento paralelo
143
e assíncrono de tarefas que não precisam bloquear a resposta imediata da API,
144
como o disparo de notificações programadas e o processamento de indicadores
145
a partir do histórico de registros do usuário. A escolha do Valkey em
146
específico, e não do Redis diretamente, se deve à mudança de licenciamento do
147
Redis em 2024, que passou a restringir certos usos comerciais; o Valkey
148
mantém compatibilidade total de protocolo, permanecendo sob licença
149
permissiva de código aberto.
150

151
\section{Infraestrutura e Persistência de Dados}
152

153
A camada de \textit{infraestrutura e persistência} de um sistema abrange os
154
componentes responsáveis por armazenar e gerenciar os dados de forma durável,
155
bem como os serviços auxiliares que sustentam o funcionamento da aplicação em
156
produção \cite{Fowler_2013}. O elemento central dessa camada costuma ser o
157
sistema de gerenciamento de banco de dados (SGBD), responsável por garantir a
158
integridade, consistência e durabilidade dos dados armazenados. No caso do
159
Nexo, essa função é cumprida pelo PostgreSQL, um SGBD relacional, acessado
160
através do Prisma, um \textit{Object-Relational Mapper} (ORM) que traduz
161
operações do banco de dados em chamadas de código TypeScript, eliminando a
162
necessidade de escrever consultas SQL manualmente na maior parte dos casos.
163

164
Complementarmente, o Nexo utiliza o Valkey como armazenamento em memória de
165
alta velocidade, empregado tanto para cache quanto como \textit{broker} de
166
mensagens para o sistema de filas, cujo suas filas são gerenciadas com a
167
biblioteca BullMQ para lidar com os \textit{background jobs} enviados. Os
168
mesmos são responsáveis por processar os indicadores semanais e diários
169
descritos nos capítulos anteriores. Além disso, o Nexo mantém uma pilha de
170
observabilidade dedicada à coleta e à consulta dos \textit{logs} de seus
171
contêineres, descrita a seguir, que complementa a persistência dos dados de
172
domínio.
173

174
\subsection{PostgreSQL}
175

176
PostgreSQL é um sistema gerenciador de banco de dados relacional, de código
177
aberto, reconhecido por sua conformidade com o padrão SQL, robustez e suporte
178
a tipos de dados avançados \citep{postgresql:2026}.
179

180
Sua escolha se justifica pela natureza estruturada e relacional dos dados do
181
domínio da aplicação (registros de humor associados a usuários, horários,
182
gatilhos e categorias), que se beneficia das garantias de integridade
183
referencial e das transações ACID oferecidas por um banco relacional
184
maduro, em detrimento de soluções não relacionais.
185

186
\subsection{Prisma}
187

188
Prisma é um \textit{Object-Relational Mapper} (ORM) para o ecossistema
189
Node.js/TypeScript, que gera automaticamente um cliente de acesso ao banco de
190
dados tipado a partir de um esquema declarativo, além de oferecer um sistema
191
de migrações versionadas \citep{prisma:2026}.
192

193
O Prisma foi escolhido por permitir a manutenção do esquema do banco de dados
194
de forma versionada e sincronizada com o código da aplicação, reduzindo a
195
chance de divergência entre a estrutura do banco e as entidades manipuladas
196
pelo back-end, além de prover checagem de tipos em tempo de compilação para
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)}
239

240
\subsection{VictoriaLogs e Vector}
241

242
Observabilidade, no contexto de sistemas distribuídos, designa a capacidade de
243
compreender o estado interno de uma aplicação a partir dos sinais que ela emite
244
--- em especial \textit{logs}, métricas e rastros ---, o que permite investigar
245
problemas sem conhecer previamente sua causa \citep{opentelemetry:2026}. Entre
246
esses sinais, os \textit{logs} são o mais direto: registros datados de eventos
247
emitidos pelos serviços. No Nexo, eles cobrem tanto as requisições HTTP
248
recebidas pela API quanto as tarefas processadas pelos \textit{workers} de
249
segundo plano. A pilha descrita a seguir é dedicada a coletá-los, armazená-los
250
e torná-los consultáveis.
251

252
VictoriaLogs é um sistema de armazenamento e consulta de \textit{logs}
253
desenvolvido pela VictoriaMetrics, projetado para operar como um único binário,
254
sem dependências externas, e com consumo reduzido de memória e de disco
255
\citep{victorialogs:2026}. Vector é um agente de coleta e roteamento de
256
observabilidade, também distribuído como um único binário, capaz de ler
257
\textit{logs} de diversas fontes --- arquivos, soquetes ou a API do Docker ---,
258
transformá-los por meio de uma linguagem própria de remapeamento e enviá-los a
259
diferentes destinos \citep{vector:2026}.
260

261
A motivação para adotar essa combinação, em vez de uma pilha bastante difundida
262
como Elasticsearch, Logstash e Kibana (ELK) \citep{elastic:2026}, é
263
proporcional ao porte do projeto: o ELK exige múltiplos serviços, e o
264
Elasticsearch é executado sobre a máquina virtual Java; segundo a documentação
265
do VictoriaLogs, soluções como ele demandam consideravelmente mais memória e
266
disco que o VictoriaLogs \citep{victorialogs:2026}. Em um servidor único que
267
também hospeda a API, o PostgreSQL e o Valkey, esse custo seria
268
desproporcional. VictoriaLogs e Vector cumprem o mesmo papel com dois
269
contêineres menores/mais leves.
270

271
No Nexo, o fluxo de \textit{logs} começa na própria API, que utiliza a
272
biblioteca Pino para emitir um objeto JSON por linha, em vez de texto livre
273
\citep{pino:2026}. Como o \textit{logger} só aplica formatação legível quando a
274
saída é um terminal interativo, no contêiner --- onde a saída é um
275
\textit{pipe} --- os registros permanecem em JSON de linha única. O
276
\textit{middleware} \texttt{pino-http} associa a cada requisição um
277
identificador (\texttt{X-Request-Id}), convenção difundida para correlacionar
278
os registros de uma mesma requisição \citep{heroku:2026}, reaproveitando o
279
cabeçalho homônimo quando enviado pelo cliente ou gerando um UUID.
280

281
O Vector lê essas linhas por meio da API do Docker, cujo \textit{socket} é
282
montado no contêiner como volume somente leitura, e aplica uma transformação de
283
remapeamento que tenta interpretar cada mensagem como JSON; quando a
284
interpretação é bem-sucedida, os campos do objeto são elevados ao nível
285
superior do evento, o que torna consultáveis atributos como o nível do
286
\textit{log}, a mensagem e o identificador do usuário. O resultado é enviado em
287
lote ao \textit{endpoint} de ingestão compatível com a API do Elasticsearch do
288
VictoriaLogs, conforme o Código~\ref{lst:vector-pipeline}.
289

290
\begin{lstlisting}[caption={Configuração do \textit{pipeline} de \textit{logs} no \texttt{vector.yaml}}, label={lst:vector-pipeline}]
291
sources:
292
  docker:
293
    type: docker_logs
294

295
transforms:
296
  parse_json_message:
297
    type: remap
298
    inputs: [docker]
299
    source: |
300
      parsed, err = parse_json(.message)
301
      if err == null && is_object(parsed) {
302
        . = merge(., parsed) ?? .
303
      }
304

305
sinks:
306
  victorialogs:
307
    type: elasticsearch
308
    inputs: [parse_json_message]
309
    endpoints: ["http://victorialogs:9428/logs/insert/elasticsearch/"]
310
    mode: bulk
311
    api_version: v8
312
    compression: gzip
313
    request:
314
      headers:
315
        VL-Stream-Fields: label.com.docker.compose.service
316
        VL-Time-Field: timestamp
317
        VL-Msg-Field: msg,message,_msg
318
\end{lstlisting}
319

320
\sourcecode{Elaborado pelo autor (2026)}
321

322
O contêiner do VictoriaLogs é configurado com um período de retenção de 14
323
dias e um teto de 2\,GiB de uso de disco; ambos são limites deliberados para
324
que o armazenamento de \textit{logs} não comprometa o disco da máquina de
325
produção, descartando os registros mais antigos quando qualquer um deles é
326
atingido. O serviço escuta apenas na interface de \textit{loopback}
327
(\texttt{127.0.0.1:9428}), e não em todas as interfaces, de modo que a
328
interface de consulta não é exposta diretamente à internet. O acesso externo
329
se dá pelo Nginx, que publica o caminho \texttt{/logs/} sob autenticação
330
básica HTTP, com credenciais definidas no \textit{vault} criptografado do
331
Ansible; a diretiva \texttt{-http.pathPrefix=/logs} no VictoriaLogs garante
332
que os caminhos gerados pela interface correspondam ao prefixo publicado
333
pelo \textit{proxy}, conforme ilustrado na
334
Figura~\ref{fig:screenshot-victorialogs}.
335

336
\begin{figure}[H]
337
\centering
338
\caption{Interface de consulta do VictoriaLogs com os \textit{logs} da API}
339
\label{fig:screenshot-victorialogs}
340
\includegraphics[width=\linewidth]{screenshot-victorialogs.png}
341
\source{Elaborado pelo autor (2026)}
342
\end{figure}
343

344
Na captura, a consulta está restrita ao fluxo de \textit{logs} da API, por
345
meio do filtro \texttt{label.com.docker.compose.service="api"}. À esquerda,
346
o painel de \textit{stream fields} lista os fluxos disponíveis e o volume de
347
registros de cada um; na parte superior, um histograma mostra a distribuição
348
desses registros em intervalos de 30 minutos ao longo do período
349
selecionado; e, na parte inferior, a tabela exibe as entradas
350
correspondentes, com data, nível e mensagem, incluindo desde a sequência de
351
inicialização da API e de seus \textit{workers} até o envio de notificações.
352
Consultas como essa permitem, por exemplo, correlacionar as linhas de uma
353
mesma requisição a partir do seu \texttt{X-Request-Id}.
354

355
Como o Vector não substitui o \textit{driver} de \textit{logs} do Docker, o
356
\textit{driver} \texttt{json-file} permanece ativo, porém limitado por
357
rotação (10\,MB por arquivo, 3 arquivos por contêiner), de modo que
358
\texttt{docker compose logs} continue funcionando sem que o disco do servidor
359
cresça indefinidamente. As versões das imagens, a retenção e os limites de
360
disco são declarados como variáveis no inventário do Ansible e renderizados
361
no arquivo de ambiente, e o arquivo de configuração do Vector é copiado para
362
o diretório da aplicação pela \textit{role} \texttt{app}, o que mantém a
363
pilha de observabilidade reproduzível pelo mesmo processo de provisionamento
364
descrito na Seção~\ref{sec:deploy-and-ci}.
365

366
\section{Implantação e Desenvolvimento Contínuo} \label{sec:deploy-and-ci}
367

368
Já os termos \textit{implantação} (\textit{deployment}) e
369
\textit{automatização} referem-se ao conjunto de práticas e ferramentas
370
utilizadas para transformar o código-fonte de uma aplicação em um sistema
371
efetivamente disponível e em execução, de forma repetível e com o mínimo de
372
intervenção manual \cite{Humble_Farley_2011}. No caso do Nexo, essa camada é
373
sustentada por três ferramentas complementares. O Docker é utilizado para
374
empacotar a aplicação e suas dependências em contêineres isolados e portáveis,
375
garantindo que o ambiente de execução seja idêntico entre desenvolvimento e
376
produção; o Docker Compose orquestra a execução conjunta de múltiplos
377
contêineres (aplicação, banco de dados, Valkey), definindo suas configurações e
378
interdependências em um único arquivo declarativo. O GitHub Actions é
379
responsável pela integração e entrega contínuas (CI/CD), automatizando
380
etapas como execução de testes e construção de imagens a cada alteração no
381
código. Por fim, o Ansible é utilizado para o provisionamento inicial da
382
infraestrutura, automatizando a configuração do servidor onde a aplicação é
383
hospedada.
384

385
\subsection{Docker e Docker Compose}
386

387
Docker é uma plataforma de conteinerização que permite empacotar uma
388
aplicação e suas dependências em uma unidade isolada e portátil, capaz de ser
389
executada de forma consistente em diferentes ambientes \citep{docker:2026}. O
390
Docker Compose é uma ferramenta complementar que permite definir e orquestrar
391
múltiplos contêineres --- e as relações entre eles --- por meio de um único
392
arquivo de configuração declarativo \citep{dockercompose:2026}.
393

394
No projeto, o Docker Compose é utilizado tanto no ambiente de desenvolvimento
395
quanto no ambiente de produção para orquestrar os serviços de banco de dados
396
(PostgreSQL), armazenamento em memória (Valkey) e da própria API, garantindo
397
que as versões e configurações desses serviços sejam idênticas entre as
398
máquinas de desenvolvimento e o servidor de produção.
399

400
O uso de \textit{healthchecks} em conjunto com a diretiva
401
\texttt{depends\_on} e a condição \texttt{service\_healthy} assegura
402
que a API só seja iniciada após o banco de dados e o Valkey estarem de
403
fato prontos para aceitar conexões, evitando falhas de inicialização
404
por condição de corrida (\textit{race condition}), como ilustrado no
405
Código~\ref{lst:healthcheck-compose}.
406

407
\begin{lstlisting}[caption={Configuração do \textit{healthcheck} do Valkey e de dependência condicional da API no \texttt{docker-compose.yml}}, label={lst:healthcheck-compose}]
408
valkey:
409
  image: valkey/valkey:9.0-alpine
410
  healthcheck:
411
    test: ["CMD", "valkey-cli", "ping"]
412
    interval: 10s
413
    timeout: 5s
414
    retries: 5
415
    start_period: 5s
416

417
api:
418
  build: ./api
419
  depends_on:
420
    valkey:
421
      condition: service_healthy
422
    db:
423
      condition: service_healthy
424
\end{lstlisting}
425

426
\sourcecode{Elaborado pelo autor (2026)}
427

428
A adoção do Docker se justifica, no contexto de uma aplicação de saúde
429
mental de código aberto, pelo objetivo de garantir a reprodutibilidade do
430
ambiente de execução: qualquer pessoa que clone o repositório --- seja para
431
contribuir com o projeto, auditar seu funcionamento ou realizar sua própria
432
implantação --- consegue reproduzir o ambiente completo sem depender de
433
configurações manuais específicas do sistema operacional hospedeiro, o que
434
reforça o caráter \textit{self-service} do projeto.
435

436
\subsection{GitHub Actions}
437

438
GitHub Actions é uma plataforma de integração e entrega contínuas (CI/CD)
439
nativa do GitHub, que permite a definição de fluxos de trabalho automatizados
440
--- disparados por eventos como \textit{push} ou \textit{pull request} ---
441
descritos de forma declarativa em arquivos YAML \citep{githubactions:2026}.
442

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
448
diretório da API.
449

450
A segunda etapa (\texttt{build-and-push}), condicionada ao
451
sucesso da primeira e restrita a eventos de \textit{push}, constrói a imagem
452
Docker de produção da API e a publica no GitHub Container Registry (GHCR),
453
com marcação automática pelo SHA do \textit{commit} e, condicionalmente, pela
454
tag \texttt{latest} quando o \textit{push} ocorre na \textit{branch}
455
\texttt{master}.
456

457
Por último, a terceira etapa (\texttt{deploy-production}), restrita à
458
\textit{branch} \texttt{master}, conecta-se ao servidor de produção via SSH e
459
realiza a atualização do serviço da API por meio da atualização da tag da
460
imagem no arquivo \texttt{.env} e da recriação do contêiner correspondente
461
via Docker Compose.
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

471
Esse processo elimina a necessidade de intervenção manual para a maior
472
parte do ciclo de implantação, restringindo a ação humana direta sobre o
473
servidor de produção a situações excepcionais, o que reduz a superfície de
474
erro humano e aumenta a confiabilidade do processo de entrega de novas
475
versões da aplicação.
476

477
\subsection{Ansible}
478

479
Ansible é uma ferramenta de automação de infraestrutura que permite
480
descrever, de forma declarativa e idempotente, o estado desejado de um ou
481
mais servidores, dispensando a necessidade de um agente instalado na máquina
482
gerenciada, uma vez que se comunica com ela via SSH \citep{ansible:2026}.
483

484
No escopo deste projeto, o papel do Ansible é ortogonal ao do Docker Compose
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.
595

596
A separação entre o provisionamento inicial (\textit{Ansible}), a orquestração de
597
execução (\textit{Docker Compose}) e a automação do ciclo de
598
entrega (\textit{GitHub Actions}) reflete uma decisão arquitetural:
599
cada ferramenta atua em uma camada distinta e bem delimitada do processo
600
de implantação, o que favorece a reprodutibilidade de todo o processo,
601
do zero, por qualquer pessoa que deseje hospedar sua própria instância da aplicação.
602

603
Essa preocupação é particularmente relevante no contexto de um
604
aplicativo de saúde mental de código aberto: ao reduzir a dependência
605
de serviços proprietários de implantação (\textit{walled gardens}) e
606
ao documentar e automatizar o processo de provisionamento, o projeto
607
favorece um modelo mais alinhado aos princípios de privacidade e de
608
autonomia sobre os dados dos usuários, permitindo que instituições ou
609
indivíduos preocupados com a confidencialidade de dados sensíveis de
610
saúde mental hospedem e controlem sua própria instância da aplicação.