명확하고 체계적인 프로세스가 없으면 개발자가 효과적으로 협업하는 데 어려움을 겪어 소프트웨어 업데이트 제공이 지연될 수 있다는 것은 누구나 다 아는 사실입니다. 몇 년 전 Social Discovery Group 팀은 최적이 아닌 CI/CD 프로세스 문제에 직면했습니다. 당시 팀에서는 각각 장점이 있는 TeamCity와 Octopus를 사용했습니다. 예를 들어 Octopus는 배포에 편리한 반면 TeamCity는 자동화된 테스트에 적합하고 프로젝트 빌드에 충분히 편리합니다. 최대한 편리하고 구성이 유연한 포괄적이고 시각적으로 매력적인 CI/CD 파이프라인을 구축하려면 여러 도구를 혼합하여 사용해야 합니다. 코드는 여러 프로젝트에 대해 Bitbucket의 로컬 저장소에 저장되었습니다. SDG 팀은 문제를 연구하고 기존 도구를 사용하여 프로세스를 최적화하기로 결정했습니다.
주요 최적화 목표:
TeamCity에 구현된 내용은 다음과 같습니다.
NuGet 배포는 다음과 같습니다.
서비스 배포는 아래에서 볼 수 있습니다.
Docker 빌드 단계의 예가 스크린샷에 나와 있습니다.
앞서 언급한 4단계와 5단계의 차이점은 다음과 같습니다.
%deploymentTarget%
변수는 배포 중에 Octopus의 해당 단계(예: 테스트, 개발)가 전달되는 환경 매개 변수로 사용되었습니다. 개발팀의 각 분기(구성)에 변경 사항이 푸시되면 해당 테스트 환경에 대한 빌드 및 소프트웨어 배포가 자동으로 수행되었습니다. 설정은 아래 스크린샷에 표시됩니다. Octopus와 연결하려면 octopus.apiKey 및 octopus.url이라는 두 개의 전역 매개변수를 추가해야 합니다.
아래 스크린샷은 이것이 SDG 팀의 모습을 보여줍니다.
오른쪽에는 이전에 설명한 수명 주기가 선택되었습니다. 패키지 배포 단계에는 매우 간단한 기본 설정이 포함되어 있습니다.
Raw Kubernetes Yaml 배포 단계에서 SDG 팀은 자체 작성 범용 Yaml 템플릿을 사용했습니다. 이 예(Kubernetes 스크립트)에서는 아래에 더 자세히 설명되어 있습니다. 빨간색으로 표시된 해당 매개변수도 대체되었습니다. 참고로 필요한 전역 변수 그룹은 Variables->Variable Set 메뉴에서 연결했고, 프로젝트별 변수는 Variables->Project 메뉴에서 우선순위가 더 높은 것으로 설정했다는 점에 주목할 필요가 있습니다.
이 경우의 코드는 다음과 같습니다.
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}
Octopus의 모든 변수는 코드에서 다음 형식으로 지정되었습니다. '#{Octopus.Project.Name | ToLower}'
, 여기서 마지막 부분은 소문자로 변환됨을 나타냅니다.
마지막으로 서비스 대시보드는 다음과 같습니다.