Google Cloud 지속적 배포에 대한 더 나은 접근 방식
게시 됨: 2022-03-11연속 배포(CD)는 새 코드를 프로덕션에 자동으로 배포하는 방법입니다. 대부분의 지속적 배포 시스템은 단위 및 기능 테스트를 실행하여 배포할 코드가 실행 가능한지 확인하고 모든 것이 좋아 보이면 배포가 시작됩니다. 롤아웃 자체는 일반적으로 코드가 예상대로 작동하지 않는 경우 롤백할 수 있도록 단계적으로 발생합니다.
AWS 스택, Google Cloud 스택, Bitbucket 파이프라인 등과 같은 다양한 도구를 사용하여 자신의 CD 파이프라인을 구현하는 방법에 대한 블로그 게시물이 부족하지 않습니다. 다음과 같아야 합니다. 먼저 빌드하고 빌드된 단일 파일만 테스트 및 배포합니다.
이 기사에서는 먼저 빌드한 다음 배포 최종 아티팩트에 대해 테스트를 실행하는 이벤트 기반 연속 배포 파이프라인을 구축할 것입니다. 이는 테스트 결과를 보다 안정적으로 만들 뿐만 아니라 CD 파이프라인을 쉽게 확장할 수 있게 해줍니다. 다음과 같이 보일 것입니다.
- 소스 리포지토리에 커밋이 이루어집니다.
- 그러면 연결된 이미지의 빌드가 트리거됩니다.
- 테스트는 빌드된 아티팩트에서 실행됩니다.
- 모든 것이 좋아 보이면 이미지가 프로덕션에 배포됩니다.
이 기사에서는 최소한 Kubernetes 및 컨테이너 기술에 대해 어느 정도 익숙하다고 가정하지만 익숙하지 않거나 복습을 사용할 수 있는 경우 Kubernetes란?을 참조하세요. 컨테이너화 및 배포 가이드.
대부분의 CD 설정 문제
다음은 대부분의 CD 파이프라인에 대한 내 문제입니다. 일반적으로 빌드 파일의 모든 작업을 수행합니다. 이에 대해 내가 읽은 대부분의 블로그 게시물은 빌드 파일에 관계없이 다음 시퀀스의 일부 변형이 있습니다(Google Cloud Build의 경우 cloudbuild.yaml
, Bitbucket의 경우 bitbucket-pipeline.yaml
).
- 테스트 실행
- 빌드 이미지
- 이미지를 컨테이너 저장소로 푸시
- 새 이미지로 환경 업데이트
최종 아티팩트에서 테스트를 실행하고 있지 않습니다.
이 순서대로 작업을 수행하여 테스트를 실행합니다. 성공하면 이미지를 빌드하고 나머지 파이프라인을 계속 진행합니다. 빌드 프로세스가 테스트를 더 이상 통과하지 못하는 방식으로 이미지를 변경하면 어떻게 됩니까? 제 생각에는 아티팩트(최종 컨테이너 이미지)를 생성하는 것으로 시작해야 하며 이 아티팩트는 빌드와 프로덕션에 배포되는 시간 사이에 변경되지 않아야 합니다. 이렇게 하면 해당 아티팩트(테스트 결과, 크기 등)에 대한 데이터가 항상 유효합니다.
빌드 환경에는 "왕국의 열쇠"가 있습니다.
빌드 환경을 사용하여 프로덕션 스택에 이미지를 배포함으로써 프로덕션 환경을 효과적으로 변경할 수 있습니다. 소스 리포지토리에 대한 쓰기 액세스 권한이 있는 모든 사람이 이제 프로덕션 환경에서 원하는 모든 작업을 수행할 수 있기 때문에 이것을 매우 나쁜 것으로 봅니다.
마지막 단계가 실패하면 전체 파이프라인을 다시 실행해야 합니다.
마지막 단계가 실패하면(예: 자격 증명 문제로 인해) 전체 파이프라인을 다시 실행해야 하므로 다른 작업을 수행하는 데 더 나은 시간과 기타 리소스를 사용할 수 있습니다.
이것은 나를 최종 요점으로 이끕니다.
당신의 단계는 독립적이지 않습니다.
더 일반적인 의미에서 독립적인 단계를 사용하면 파이프라인에서 더 많은 유연성을 가질 수 있습니다. 파이프라인에 기능 테스트를 추가하려고 한다고 가정해 보겠습니다. 하나의 빌드 파일에 단계를 포함하면 빌드 환경이 기능 테스트 환경을 가동하고 테스트를 실행해야 합니다(대부분 순차적으로). 단계가 독립적인 경우 "이미지 빌드" 이벤트에 의해 시작되는 단위 테스트와 기능 테스트를 모두 가질 수 있습니다. 그런 다음 자체 환경에서 병렬로 실행됩니다.
나의 이상적인 CD 설정
제 생각에는 이 문제에 접근하는 더 좋은 방법은 일련의 독립적인 단계를 모두 이벤트 메커니즘으로 함께 연결하는 것입니다.
이것은 이전 방법에 비해 몇 가지 장점이 있습니다.
서로 다른 이벤트에 대해 몇 가지 독립적인 작업을 수행할 수 있습니다.
위에서 언급했듯이 새 이미지의 성공적인 빌드는 "성공적인 빌드" 이벤트를 게시합니다. 차례로, 이 이벤트가 트리거될 때 여러 일이 실행되도록 할 수 있습니다. 우리의 경우 단위 및 기능 테스트를 시작합니다. 빌드 실패 이벤트가 트리거되거나 테스트가 통과하지 못한 경우 개발자에게 경고하는 것과 같은 것을 생각할 수도 있습니다.
각 환경에는 고유한 권한이 있습니다.
각 단계를 자체 환경에서 수행함으로써 단일 환경이 모든 권한을 가질 필요가 없습니다. 이제 빌드 환경은 빌드만 가능하고 테스트 환경은 테스트만 가능하며 전개 환경은 전개만 가능합니다. 이렇게 하면 이미지가 일단 구축되면 변경되지 않을 것이라는 확신을 가질 수 있습니다. 생성된 아티팩트는 프로덕션 스택에서 끝나는 것입니다. 또한 하나의 자격 증명 집합을 한 단계에 연결할 수 있으므로 파이프라인의 어떤 단계에서 무엇을 하는지 쉽게 감사할 수 있습니다.
더 많은 유연성이 있습니다.
각 성공적인 빌드에 대해 누군가에게 이메일을 보내고 싶습니까? 해당 이벤트에 반응하고 이메일을 보내는 무언가를 추가하기만 하면 됩니다. 간단합니다. 빌드 코드를 변경할 필요가 없고 소스 리포지토리에 있는 누군가의 이메일을 하드코딩할 필요도 없습니다.
재시도가 더 쉽습니다.
독립적인 단계가 있다는 것은 한 단계가 실패하더라도 전체 파이프라인을 다시 시작할 필요가 없다는 것을 의미합니다. 실패 조건이 일시적이거나 수동으로 수정된 경우 실패한 단계를 다시 시도할 수 있습니다. 이를 통해 보다 효율적인 파이프라인이 가능합니다. 빌드 단계에 몇 분이 걸리면 배포 환경에 클러스터에 대한 쓰기 액세스 권한을 부여하는 것을 잊었다고 해서 이미지를 다시 빌드하지 않아도 되는 것이 좋습니다.
Google Cloud 지속적 배포 구현
Google Cloud Platform에는 짧은 시간에 아주 적은 코드로 이러한 시스템을 구축하는 데 필요한 모든 도구가 있습니다.
우리의 테스트 애플리케이션은 정적 텍스트를 제공하는 간단한 Flask 애플리케이션입니다. 이 애플리케이션은 더 광범위한 인터넷에 서비스를 제공하는 Kubernetes 클러스터에 배포됩니다.
앞서 소개한 파이프라인의 간소화된 버전을 구현하겠습니다. 기본적으로 테스트 단계를 제거하여 이제 다음과 같이 보입니다.
- 소스 리포지토리에 새 커밋이 만들어집니다.
- 그러면 이미지 빌드가 트리거됩니다. 성공하면 컨테이너 저장소로 푸시되고 이벤트가 Pub/Sub 주제에 게시됩니다.
- 작은 스크립트가 해당 주제를 구독하고 이미지의 매개변수를 확인합니다. 요청한 내용과 일치하면 Kubernetes 클러스터에 배포됩니다.
다음은 파이프라인을 그래픽으로 나타낸 것입니다.
흐름은 다음과 같습니다.
- 누군가가 우리 저장소에 커밋합니다.
- 그러면 소스 리포지토리를 기반으로 Docker 이미지를 빌드하는 클라우드 빌드가 트리거됩니다.
- 클라우드 빌드는 이미지를 컨테이너 저장소로 푸시하고 클라우드 pub/sub에 메시지를 게시합니다.
- 이것은 게시된 메시지의 매개변수(빌드 상태, 빌드된 이미지 이름 등)를 확인하는 클라우드 기능을 트리거합니다.
- 매개변수가 양호하면 클라우드 기능이 새 이미지로 Kubernetes 배포를 업데이트합니다.
- Kubernetes는 새 이미지로 새 컨테이너를 배포합니다.
소스 코드
우리의 소스 코드는 정적 텍스트를 제공하는 매우 간단한 Flask 앱입니다. 다음은 우리 프로젝트의 구조입니다:
├── docker │ ├── Dockerfile │ └── uwsgi.ini ├── k8s │ ├── deployment.yaml │ └── service.yaml ├── LICENSE ├── Pipfile ├── Pipfile.lock └── src └── main.py
Docker 디렉토리에는 Docker 이미지를 빌드하는 데 필요한 모든 것이 포함되어 있습니다. 이미지는 uWSGI 및 Nginx 이미지를 기반으로 하며 종속성을 설치하고 앱을 올바른 경로에 복사합니다.
k8s 디렉토리에는 Kubernetes 구성이 포함되어 있습니다. 하나의 서비스와 하나의 배포로 구성됩니다. 배포는 Dockerfile 에서 빌드된 이미지를 기반으로 하나의 컨테이너를 시작합니다. 그런 다음 서비스는 공용 IP 주소가 있는 로드 밸런서를 시작하고 앱 컨테이너로 리디렉션합니다.

클라우드 빌드
클라우드 빌드 구성 자체는 클라우드 콘솔 또는 GCP 명령줄을 통해 수행할 수 있습니다. 저는 클라우드 콘솔을 사용하기로 했습니다.
여기에서는 모든 분기에 대한 커밋에 대한 이미지를 빌드하지만 예를 들어 개발 및 프로덕션에 대해 다른 이미지를 가질 수 있습니다.
빌드가 성공하면 클라우드 빌드가 이미지를 자체적으로 컨테이너 레지스트리에 게시합니다. 그런 다음 클라우드 빌드 게시/구독 주제에 메시지를 게시합니다.
클라우드 빌드는 또한 빌드가 진행 중일 때와 빌드가 실패할 때 메시지를 게시하므로 해당 메시지에 반응하도록 할 수도 있습니다.
클라우드 빌드의 게시/구독 알림에 대한 문서는 여기에 있으며 메시지 형식은 여기에서 찾을 수 있습니다.
클라우드 게시/구독
클라우드 콘솔의 클라우드 게시/구독 탭을 보면 클라우드 빌드가 클라우드 빌드라는 주제를 생성했음을 알 수 있습니다. 클라우드 빌드가 상태 업데이트를 게시하는 곳입니다.
클라우드 기능
이제 클라우드 빌드 주제에 게시된 모든 메시지에 대해 트리거되는 클라우드 기능을 생성할 것입니다. 다시 말하지만, 클라우드 콘솔이나 Google Cloud 명령줄 유틸리티를 사용할 수 있습니다. 제 경우에는 클라우드 빌드를 사용하여 변경 사항이 있을 때마다 클라우드 기능을 배포했습니다.
클라우드 기능의 소스 코드는 여기에 있습니다.
먼저 이 클라우드 기능을 배포하는 코드를 살펴보겠습니다.
steps: - name: 'gcr.io/cloud-builders/gcloud' id: 'test' args: ['functions', 'deploy', 'new-image-trigger', '--runtime=python37', '--trigger-topic=cloud-builds', '--entry-point=onNewImage', '--region=us-east1', '--source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME']
여기서는 Google Cloud Docker 이미지를 사용합니다. 이를 통해 GCcloud 명령을 쉽게 실행할 수 있습니다. 우리가 실행하는 것은 터미널에서 직접 다음 명령을 실행하는 것과 같습니다.
gcloud functions deploy new-image-trigger --runtime=python37 --trigger-topic=cloud-builds --entry-point=onNewImage --region=us-east1 --source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME
Google Cloud에 Python 3.7 런타임을 사용하고 클라우드 빌드 주제의 새 메시지에 의해 트리거되는 새 클라우드 기능(또는 해당 지역에 해당 이름의 기능이 이미 있는 경우 교체)을 배포하도록 요청하고 있습니다. 또한 해당 기능의 소스 코드를 찾을 위치를 Google에 알려줍니다(여기서 PROJECT_ID 및 REPO_NAME은 빌드 프로세스에 의해 설정되는 환경 변수입니다). 또한 진입점으로 호출할 함수를 알려줍니다.
참고로 이것이 작동하려면 클라우드 기능을 배포할 수 있도록 cloudbuild 서비스 계정에 "클라우드 기능 개발자"와 "서비스 계정 사용자"를 모두 부여해야 합니다.
다음은 Cloud 함수 코드의 주석 처리된 스니펫입니다.
진입점 데이터에는 게시/구독 주제에 대해 수신된 메시지가 포함됩니다.
def onNewImage(data, context):
첫 번째 단계는 환경에서 해당 특정 배포에 대한 변수를 가져오는 것입니다(클라우드 콘솔에서 클라우드 기능을 수정하여 변수를 정의했습니다.
project = os.environ.get('PROJECT') zone = os.environ.get('ZONE') cluster = os.environ.get('CLUSTER') deployment = os.environ.get('DEPLOYMENT') deploy_image = os.environ.get('IMAGE') target_container = os.environ.get('CONTAINER')
메시지 구조가 예상한 것과 일치하는지 확인하고 빌드가 성공하고 하나의 이미지 아티팩트를 생성했는지 확인하는 부분을 건너뛸 것입니다.
다음 단계는 빌드된 이미지가 배포하려는 이미지인지 확인하는 것입니다.
image = decoded_data['results']['images'][0]['name'] image_basename = image.split('/')[-1].split(':')[0] if image_basename != deploy_image: logging.error(f'{image_basename} is different from {deploy_image}') return
이제 Kubernetes 클라이언트를 얻고 수정하려는 배포를 검색합니다.
v1 = get_kube_client(project, zone, cluster) dep = v1.read_namespaced_deployment(deployment, 'default') if dep is None: logging.error(f'There was no deployment named {deployment}') return
마지막으로 새 이미지로 배포를 패치합니다. Kubernetes가 롤아웃을 처리합니다.
for i, container in enumerate(dep.spec.template.spec.containers): if container.name == target_container: dep.spec.template.spec.containers[i].image = image logging.info(f'Updating to {image}') v1.patch_namespaced_deployment(deployment, 'default', dep)
결론
이것은 내가 CD 파이프라인에서 어떻게 구성되는 것을 좋아하는지에 대한 아주 기본적인 예입니다. 어떤 pub/sub 이벤트가 무엇을 트리거하는지 변경하여 더 많은 단계를 수행할 수 있습니다.
예를 들어 이미지 내에서 테스트를 실행하고 성공 시 이벤트를 게시하고 실패 시 또 다른 이벤트를 게시하고 배포를 업데이트하거나 결과에 따라 경고하여 이에 대응하는 컨테이너를 실행할 수 있습니다.
우리가 구축한 파이프라인은 매우 간단하지만 다른 부분에 대해 다른 클라우드 기능을 작성할 수 있습니다(예: 단위 테스트를 위반한 코드를 커밋한 개발자에게 이메일을 보내는 클라우드 기능).
보시다시피 빌드 환경은 Kubernetes 클러스터에서 아무 것도 변경할 수 없으며 배포 코드(클라우드 기능)는 빌드된 이미지를 수정할 수 없습니다. 우리의 권한 분리는 좋아 보이며 불량 개발자가 프로덕션 클러스터를 중단시키지 않을 것이라는 사실을 알고 잠을 잘 수 있습니다. 또한 운영 지향적인 개발자에게 클라우드 기능 코드에 대한 액세스 권한을 부여하여 수정하거나 개선할 수 있습니다.
질문, 의견 또는 개선 사항이 있는 경우 아래 의견에 자유롭게 문의하세요.