Não é nenhum segredo que, sem processos claros e organizados, os desenvolvedores podem ter dificuldades para colaborar de forma eficaz, levando a atrasos na entrega de atualizações de software. Há alguns anos, a equipe do Social Discovery Group enfrentou o desafio de um processo de CI/CD abaixo do ideal. Naquela época, a equipe utilizava TeamCity e Octopus, cada um com seus pontos fortes. Por exemplo, o Octopus é conveniente para implantações, enquanto o TeamCity é bom para testes automatizados e suficientemente conveniente para construção de projetos. Para construir um pipeline de CI/CD abrangente e visualmente atraente que seja extremamente conveniente e flexível na configuração, é necessário usar uma combinação de ferramentas. O código foi armazenado em um repositório local no Bitbucket para diversos projetos. A equipe do ODS estudou o assunto e decidiu otimizar o processo utilizando as ferramentas existentes.
Principais objetivos de otimização:
A equipe do SDG decidiu usar o TeamCity para compilações e testes automatizados, e o Octopus para implantações.
O que foi implementado no TeamCity:
O TeamCity permite a utilização de três agentes na versão gratuita, o que foi suficiente para a equipe do ODS. Eles instalaram um novo agente, adicionaram-no ao pool e aplicaram-no aos seus modelos.
Na hora de usar a versão mais recente do TeamCity, a equipe estava trabalhando no Ubuntu Server. A captura de tela mostra os plugins adicionais usados pela equipe:
A implantação do NuGet foi a seguinte:
Vale ressaltar que dependendo se o branch é master ou não, "-release" foi adicionado ao release (etapas 3, 4).
A implantação dos serviços pode ser vista abaixo:
Para cada serviço, as variáveis correspondentes foram substituídas com base nas variáveis do sistema (nome do serviço, %build.number% e outras).
Um exemplo da etapa Docker Build é apresentado na captura de tela:
Cada repositório do projeto continha o Dockerfile correspondente.
As diferenças entre as etapas 4 e 5, conforme mencionado anteriormente, foram as seguintes:
A variável %deploymentTarget%
serviu como parâmetro de Ambiente(s), para o(s) qual(is) estágio(s) correspondente(s) no Octopus foram passados durante a implantação (por exemplo, Teste, Dev). Quando as alterações eram enviadas para as respectivas filiais (configuradas) das equipes de desenvolvimento, as compilações e implantações de software nos ambientes de teste correspondentes eram executadas automaticamente. As configurações estão visíveis na imagem abaixo. Para conectar-se ao Octopus, dois parâmetros globais precisaram ser adicionados: octopus.apiKey e octopus.url
Além disso, a equipe do SDG conectou um repositório NuGet comum e um Container Registry para todos os projetos na seção Conexões.
Além disso, o SDG recomenda configurar notificações por email na seção Email Notifier, configurar backups na seção Backup, criar grupos necessários, atribuir funções apropriadas e adicionar usuários aos grupos necessários. A configuração principal está concluída e, para concluir, a equipe recomenda verificar regularmente se há atualizações e atualizar o TeamCity uma vez por mês.
Em seguida, a equipe do Social Discovery Group passou a configurar o Octopus. Este artigo não descreverá os detalhes da instalação, configurações básicas de direitos do usuário e outros aspectos, porque você pode fazer isso facilmente sozinho. A equipe abordou imediatamente o ciclo de vida, que está configurado na seção Biblioteca. Na captura de tela abaixo, você pode ver um exemplo de fluxo da equipe ODS:
Em seguida, a equipe criou todos os grupos de variáveis necessários por temas em Conjuntos de Variáveis. Para cada variável foram definidos valores e estabelecidas dependências do ambiente, alvos e funções alvo (tags). Um exemplo é mostrado na captura de tela abaixo:
Os clusters no Kubernetes serviam como alvos, e as funções de destino eram tags anexadas aos clusters ou ambientes de computador correspondentes. Tudo isso pode ser configurado na seção Infraestrutura.
Os projetos também podem ser agrupados e um painel conveniente pode ser configurado para exibir serviços, estágios e versões implantadas neles.
O processo de implantação do ODS foi o seguinte: todos os estágios de teste foram combinados em uma única etapa, e um modelo comum foi criado para eles, da mesma forma para os estágios de estágio e de produção.
A captura de tela abaixo mostra como isso foi para a equipe do ODS:
À direita, foi selecionado o Ciclo de Vida descrito anteriormente. O estágio Implantar um Pacote incluía configurações padrão bastante simples.
Para o estágio Deploy Raw Kubernetes Yaml, a equipe SDG usou modelos Yaml universais auto-escritos. Neste exemplo - Kubernetes Script, é explicado com mais detalhes abaixo. Os parâmetros correspondentes marcados em vermelho também foram substituídos. Vale a pena notar que os grupos de variáveis globais necessários foram conectados no menu Variáveis->Conjuntos de variáveis, e as variáveis específicas do projeto foram definidas no menu Variáveis->Projeto, que tinha maior prioridade.
Neste artigo, a equipe do ODS decidiu pular detalhes como adicionar um logotipo ao projeto, configurar gatilhos ou outros detalhes menores. Vamos nos concentrar em dois itens importantes do menu: 1 - Lançamentos, onde você sempre pode ver a versão e a data de criação de um determinado lançamento; essas informações também são exibidas no dashboard do projeto, 2 - Variáveis->Pré-visualização, onde é possível visualizar quais variáveis serão substituídas pela etapa correspondente.
Passando para a parte mais importante: implantação de modelos Yaml em clusters Kubernetes. Eles foram criados na seção Biblioteca->Step Templates. Abaixo, a equipe do ODS apresentou uma captura de tela utilizando seus parâmetros. Para cada parâmetro, você pode escolher uma tag, tipo e valor padrão, bem como adicionar uma descrição, o que é altamente recomendado.
O código neste caso ficou assim:
apiVersion: apps/v1 kind: Deployment metadata: name: '#{Octopus.Project.Name | ToLower}' namespace: #{Octopus.Environment.Name | ToLower} labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: replicas: #{Replicas} strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25% revisionHistoryLimit: 10 progressDeadlineSeconds: 600 selector: matchLabels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' template: metadata: labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: volumes: #{if usesidecar} - name: dump-storage persistentVolumeClaim: claimName: dumps-#{Octopus.Environment.Name | ToLower} #{/if} #{if MountFolders} #{each folder in MountFolders} - name: volume-#{folder | ToBase64 | Replace "\W" X | ToLower} hostPath: path: #{folder} type: DirectoryOrCreate #{/each} #{/if} - name: logs-volume hostPath: path: #{LogsDir} type: DirectoryOrCreate - name: appsettings secret: secretName: #{Octopus.Project.Name | ToLower} #{if Secrets} #{each secret in Secrets} - name: #{secret.name} secret: secretName: #{secret.name} #{/each} #{/if} #{if usesidecar} - name: diagnostics emptyDir: {} - name: dumps configMap: name: dumps defaultMode: 511 #{/if} containers: - name: #{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}-container image: #{DockerRegistry}/projectname.#{Octopus.Project.Name | ToLower}:#{Octopus.Release.Notes} #{if resources} resources: #{each resource in resources} #{resource.Key}: #{each entry in resource.Value} #{entry.Key}: #{entry.Value} #{/each} #{/each} #{/if} ports: - name: http containerPort: 80 protocol: TCP env: - value: "Development" name: "ASPNETCORE_ENVIRONMENT" - name: DD_ENV value: "#{Octopus.Environment.Name | ToLower}" - name: DD_SERVICE value: "#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}" - name: DD_VERSION value: "1.0.0" - name: DD_AGENT_HOST value: "#{DatadogAgentHost}" - name: DD_TRACE_ROUTE_TEMPLATE_RESOURCE_NAMES_ENABLED value: "true" - name: DD_RUNTIME_METRICS_ENABLED value: "true" volumeMounts: #{if usesidecar} - name: dump-storage mountPath: /tmp/dumps #{/if} #{if MountFolders} #{each folder in MountFolders} - mountPath: #{folder} name: volume-#{folder | ToBase64 | Replace "\W" X | ToLower} #{/each} #{/if} - mountPath: #{LogsDir} name: logs-volume #{if usesidecar} - name: diagnostics mountPath: /tmp #{/if} - name: appsettings readOnly: true mountPath: /app/appsettings.json subPath: appsettings.json #{if Secrets} #{each secret in Secrets} - name: #{secret.name} readOnly: true mountPath: #{secret.mountPath} subPath: #{secret.subPath} #{/each} #{/if} readinessProbe: httpGet: path: hc port: http scheme: HTTP initialDelaySeconds: #{InitialDelaySeconds} imagePullPolicy: IfNotPresent securityContext: {} #{if usesidecar} - name: sidecar image: '#{DockerRegistry}/monitor:3' command: - /bin/sh args: - '-c' - while true; do . /app/init.sh; sleep 1m;done env: - name: USE_MEMORY value: '2048' - name: PROJECT value: "#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}" resources: {} volumeMounts: - name: diagnostics mountPath: /tmp - name: dump-storage mountPath: /tmp/dumps - name: dumps mountPath: /app/init.sh subPath: init.sh shareProcessNamespace: true #{/if} affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: environment operator: In values: - "#{Node}" --- apiVersion: v1 kind: Service metadata: name: #{Octopus.Project.Name | ToLower} namespace: #{Octopus.Environment.Name | ToLower} labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: type: ClusterIP selector: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' ports: - name: http port: 80 targetPort: http protocol: TCP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: ingress.kubernetes.io/ssl-redirect: 'false' nginx.ingress.kubernetes.io/ssl-redirect: 'false' cert-manager.io/cluster-issuer: "letsencrypt-cluster-issuer" cert-manager.io/renew-before: '#{LetsencryptRenewBefore}' kubernetes.io/ingress.class: nginx #{if IngressAnnotations} #{each annotation in IngressAnnotations} #{annotation.Key}: #{annotation.Value} #{/each} #{/if} name: #{Octopus.Project.Name | ToLower} namespace: #{Octopus.Environment.Name | ToLower} labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: tls: #{if ExternalHost} #{each host in ExternalHost} - hosts: - #{host} secretName: #{Octopus.Project.Name | ToLower}-#{host | ToBase64 | Replace "\W" X | ToLower}-tls #{/each} #{/if} rules: #{if ExternalHost} #{each host in ExternalHost} - host: '#{host}' http: paths: - path: / pathType: ImplementationSpecific backend: service: name: #{Octopus.Project.Name | ToLower} port: name: http #{/each} #{/if} #{if usesidecar} --- apiVersion: v1 kind: ConfigMap metadata: name: dumps namespace: #{Octopus.Environment.Name | ToLower} data: init.sh: |- #!/usr/bin/env bash mem=$(ps aux | awk '{print $6}' | sort -rn | head -1) mb=$(($mem/1024)) archiveDumpPath="/tmp/dumps/$PROJECT-$(date +"%Y%m%d%H%M%S").zip" fullPathGc="/tmp/$PROJECT-$(date +"%Y%m%d%H%M%S").dump" echo "mem:" $mb" project:" $PROJECT "use:" $USE_MEMORY if [ "$mb" -gt "$USE_MEMORY" ]; then export USE_MEMORY=$(($USE_MEMORY*2)) pid=$(dotnet-dump ps | awk '{print $1}') dotnet-dump collect -p $pid -o $fullPathGc zip $fullPathGc.zip $fullPathGc mv $fullPathGc.zip $archiveDumpPath rm $fullPathGc fi #{/if}
Todas as variáveis no Octopus foram especificadas no seguinte formato no código: '#{Octopus.Project.Name | ToLower}'
, onde a última parte indica a conversão para minúsculas.
O último arquivo de configuração foi criado para salvar automaticamente o estado dos serviços .NET quando atingirem um determinado limite de uso de memória. Isso ajudou significativamente a identificar vazamentos de memória durante o desenvolvimento e a corrigir prontamente os serviços.
Por fim, o painel de serviço ficou assim:
As equipes de desenvolvimento e teste acharam muito conveniente trabalhar com este painel.
Resultados de otimização:
Posteriormente, o SDG implementou muitos outros recursos no Octopus. Por exemplo, desligamento automático de clusters à noite de acordo com uma programação.
No entanto, a busca pela perfeição não tem limites. A equipe do Social Discovery Group avançou em seu desenvolvimento dominando o Azure DevOps. Eles configuraram um processo no Azure DevOps dentro de um ecossistema no Helm, ainda mais abrangente e eficiente. Isso será abordado no próximo artigo.
Adoraríamos ouvir sobre sua experiência na configuração e otimização de CI/CD usando Octopus e TeamCity. Compartilhe seus insights e dicas!