paint-brush
Octopus와 TeamCity를 사용하여 CI/CD를 간소화하는 방법~에 의해@socialdiscoverygroup
4,027 판독값
4,027 판독값

Octopus와 TeamCity를 사용하여 CI/CD를 간소화하는 방법

~에 의해 Social Discovery Group13m2024/05/14
Read on Terminal Reader

너무 오래; 읽다

명확하고 체계적인 프로세스가 없으면 개발자가 효과적으로 협업하는 데 어려움을 겪어 소프트웨어 업데이트 제공이 지연될 수 있다는 것은 누구나 다 아는 사실입니다. 이 기사에서 Social Discovery Group 팀은 TeamCity와 Octopus를 혼합하여 편리하고 유연한 CI/CD 파이프라인을 구성하는 방법을 공유합니다.
featured image - Octopus와 TeamCity를 사용하여 CI/CD를 간소화하는 방법
Social Discovery Group HackerNoon profile picture
0-item


명확하고 체계적인 프로세스가 없으면 개발자가 효과적으로 협업하는 데 어려움을 겪어 소프트웨어 업데이트 제공이 지연될 수 있다는 것은 누구나 다 아는 사실입니다. 몇 년 전 Social Discovery Group 팀은 최적이 아닌 CI/CD 프로세스 문제에 직면했습니다. 당시 팀에서는 각각 장점이 있는 TeamCity와 Octopus를 사용했습니다. 예를 들어 Octopus는 배포에 편리한 반면 TeamCity는 자동화된 테스트에 적합하고 프로젝트 빌드에 충분히 편리합니다. 최대한 편리하고 구성이 유연한 포괄적이고 시각적으로 매력적인 CI/CD 파이프라인을 구축하려면 여러 도구를 혼합하여 사용해야 합니다. 코드는 여러 프로젝트에 대해 Bitbucket의 로컬 저장소에 저장되었습니다. SDG 팀은 문제를 연구하고 기존 도구를 사용하여 프로세스를 최적화하기로 결정했습니다.


주요 최적화 목표:

  1. TeamCity에서 지정된 환경으로 자동 빌드 및 배포.
  2. 빌드 이름 지정: 빌드를 이름으로 구별하기 위해 "릴리스"가 마스터 브랜치에 추가됩니다.
  3. 각 서비스의 각 테스트 환경에서 해당 브랜치로 Push 시 자동으로 빌드 및 배포됩니다.
  4. 테스트 및 개발 환경에 대한 배포가 준비 단계로 배포된 다음 프로덕션으로 배포되기 전에 완료되어야 하는 프로세스를 설정합니다. 이는 Octopus에서 구현되었습니다.


SDG 팀은 빌드 및 자동화 테스트에 TeamCity를 사용하고 배포에 Octopus를 사용하기로 결정했습니다.


TeamCity에 구현된 내용은 다음과 같습니다.

  1. TeamCity는 무료 버전에서 세 명의 에이전트를 사용할 수 있도록 허용하는데, 이는 SDG 팀에게 충분했습니다. 그들은 새 에이전트를 설치하고 이를 풀에 추가한 후 템플릿에 적용했습니다.

  2. 최신 버전의 TeamCity를 사용할 당시 팀은 Ubuntu Server를 작업하고 있었습니다. 스크린샷은 팀에서 사용하는 추가 플러그인을 보여줍니다.



  1. 도구에서 팀은 보고서 생성을 위한 Allure 2.14.0 및 Nuget 5.5.1과 같은 플러그인을 추가했습니다.
  2. 유사한 작업의 실행을 단순화하기 위해 SDG 팀은 NuGet 및 서비스 등 다양한 배포 유형을 위한 여러 템플릿을 만들었습니다.
  3. 이러한 각 템플릿에는 아래 스크린샷에 반영된 여러 단계가 포함되어 있습니다.


NuGet 배포는 다음과 같습니다.




브랜치가 마스터인지 아닌지에 따라 릴리스에 "-release"가 추가되었다는 점은 주목할 가치가 있습니다(3, 4단계).


서비스 배포는 아래에서 볼 수 있습니다.


각 서비스에 대해 시스템 변수(서비스 이름, %build.number% 등)를 기준으로 해당 변수를 대체했습니다.


Docker 빌드 단계의 예가 스크린샷에 나와 있습니다.


각 프로젝트 저장소에는 해당 Dockerfile이 포함되어 있습니다.


앞서 언급한 4단계와 5단계의 차이점은 다음과 같습니다.



%deploymentTarget% 변수는 배포 중에 Octopus의 해당 단계(예: 테스트, 개발)가 전달되는 환경 매개 변수로 사용되었습니다. 개발팀의 각 분기(구성)에 변경 사항이 푸시되면 해당 테스트 환경에 대한 빌드 및 소프트웨어 배포가 자동으로 수행되었습니다. 설정은 아래 스크린샷에 표시됩니다. Octopus와 연결하려면 octopus.apiKey 및 octopus.url이라는 두 개의 전역 매개변수를 추가해야 합니다.



또한 SDG 팀은 연결 섹션의 모든 프로젝트에 대해 공통 NuGet 저장소와 Container Registry를 연결했습니다.


또한 SDG에서는 이메일 알리미 섹션에서 이메일 알림을 구성하고, 백업 섹션에서 백업을 설정하고, 필요한 그룹을 생성하고, 적절한 역할을 할당하고, 필수 그룹에 사용자를 추가할 것을 권장합니다. 주요 설정이 완료되었으며, 결론적으로 팀에서는 정기적으로 업데이트를 확인하고 한 달에 한 번씩 TeamCity를 업데이트할 것을 권장합니다.


다음으로 Social Discovery Group 팀은 Octopus 구성 작업을 진행했습니다. 설치 세부 사항, 기본 사용자 권한 설정 및 기타 측면은 직접 쉽게 수행할 수 있으므로 이 문서에서는 설명하지 않습니다. 팀은 라이브러리 섹션에 구성된 수명 주기를 즉시 해결했습니다. 아래 스크린샷에서 SDG 팀의 흐름 예시를 볼 수 있습니다.



그런 다음 팀은 변수 세트에서 테마별로 필요한 모든 변수 그룹을 만들었습니다. 각 변수에 대해 값이 설정되고 환경, 대상 및 대상 역할(태그)에 대한 종속성이 설정되었습니다. 아래 스크린샷에 예가 나와 있습니다.



Kubernetes의 클러스터는 대상 역할을 했으며 대상 역할은 해당 클러스터 또는 컴퓨터 환경에 연결된 태그였습니다. 이 모든 것은 인프라 섹션에서 구성할 수 있습니다.


프로젝트를 그룹화할 수도 있고 배포된 서비스, 단계 및 버전을 표시하도록 편리한 대시보드를 설정할 수도 있습니다.

SDG의 배포 프로세스는 다음과 같습니다. 모든 테스트 단계를 하나의 단계로 결합하고 스테이지 및 라이브 단계와 유사하게 공통 템플릿을 생성했습니다.


아래 스크린샷은 이것이 SDG 팀의 모습을 보여줍니다.

오른쪽에는 이전에 설명한 수명 주기가 선택되었습니다. 패키지 배포 단계에는 매우 간단한 기본 설정이 포함되어 있습니다.

Raw Kubernetes Yaml 배포 단계에서 SDG 팀은 자체 작성 범용 Yaml 템플릿을 사용했습니다. 이 예(Kubernetes 스크립트)에서는 아래에 더 자세히 설명되어 있습니다. 빨간색으로 표시된 해당 매개변수도 대체되었습니다. 참고로 필요한 전역 변수 그룹은 Variables->Variable Set 메뉴에서 연결했고, 프로젝트별 변수는 Variables->Project 메뉴에서 우선순위가 더 높은 것으로 설정했다는 점에 주목할 필요가 있습니다.


이 기사에서 SDG 팀은 프로젝트에 로고 추가, 트리거 설정 또는 기타 사소한 세부 사항과 같은 세부 사항을 건너뛰기로 결정했습니다. 두 가지 중요한 메뉴 항목에 중점을 두겠습니다. 1 - 릴리스(릴리스)에서는 특정 릴리스의 버전과 생성 날짜를 항상 볼 수 있습니다. 이 정보는 프로젝트 대시보드 2 - 변수->미리보기에도 표시되며, 여기서 해당 단계를 대체할 변수를 확인할 수 있습니다.





가장 중요한 부분인 Kubernetes 클러스터에 Yaml 템플릿 배포로 이동합니다. 이는 라이브러리->단계 템플릿 섹션에서 생성되었습니다. 아래에서 SDG 팀은 해당 매개변수를 사용한 스크린샷을 제시했습니다. 각 매개변수에 대해 태그, 유형, 기본값을 선택할 수 있을 뿐만 아니라 설명도 추가할 수 있으므로 적극 권장됩니다.




이 경우의 코드는 다음과 같습니다.


 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}' , 여기서 마지막 부분은 소문자로 변환됨을 나타냅니다.


특정 메모리 사용 제한에 도달하면 .NET 서비스의 상태를 자동으로 저장하기 위해 마지막 구성 파일이 생성되었습니다. 이는 개발 중 메모리 누수를 식별하고 서비스를 즉시 수정하는 데 큰 도움이 되었습니다.


마지막으로 서비스 대시보드는 다음과 같습니다.


개발 및 테스트 팀은 이 대시보드를 사용하는 것이 매우 편리하다는 것을 알았습니다.


최적화 결과:


  1. SDG팀은 개발 속도와 편의성을 크게 향상시키는 효율적인 CI/CD 프로세스를 구축했습니다. 팀은 이 프로세스 프레임워크 내에서 꽤 오랫동안 작업했습니다.
  2. SDG는 또한 편리한 TeamCity 도구 및 자동화된 서비스 배포에 자동화된 테스트를 도입했습니다.
  3. 팀에서는 사용자 친화적인 Octopus 대시보드를 통해 액세스 권한을 구성하고 배포 및 환경을 관리할 수 있었습니다.

이후 SDG는 Octopus에 다른 많은 기능을 구현했습니다. 예를 들어 일정에 따라 밤에 클러스터를 자동 종료합니다.


그러나 완벽함을 추구하는 데에는 한계가 없습니다. Social Discovery Group 팀은 Azure DevOps를 마스터하여 개발을 발전시켰습니다. 그들은 Helm의 하나의 생태계 내에서 Azure DevOps의 프로세스를 훨씬 더 포괄적이고 효율적으로 설정했습니다. 그 내용은 다음 기사에서 다룰 것입니다.


Octopus와 TeamCity를 사용하여 CI/CD를 설정하고 최적화한 경험에 대해 듣고 싶습니다. 통찰력과 팁을 공유하세요!