As informações apresentadas nesta página são complementares às diretrizes de desenvolvimento de software do Ministério da Saúde.
As diretrizes contidas nesta página não são exaustivas: casos omissos devem analisados pela equipe da COATIC para que sejam adicionados oportunamente.
A seguir são apresentados alguns pilares das práticas DevSecOps e as diretrizes que devem ser seguidas para cada um desses pilares.
O controle de versionamento de softwares dentro do Datasus deve ser feito utilizando-se o SCM Git. Por ser uma ferramenta muito completa e flexivel, é importante delimitar também o fluxo de interação com os repositórios, também chamado de Git workflow. Um dos fluxos de trabalho mais comuns no SCM Git é o Gitflow, mas boa parte da comunidade de desenvolvimento de software vem desaconselhando seu uso devido a, entre outras razões, sua grande complexidade (mais informações em: https://www.endoflineblog.com/gitflow-considered-harmful e https://docs.gitlab.com/ee/topics/gitlab_flow.html#problems-with-the-git-flow).
Com um intuito de simplificar a padronizar a interação com o repositório, proporcionar o rastreamento de cada artefato e seus deploys em cada ambiente e, finalmente, minimizar a possibilidade de erro humano na referida interação, o Git workflow a ser utilizado no Datasus é o Oneflow (https://www.endoflineblog.com/oneflow-a-git-branching-model-and-workflow). Vale ressaltar, entretanto, que a implementação desse fluxo no âmbito do Datasus assume algumas particularidades, que são detalhados a seguir.
A implementação do Oneflow no Datasus assumirá 3 conjuntos de branches:
main
: branch padrão do Git. Utilizada para delimitar a versão de produção da aplicação.develop
: utilizada como base de desenvolvimento de features e como branch padrão para análise de código estático.develop
O padrão de nomenclatura das referências a commits é de extrema importância em qualquer Git workflow, já que ele tem implicações diretas cas esteiras de automação de validação e implantação das aplicações. Os nomes de tags e branches no Datasus serão ditados pelas regras que se seguem.
Branches de feature devem começar com o prefixo feat/
, acrescidas de string que denote o nome da funcionalidade a ser desenvolvida.
Exemplo: feat/tela-de-login
, feat/cadastro-paciente
.
As tags têm grande importância na implementação do Oneflow do Datasus. É através de seu uso que será possível rastrear qual versão da aplicação está implantada em cada ambiente. Seu uso também auxilia a eliminar um problema muito recorrente no Gitflow: sobrescrita de implantações em ambientes de validação. Nesse contexto, as tags também serão divididas em conjuntos, cada uma com uma regra de nomenclatura própria. O tipo de tag e as regras de nomeação de cada uma delas são detalhadas a seguir.
dev-feat-
acrescido do nome da funcionalidade e de um número inteiro, para diferenciar diferentes deploys de uma mesma feature em validação.
dev-feat-tela-de-login-1
, dev-feat-cadastro-paciente-2
.dev-v
acrescido de uma string se versão de acordo com o padrão SemVer (https://semver.org/).
dev-v1.2.0
, dev-v0.5.1
.hmg-v
acrescido da versão a ser liberada, também no padrão SemVer, acrescido no sufixo rc-
X, onde X é um número inteiro para diferenciar possíveis pacotes distintos.
dev-v1.2.0-rc1
, dev-v1.2.0-rc2
.v
mais string indicando a versão, de acordo com o padrão SemVer.
v1.2.0
.Uma das grandes vantagens do Oneflow em relação ao Gitflow é sua capacidade de manter a árvore de commits do repositório organizada mais facilmente. Uma árvore de commits linear, na qual as branches principais avançam sempre através de fast-forward merges é consideravelmente mais fácil de interpretar do que uma com commits de merge habituais. Essa visualização dá aos desenvolvedores do projeto informações valiosas sobre quais abordagens funcionaram (ou não) ao longo da evolução do código da aplicação. A imagem ilustra o processo de rebase + fast-foward merge
que deve ser seguido ao integrar branches de feature à branch develop
.
Processos de desenvolvimento maduros, alinhados à práticas DevSecOps, implementam gates de qualidade para garantir que os padrões arquiteturais e boas práticas de código estão sendo seguidos, bem como aspectos de segurança e confiabilidade da aplicação. O Datasus utiliza ferramentas como GitLab (https://about.gitlab.com/), SonarQube (https://www.sonarsource.com/products/sonarqube/), Tryvi (https://aquasecurity.github.io/trivy), OWASP ZAP (https://www.zaproxy.org/), dentre outras, para implementar os referidos gates de qualidade e segurança. Essas ferramentas são capazes de rodar em modo SAST (análise estática) ou DAST (análise dinâmica). Com o intuito de melhor elucidar onde cada verificação dessa é mais pertinente, pode-se dividir esses gates em 2 conjuntos principais, relacionados ao estágio onde são aplicados.
A imagem a seguir ilustra em detalhes em que momento cada um desses quality gates deve ser executado. Por serem executados em tempo de Merge Request, este só deverá ser aprovado caso todas as checagens tenham sido realizadas com parecer favorável. Caso contrário, os desenvolvedores devem corrigir as não-conformidades apontadas pelas ferramentas e/ou por seus pares e submeter novamente o código à Merge Request.
Os artefatos desenvolvidos para o Datasus devem ser imagens de container Docker.
A imagem a seguir ilustra os principais participantes relacionados ao deploy de um artefato, bem como em que etapa as verificações mencionadas acima devem ocorrer.
Como mencionado nas seções anteriores, uma das grandes vantagens do Oneflow em relação ao Gitflow é a capacidade de rastrear artefatos de deploy mais facilmente. Para isso, esse Git workflow faz ostensivo uso de tags. São as tags que disparam os pipelines de deploy as regras de nomenclatura associadas a elas que ditam em qual ambiente a implantação será feita. Há um rega geral, entretanto, que se aplica a todos os artefatos:
O nome da tag aplicada ao repositório deve ser identica à tag da imagem do container do artefato a ser implantado.
Tags de feature (
dev-feat-
) devem disparar a geração de artefatos para uso em ambiente localTags de desenvolvimento (
dev-
) devem disparar geração de artefato sua implantação em ambiente de devTags de candidata à release (
hmg-vW.Y.Z-rcX
) devem disparar a geração de artefato e sua implantação em ambiente hmgTags de produção (
vW.Y.Z
) deve ser aplicada ao mesmo commit que gerou a candidata à release aprovada e cujo artefato foi promovido à produção
A seguir é mostrado um exemplo prático de como se dá um fluxo de deploy habitual, bem os comandos que podem ser utilizados para implementar o Git workflow desejado. Este fluxo sintetiza os princípios que foram descritos neste documento e ilustra como a árvore de commits do repositório deve se comportar ao longo do desenvolvimento e deploy das funcionalidades da aplicação. É importante ressaltar, entretanto, que devido a complexidade do Git, é possível se obter um mesmo resultado através de formas (e comandos) distintas. O guia de comandos utilizado abaixo deve ser utilizado apenas como referência.
O pipeline padrão pode ser encontrado em: https://gitlab.saude.gov.br/datasus/arquitetura/ci-template.
Este template deve ser utilizado em todos os projetos e somente através dele será possível fazer deploy para os ambientes Kubernetes do Datasus.
Para utilizar o template é necessário:
Detalhes de como utilizar cada um desses arquivos serão apresentados a seguir.
O Dockerfile será utilizado para criar a imagem do container da aplicação, a ser executado no ambiente K8s. Ele deve conter pelo menos as etapas de build e deploy da aplicação. Um exemplo meramente ilustrativo é mostrado a seguir.
FROM alpine:3.21 as settings
RUN apk update \
&& apk add --no-cache curl sed \
&& rm -rf /var/cache/apk/*
ARG tokentype="Deploy-Token"
ARG pass
ARG NEXUS_USER=gitlab
ARG NEXUS_PASS
RUN curl -u $NEXUS_USER:$NEXUS_PASS https://nexus.saude.gov.br/repository/raw-hosted/maven/settings.xml -o settings.xml
RUN sed -i "s/TOKEN/$tokentype/" settings.xml && sed -i "s/PASS/$pass/" settings.xml
FROM maven:3.9.9-eclipse-temurin-21-alpine as build
COPY --from=settings settings.xml /home/settings.xml
COPY src /home/app/src
COPY pom.xml /home/app
RUN mvn -s /home/settings.xml -f /home/app/pom.xml -B -e clean package
FROM eclipse-temurin:21-jre-alpine
WORKDIR deploy/
COPY --from=build /home/app/target /deploy/target
COPY --from=build /home/app/target/*.jar /deploy/target/app.jar
ENV JAVA_APP_OPTS="\
-Djava.security.egd=file:/dev/./urandom \
-Dfile.encode=UTF-8 \
-Xms16m \
-Xmx6G \
-XX:ReservedCodeCacheSize=360m \
-XX:+UseG1GC \
-XX:G1HeapRegionSize=2 \
-XX:MaxGCPauseMillis=100 \
-XX:+UseStringDeduplication \
-ea \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:-OmitStackTraceInFastThrow \
-server \
-Xshareclasses:allowClasspaths,name=SpringBoot \
-Xquickstart \
-Xtune:virtualized \
-Xscmx1G \
-Xaggressive \
-XX:+UseLWPSynchronization \
-XX:+UseCompressedStrings \
-XX:+AggressiveOpts \
-XX:+OptimizeStringConcat \
-XX:+UseFastAccessorMethods \
"
ENTRYPOINT [ "sh", "-c", "java $JAVA_APP_OPTS $JAVA_OPTS -jar /deploy/target/app.jar" ]
O arquivo gitlab-ci.yml deve conter a importação do pipeline padrão, bem como a definição das variáveis que são necessárias ao deploy do projeto. Essas variáveis serão utilizadas para criar os recursos K8s do projeto, aplicados no cluster via arquivo k8s-services.yml. A versão mais recente deste arquivo será sempre disponibilizada no README.md do repositório https://gitlab.saude.gov.br/datasus/arquitetura/ci-template. Algumas partes dele, entretanto, são mostradas a seguir com o intuito de melhor esclarecer alguns aspectos de seu uso.
Para utilizar a funcionalidade de análise de código é necessário adicionar a etapa sonarqube-scan
no pipeline. As variáveis definidas nesse estágio são obtidas somente após a criação do projeto na ferramenta SonarQube, disponível em https://sonar.saude.gov.br. O acesso à ferramenta é feito através do mesmo usuário do Gitlab. Caso seu usuário não tenha acesso para criar um projeto, entre em contato com a equipe da COATIC.
sonarqube-scan:
extends:
- .sonarqube-scan
variables:
SONAR_PROJECT_KEY: "" # Project Key obtida após a criação do projeto no SonarQube
SONAR_SRC_PATH: "${CI_PROJECT_DIR}/src" # Substituir caso necessário
SONAR_JAVA_BINARIES_PATH: "${CI_PROJECT_DIR}/target" # Somente para projetos Java
Outro ponto importante a se ressaltar diz respeito à declaração das variáveis nas etapas de deploy. Todas as variáveis necessárias ao correto funcionamento da aplicação (normalmente mapeadas no Kubernetes como ConfigMap) devem ser definidas nessa etapa para que sejam corretamente interpoladas no arquivo k8s-services.yml resultante. Um exemplo de declaração dessas variáveis é mostrado a seguir.
deploy-dev:
extends:
- .deploy-k8s-dev
stage: deploy
tags:
- docker
before_script:
- export SECURITY_OAUTH2_CLIENT_CLIENT_SECRET=<valor>
- export SPRING_DATASOURCE_PASSWORD=<valor>
variables:
IMAGE: $RELEASE_IMAGE
APP_NAME: <nome-da-aplicacao>
NAMESPACE: <namespace>
BACKEND_HOST: <valor>
Finalmente, o arquivo k8s-services.yml deve conter a definição do recursos Kubernetes necessários para rodar a aplicação. Esses recursos variam de caso para caso, mas em todos eles as variáveis definidas no gitlab.yml serão utilizadas para substituir placeholders definidos nesse arquivo e, finalmente, serão aplicadas no Kubernetes via kubectl
pelo pipeline. A seguir é mostrado um exemplo de arquivo k8s-services.yml.
apiVersion: v1
data:
.dockerconfigjson: >-
<valor>
kind: Secret
type: kubernetes.io/dockerconfigjson
metadata:
name: <nome>
namespace: ${NAMESPACE}
---
apiVersion: v1
kind: Secret
metadata:
name: ${APP_NAME}
namespace: ${NAMESPACE}
type: Opaque
data:
EXAMPLE_PASSWORD: ${EXAMPLE_PASS}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ${APP_NAME}
namespace: ${NAMESPACE}
data:
AUTH_SERVER: ${AUTH_SERVER}
ENV: ${ENV}
FRONTEND_URL: ${FRONTEND_URL}
TZ: America/Sao_Paulo
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ${APP_NAME}
namespace: ${NAMESPACE}
spec:
ingressClassName: nginx
rules:
- host: ${BACKEND_HOST}
http:
paths:
- backend:
service:
name: ${APP_NAME}
port:
number: ${SERVER_PORT}
path: /
pathType: Prefix
---
apiVersion: v1
kind: Service
metadata:
labels:
name: ${APP_NAME}
tier: backend
name: ${APP_NAME}
namespace: ${NAMESPACE}
spec:
ports:
- name: backend
port: ${SERVER_PORT}
protocol: TCP
targetPort: ${SERVER_PORT}
selector:
name: ${APP_NAME}
tier: backend
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
name: ${APP_NAME}
tier: backend
name: ${APP_NAME}
namespace: ${NAMESPACE}
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
name: ${APP_NAME}
tier: backend
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
labels:
name: ${APP_NAME}
tier: backend
spec:
containers:
- env:
envFrom:
- configMapRef:
name: ${APP_NAME}
- secretRef:
name: ${APP_NAME}
image: ${IMAGE}
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 3
httpGet:
path: /actuator/health
port: ${SERVER_PORT}
scheme: HTTP
initialDelaySeconds: 300
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
name: ${APP_NAME}
ports:
- containerPort: ${SERVER_PORT}
name: 8080tcp2
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /actuator/health
port: ${SERVER_PORT}
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
resources:
limits:
cpu: "1"
memory: 1Gi
requests:
cpu: 300m
memory: 512Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: <nome>
restartPolicy: Always
schedulerName: default-scheduler
securityContext: { }
terminationGracePeriodSeconds: 30