📕 목차
1. CI(Continuous Integration)
2. Build Infrastructure
3. Build Setting : Docker compose
4. CI that only requires Docker
5. Containers related to the CI Pipeline
6. Practice
1. CI(Continuous Integration)
지금까지 우리는 아래의 두 가지를 배웠다.
- Docker를 이용해 Packaging 절차를 Docker 명령 하나로 전체 Application packaging 자동화
- Docker compose로 Application Architecture를 구축하여, Application 종료 및 시작 절차 자동화
그렇다면, 명령행 스크립트를 작성하여 정기적으로 실행시킨다면 자동화 서버도 구축할 수 있지 않을까?
📌 Continuous Integration
- CI pipeline은 Project마다 달라서, CI server를 구축하는 것이 굉장히 힘들다.
- 모든 Docker Project는 똑같은 단계를 거쳐 똑같은 결과물을 생성하므로 CI 절차의 일관성을 유지한다.
- 모든 실제 과정은 Container 내부에서 진행된다.
- Compiler나 SDK 설치 불필요
- 도중에 build가 깨져도 CI 절차가 해당 시점에서 중단
- 트래픽을 일으킬 별도의 사용자 역할 Container를 실행하여 E2E test도 가능하다.
- 전체 절차를 수행하기 위한 Infrastructure가 필요하다.
- 중앙 집권적인 형상 관리 시스템 (ex. Github)
- Image를 저장할 Docker Registry (ex. Docker hub)
- CI 작업을 수행할 자동화 Server (ex. Azyre DevOps)
📌 CI & CD
- 지속적 통합(Continuous Integration)
- Application 개발을 위한 자동화 프로세스
- 코드의 변경 사항을 정기적으로 build & test(단위 및 통합 테스트) 하여 Remote Repository로 통합
- 여러 명의 개발자가 동시에 작업을 하더라도 원활하게 진행이 가능하다
- 지속적 전달(Continuous Delivery)
- Remote Repository에 통합된 Application의 유효성 테스트(버그 & UI/UX & UAT 테스트) 등을 거쳐 배포 가능한 Application artifacts를 생성한다.
- 최종적으로 배포할 준비가 완료된 코드 베이스를 확보한다.
- 지속적 배포(Continuous Deployment)
- CI/CD 마지막 단계, 지속적 전달의 확장 개념
- Continuous Delivery 단계를 거친 artifact를 운영 환경에 배포한다.
✒️ DevOps Application Lifecycle
2. Build Infrastructure
📌 Private Infrastructure
services:
gogs:
image: diamol/gogs # 형상 관리 기능
ports:
- "3000:3000"
networks:
- infrastructure
registry.local: # 오픈 소스 도커 레지스트리
image: diamol/registry
ports:
- "5000:5000"
networks:
- infrastructure
jenkins: # CI server
image: diamol/jenkins
ports:
- "8080:8080"
networks:
- infrastructure
networks:
infrastructure:
name: build-infrastructure
# docker-compose-linux.yml
services:
jenkins:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# docker-compose-windows.yml
services:
jenkins:
volumes:
- type: npipe
source: \\.\pipe\docker_engine
target: \\.\pipe\docker_engine
- 신뢰성 있는 Managed service를 무료로 사용 가능한데, 굳이 직접 Infrastructure를 구축하는 경우는 드물다.
- 그래도 Docker를 이용한 Build System을 배워두면 좋다.
- 보안 혹은 속도 측면으로, 소스 코드와 패키징된 Image를 내부 Network 밖으로 반출하지 않아도 된다.
- 외부 Service에 의존하더라도 Internet 회선이 다운된 상황을 대비할 수 있다.
- 해당 compose script에서 사용한 세 가지 컴포넌트
- 형상 관리 기능 : Gogs
- Image 배포 : Open-source Docker registry
- 자동화 server : Jenkins
📌 서로 다른 자동화 수준
docker-compose -f docker-compose.yml -f docker-compose-windows.yml up -d
# 레지스트리 도메인을 hosts 파일에 추가 (window)
Add-Content -Value "127.0.0.1 registry.local" -Path C:\windows\System32\drivers\etc\hosts
- Registry Server : "registry.local:5000"을 Image tag에 포함시키면 바로 Image를 push 할 수 있다.
- Jenkins : 플러그인을 수동 설치하거나, Dockerfile에 스크립트를 포함시켜 자동으로 설치할 수 있다.
- Gogs : Container를 실행했더라도 수동 설정이 필요하다.
📌 Gogs
간단한 Gogs 환경 설정과 회원 가입 과정을 거친 후, 로그인을 하면 볼 수 있는 UI다.
새롭게 저장소를 만들고 해당 repo에 push를 하면 Jenkins에 의한 CI 작업이 진행된다.
docker container run 명령만으로 모든 것이 자동으로 진행되도록 패키징 할 수 없는 소프트웨어도 있다는 사실을 알아둬라.
📌 Jenkins
Jenkins는 Java Application으로, Container 실행 시 Jenkins를 실행하는 스크립트와 함께 Docker Image 형태로 패키징할 수 있다.
현재 Jenkins는 플러그인 설치, 사용자 등록, 파이프 라인 작업 생성 등. 모든 스크립트가 모두 적용된 상태다.
따라서 로그인만 수행하면 된다.
git remote add local http://localhost:3000/diamol/diamol.git
git push local
아, 자꾸 젠킨스 버전 오류 떠서 업뎃 해줬더니 베이스 이미지 자바 버전이 낮아서 application이 실행조차 안 된다...진짜 열받네. 일단 급한대로 다른 블로그에서 가져온 사진을 들고 왔다.
- Jenkins 작업 페이지에서 Pipeline을 구성하고 최근 Build 상태를 확인할 수 있다.
- Jenkins server는 코드 변경 여부를 확인하기 위해 1분에 한 번씩 Git server에 접근한다.
📌 Flow
와, 이 부분 진짜 이해하는데 애먹었다.
- Docker에서 실행된 Container는 Docker API를 통해 같은 Docker engine에서 실행된 Container와 연결된다. (Docker network를 통해서)
- Docker CLI는 Docker API를 호출하는 방식으로 동작한다.
- 기본적으로 Docker CLI는 Local PC에서 실행 중인 Docker API에 접속을 시도한다.
- Linux는 socket, Window는 명명된 파이프(named pipe)를 사용한다.
- CLI가 Container 안에서 실행됐다면, 해당 통신 또한 Host PC의 socket(named pipe)를 통해 이루어진다.
- 즉, Container가 Host PC의 Docker에 요청해서 다른 Container를 찾거나, 새로운 Container를 시작하고 종료하는 일까지 가능해진다.
- Container의 Application이 Host의 Docker에 접근 가능하다는 것은 보안 문제를 유발한다.
- 온전히 신뢰할 수 있는 Docker Image 외에는 적용해선 안 된다.
- 기본적으로 Docker CLI는 Local PC에서 실행 중인 Docker API에 접속을 시도한다.
혹시나 미래의 내가 또 이 개념이 헷갈려서 찾아올 때를 대비해 정리하자면,
우리가 터미널에서 명령어 쳐가면서 Docker 컨트롤하던 걸 Container가 해버릴 수 있다는 게 문제가 될 수 있음.
하지만 Docker에서 Jenkins가 동작하는 Container에서 Gogs Container의 동작을 감지하려면 Host의 Docker를 통할 수 밖에 없잖아?
그래서 신뢰가능하다고 가정하고 Jenkins Container를 Docker engine과 연결하여 Docker 및 Docker compose 명령 실행 권한을 제공해버린 것이다.
3. Build Setting : Docker compose
📌 Docker compose와 CI
1️⃣ 환경 변수 기본값
services:
numbers-api:
image: ${REGISTRY:-docker.io}/diamol/ch11-numbers-api:v3-build-${BUILD_NUMBER:-local}
networks:
- app-net
numbers-web:
image: ${REGISTRY:-docker.io}/diamol/ch11-numbers-web:v3-build-${BUILD_NUMBER:-local}
environment:
- RngApi__Url=http://numbers-api/rng
depends_on:
- numbers-api
networks:
- app-net
- (환경변수):-(기본값)
- '환경변수'가 정의되어 있지 않다면, '기본값'을 적용한다.
- ex. ${REGISTRY:-docker.io} : REGISTRY가 정의되어 있지 않으면 docker.io를 삽입한다.
- 최종적으로 위의 image 경로는 'docker.io/diamol/ch11-numbers-api:v3-build-local'이 된다.
- Image 이름을 유연하게 결정하도록 하는 설정은 CI 설정에서 중요한 부분이다.
- 언제 어디서나 Build가 같은 방식으로 수행되려면 Dockerfile을 하나로 유지하고, Compose 파일에 기본값을 설정한다면 Dockerfile에는 기본값을 적용하지 않아도 될 것이다.
2️⃣ Image tag 자동 생성
x-args: &args
args:
BUILD_NUMBER: ${BUILD_NUMBER:-0}
BUILD_TAG: ${BUILD_TAG:-local}
services:
numbers-api:
build:
context: numbers
dockerfile: numbers-api/Dockerfile.v4
<<: *args
numbers-web:
build:
context: numbers
dockerfile: numbers-web/Dockerfile.v4
<<: *args
networks:
app-net:
BUILD_TAG를 명시했다.
docker-compose -f docker-compose.yml -f docker-compose-build.yml build
docker image inspect -f '{{.Config.Labels}}' diamol/ch11-numbers-api:v3-build-local
- Docker compose는 사실상 docer image build 명령을 Service마다 실행해줄 뿐이다.
- 그럼에도 Docker Image가 하나인 경우에도 Compose를 통하는 쪽이 Image tag를 자동 생성해주므로 더 낫다.
3️⃣ Label
# numbers/numbers-api/Dockefile.v4 일부
FROM diamol/dotnet-aspnet
ARG BUILD_NUMBER=0
ARG BUILD_TAG=local
LABEL version="3.0"
LABEL build_number=${BUILD_NUMBER}
LABEL build_tag=${BUILD_TAG}
ENTRYPOINT ["dotnet", "Numbers.Api.dll"]
- 일반적으로 Docker resource(Container Image, Network volume) 대부분에 Label을 부여할 수 있다.
- 추가 데이터에 key-value 형태로 저장된다.
- Image에 포함시킬 수 있다.
- Registry에 push거나 pull하면 Label도 함께 따라온다.
- CI pipeline에서 Application build 과정이나 사후 추적을 하는 데 도움을 준다.
- docker image inspect 명령으로 Image가 build된 CI 작업을 추적 가능하다.
- 정확히 어느 버전의 코드가 build된 것인지 확인할 수 있어야 한다.
- 어떤 환경에서 실행 중인 Container라도 다시 소스 코드까지 역추적할 수 있는 감사 추적이다.
- LABEL instruction
- 정의된 key-value를 build되는 Image에 적용
- ARG Instruction로부터 환경 변수 값을 가져온다.
- ARG instruction
- ENV Instruction과 거의 같으나, Image를 build하는 시점에서만 유효하다.
- build 중에만 유효하므로, build된 Image로 실행한 Container에서는 해당 환경 변수가 정의되지 않는다.
- CI pipeline에서 만들어진 Image Label로 build number와 전체 build name이 결정된다.
- 'docker-compose-build.yml'의 args
- Build 시에 전달할 인자
- Dockerfile의 ARG Instruction으로 정의된 key와 일치해야 한다. (2에 올려놨음)
📌 Compose 설정을 우회해 Label 설정하기
docker image build -f numbers-api/Dockerfile.v4 --build-arg BUILD_TAG=ch11 -t numbers-api .
docker image inspect -f '{{.Config.Labels}}' numbers-api
- Compose로 build하지 않고 docker를 사용하면 ARG Instruction에 값을 주입하는 것도 가능하다.
4. CI that only requires Docker
📌 의존 모듈이 별도로 필요하지 않은 장점
Multi-stage build를 사용하여 Docker와 Docker compose 외의 도구를 사용하지 않아도 된다.
- 주요 Managed Build Service가 제공하는 안정적인 기능을 사용하면 된다.
- 여러 도구를 설치해야 하는 Build Server가 필요 없어진다.
- 개발 도구를 최신 버전으로 유지할 필요도 없다.
- Build script를 간결하게 유지할 수 있다.
- Local PC와 동일한 절차를 CI pipeline에서 그대로 사용 가능하다.
- 결과물이 동일하므로 Build Service를 교체하는 것도 간단하다.
📌 Build Pipeline
pipeline {
agent any
environment {
REGISTRY = "registry.local:5000"
}
stages {
stage('Verify') {
steps {
dir('ch11/exercises') {
sh 'chmod +x ./ci/00-verify.bat'
sh './ci/00-verify.bat'
}
}
}
stage('Build') {
steps {
dir('ch11/exercises') {
sh 'chmod +x ./ci/01-build.bat'
sh './ci/01-build.bat'
}
}
}
stage('Test') {
steps {
dir('ch11/exercises') {
sh 'chmod +x ./ci/02-test.bat'
sh './ci/02-test.bat'
}
}
}
stage('Push') {
steps {
dir('ch11/exercises') {
sh 'chmod +x ./ci/03-push.bat'
sh './ci/03-push.bat'
echo "Pushed web to http://$REGISTRY/v2/diamol/ch11-numbers-web/tags/list"
echo "Pushed api to http://$REGISTRY/v2/diamol/ch11-numbers-api/tags/list"
}
}
}
}
}
# 00-verify.bat
docker version && docker-compose version
# 01-build.bat
docker-compose -f docker-compose.yml -f docker-compose-build.yml build --pull
# 02-test.bat
docker-compose up -d && docker-compose ps && docker-compose down
# 03-push.bat
docker-compose -f docker-compose.yml -f docker-compose-build.yml push
- Jenkin CI 작업은 간단한 텍스트 파일로 설정 가능하다.
- 현재 pipeline 각 단계가 batch script를 실행하고 있다.
- 상업용 pipeline 작성 문법 대신 일반적인 script를 쓰면 Build Service를 갈아타기가 쉽다.
- 정의된 단계
- 검증 단계 (00-verify.bat)
- Docker 및 Docker compose의 version을 출력하는 내용을 담고 있다.
- Build에 필요한 의존 모듈인 Docker의 version을 build pipeline log 가장 처음에 출력하면 유용하다.
- 빌드 단계 (01-build.bat)
- Docker compse를 실행해 Image를 Build 한다.
- Jenkinsfile에 환경 변수 REGISTRY를 정의할 수 있다.
- 테스트 단계 (02-test.bat)
- Application을 실행하고 Container 명령을 출력한 뒤 종료한다.
- 실제 프로젝트라면 Application을 실행한 뒤, 다른 Container에서 E2E test를 진행했을 것이다.
- 푸시 단계 (03-push.bat)
- Build된 Image를 Local Registry에 push한다.
- 검증 단계 (00-verify.bat)
- --pull (옵션)
- Build에 필요한 Image를 무조건 최신 버전으로 새로 내려받음
- 항상 최신 보안 패치가 적용된 Base Image를 사용해 Build
- CI pipeline에서 Image 변경이 있을 때, Application에 미치는 영향을 최대한 빨리 알 수 있다.
- CI pipeline의 각 단계는 순서대로 실행한다.
- 중간에 실패한 단계가 있으면, 해당 작업은 그대로 종료된다.
- Build와 Test 단계를 모두 통과한 릴리즈 후보 Image만 registry에 push 될 수 있다.
📌 Local Reigstry에 저장된 릴리즈 Image 확인
curl.exe http://registry.local:5000/v2/_catalog
$ curl http://registry.local:5000/v2/diamol/ch11-numbers-api/tags/list
{"name":"diamol/ch11-numbers-api","tags":["v3-build-2"]}
$ curl http://registry.local:5000/v2/diamol/ch11-numbers-web/tags/list
{"name":"diamol/ch11-numbers-web","tags":["v3-build-2"]}
- hosts 파일에 추가한 도메인에서 Docker registry API를 호출 간으하다.
- 해당 Pipeline의 핵심은 복잡한 일은 Docker에 맡기면 된다는 것이다.
- 이것만 지키면, 어떤 CI server를 사용해도 기존의 script를 그대로 옮겨 Pipeline을 정의하면 된다.
5. Containers related to the CI Pipeline
📌 안전 소프트웨어 공급 체인(secure software supply chain)
- 알려진 취약점을 탐지하는 보안 검사 및 Image에 디지털 서명을 넣는 Container가 추가된 pipeline
- 보안 취약점 탐색 도구로 취약점이 발견되면 반드시 Build에 실패한다.
- 디지털 서명이 있는 Image로만 Container를 실행하게끔 설정할 수도 있다.
- 여러 기술이 도중에 추가되어도 Docker를 사용하면 Build process 최상위 Layer가 언제나 동일하므로 CI pipeline은 변경하지 않아도 된다.
6. Practice
Jenkins 버전 문제로 인해 현재 작업 불가능..ㅡㅡ 열받네
종속된 모든 Plugin 설치하고, 따로 Jenkins Container 만들면 되려나