| 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.
|