gitlab ci/cd 도입하기 - 파이프라인 작성하기

junto·2024년 5월 26일
1

devops

목록 보기
3/9
post-thumbnail

gitlab-ci 는 기능이 많다. 자세한 기능은 https://docs.gitlab.com/ee/topics/build_your_application.html 를 참고하기로 하고, 아래 내 구체적인 요구사항을 해결하기 위한 파이프라인을 작성하는 데 초점을 둔다.

나의 요구사항

1. 작성된 코드는 컴파일이 성공해야 한다.

2. 작성된 코드는 테스트가 성공해야 한다.

3. 작성된 코드를 기반으로 빌드가 되어야 한다. 빌드 이력을 추적할 수 있도록 도커 레지스트리를 이용해 관리한다. (필요시 롤백)

4. 작성된 도커 이미지를 통해 서버에 배포가 되어야 한다.

5. 프론트 코드를 빌드해서 배포 파일을 서버에 보내야 한다. (nginx 라우팅 경로)

1. 컴파일 및 테스트 파이프라인

test:
  stage: test
  before_script: 
    - export PATH="/home/ubuntu/.sdkman/candidates/gradle/current/bin:$PATH"
    - cat $SPRING_ENV > src/main/resources/env.yml
  script:
    - gradle build test
  tags:
    - test
  • 미리 필요한 명령어의 $PATH을 추가한 후 프로젝트에서 필요한 환경 변수를 가져오는 작업이다.
  • cat대신 echo를 쓰게 되면 key: value 값이 아닌 해당 env.yml의 경로가 저장되니 주의하자! 이것 때문에 많은 시간을 소모했었다. 해당 이슈에 자세한 내용이 나온다. https://gitlab.com/gitlab-org/gitlab/-/issues/294292

2. 빌드 파이프라인


build:
  stage: build
  image: docker:26.1.3
  services:
    - docker:26.1.3-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - cat $SPRING_ENV > src/main/resources/env.yml
    - echo $DOCKER_REGISTRY_PASS | docker login --username $DOCKER_REGISTRY_USER --password-stdin
  script:
    - docker build -t $IMAGE_NAME:$IMAGE_TAG .
    - docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
    - docker push $IMAGE_NAME:$IMAGE_TAG
    - docker push $IMAGE_NAME:latest
  tags:
    - build

1) docker in docker

  • 왜 build할 때 docker in docker를 쓰는 걸까? 처음에 gitlab-ci를 쓸 때 의문을 가졌던 점과 일치하는 부분이 있다. gitlab-runner가 실행되는 빌드서버는 실행할 때마다 환경변수와 $PATH이 초기화되어 있다. 처음에는 불편했지만 gitlab-runner를 여러 프로젝트에 등록해 보니 프로젝트마다 필요한 환경변수나 의존성이 다르기에 필요성을 알 수 있었다.
  • docker in docker도 마찬가지다. 여러 프로젝트에서 어떤 라이브러리는 버전만 달라 실행 환경이 충돌 날 여지가 있거나 또 같은 프로젝트 내에서 여러 작업을 병렬로 실행시키고 싶지만, 환경변수 등 충돌 발생 여부가 있을 때 사용하는 것이다. 컨테이너 파일시스템(docker in docker, dind)을 이용해 독립된 빌드 환경을 구성할 수 있다.

2) 이미지 태그

  • IMAGE_TAG를 사용자가 지정한 값을 쓸 수 있다. IMAGE_TAG: 1.0처럼 상수로 지정하게 되면 도커 레지스트리 이력은 하나의 태그 기록만 남게된다. 이미지를 푸시할 때마다 변경사항을 추적하고 싶다면 커밋 SHA를 이용한다. SHA는 커밋마다 고유한 값이므로 이를 이용하면 이미지를 푸시할 때마다 고유한 태그를 가진 이미지가 만들어지고 변경 사항을 추적할 수 있다.
IMAGE_TAG: $CI_COMMIT_SHA  
  • 이 경우 배포 파이프라인에서 기존 이미지를 지우는 작업을 하는 스크립트가 있으면 해당 스크립트가 의미가 없게 된다. 그 이유는 IMAGE_TAG는 이전 값이 아닌 새로운 값이기 때문이다. 이 경우 수동으로 IMAGE를 지워주지 않으면 서버 용량의 대부분이 도커 이미지로 가득 차게 된다. 좋은 방법이 있는데 TAG를 하나 더 달아주는 것이다. 예를 들어 추가로 latest 태그를 달면 배포 과정에서 latest를 지우면 기존에 존재하는 이미지 볼륨을 자동으로 제거할 수 있다.
docker push $IMAGE_NAME:latest
  • 서버에서는 로컬에 있는 이미지 중 latest 태그를 지우고 원격에서 latest 태그가 붙은 이미지를 새로 다운받게 된다.

3. 배포 파이프라인 (backend)

deploy:
  stage: deploy
  before_script:
    - chmod 400 $SSH_KEY
    - mkdir -p ~/.ssh
    - ssh-keyscan -H 13.209.19.192 >> ~/.ssh/known_hosts
  script:
    - ssh -i $SSH_KEY ubuntu@13.209.19.192 "echo $DOCKER_REGISTRY_PASS | docker login --username $DOCKER_REGISTRY_USER --password-stdin \
    && docker rm spring-app -f || true && docker rmi $IMAGE_NAME:$IMAGE_TAG || true \ 
    && docker pull $IMAGE_NAME:$IMAGE_TAG \ 
    && docker run -d -p 8080:8080 --name spring-app $IMAGE_NAME:$IMAGE_TAG"
  tags:
    - deploy

SSH 동작 방식

  • 배포 파이프라인에서 사용하는 명령어를 이해하기 위해 SSH 동작 방식을 이해하는 건 필수적이다.
  • SSH는 클라이언트와 서버가 서로 인증하여 연결을 설정한다. 크게 서버 인증과 클라이언트 인증으로 구분할 수 있다.

1) 서버 인증

  • 클라이언트가 접속하려는 서버를 신뢰할 수 있는지 확인하는 과정이다.
  • SSH에선 공개 채널에서 먼저 디피-헬만 알고리즘을 이용해 서로 비밀 키를 공유한다. 해당 비밀키는 통신을 암호화하는 데 사용한다.
  • 서버는 자신의 공개 키를 클라이언트에 전송하고 클라이언트는 known_hosts에 서버의 공개 키가 있는지 확인한다.
  • 공개 키가 있다면 신뢰할 수 있는 서버로 판단한다.
  • 공개 키가 없다면 해당 서버를 신뢰할 수 있는지 묻는 안내창이 나오고, 클라이언트가 신뢰할 수 있다고 하면 서버의 공개키가 known_hosts에 추가되어 신뢰할 수 있는 서버로 인식한다.

2) 클라이언트 인증

  • 서버가 접속하려는 클라이언트를 신뢰할 수 있는지 확인하는 과정이다.
  • 클라이언트는 자신의 공개 키를 서버에 전송한다.
  • 서버의 authroized_key에 클라이언트의 공개 키가 등록되어 있다면 클라이언트를 신뢰할 수 있고, SSH 연결을 허용한다. 등록되어 있지 않다면 연결에 실패한다.
  • ec2 인스턴스를 만들면 초기에 비밀키를 받을 수 있는데 해당 비밀키의 공개키 쌍이 서버에 authorized_key에 추가되어 있다. 즉, ssh -i "example.pem" 계정@IP 명령어를 통해 SSH 연결을 시도할 수 있다.
  • 마찬가지로 처음 서버에 연결하면 위와 같이 서버를 신뢰할지 묻는 창이 나온다. YES로 선택하면 클라이언트의 known_hosts에 추가되어 신뢰할 수 있는 서버로 인식하게 된다.

gitlab-ci SSH 연결

  • 공식문서를 보면 gitlab runner는 SSH 키 관리를 기본적으로 지원하지 않는다고 한다. ssh -i 옵션을 통해 서버에 이미 등록된 공개키 쌍인 비밀키로 연결하는 방법을 선택할 수 있다.
  • 빌드 서버에 SSH 키를 만든다. (SSH-KEYGEN) 해당 공개키를 배포 서버에 추가한 후 gitalb-runner에선 -i 옵션을 통해 등록된 공개키의 비밀키 쌍을 이용해 접속할 수 있다.

Host key verification failed 오류

  • gitlab runner가 해당 서버에 비밀키로 접속하려고 할 때 신뢰할 수 있는 서버에 등록되지 않았기 때문에 발생하는 에러이다.
  • 접속하기 전에 known_hosts에 접속하려는 서버의 공개키를 추가하여 신뢰할 수 있는 서버로 등록한다.
  • ssh-keyscan을 통해 비대화형 모드로 공개키를 추가할 수 있다.

4. 배포 파이프라인 (frontend - next.js)

deploy:
  before_script:
    - export PATH="/home/ubuntu/.nvm/versions/node/v20.12.2/bin:$PATH"
    - npm install next
    - chmod 400 $SSH_KEY
    - mkdir -p ~/.ssh
    - ssh-keyscan -H 34.64.229.17 >> ~/.ssh/known_hosts
  script:
    - npm run build
    - mv out html
    - rsync -avz --delete -e "ssh -i $SSH_KEY" html/ elice@34.64.229.17:/home/elice/nginx/html/
  tags:
    - deploy
  rules:
    - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"
  • front에서 next.js를 사용하고 있었다. 파이프라인을 추가하여 프론트 배포 파일을 서버로 옮겨야한다. next.js의 서버랜더링 프레임워크를 사용하지 않고, 서버로 옮겨서 정적 파일을 nginx로 라우팅할 예정이므로 next.config.mjs파일에 export 조건을 추가해 빌드 파일을 만든다.

rsync로 빌드 파일 전송

  • 처음엔 linux scp 프로토콜을 이용해 파일을 전송하려 했지만, rsync protocol을 이용하면 변경된 부분만을 보낼 수 있다고 하여 rsync를 사용하기로 하였다.
  • 프론트에서 작업한 정적파일은 어떤 요구사항에 따라 새로 추가되거나 제거될 수도 있다. 서버에서 rsync로 이미 받은 내용과 동기화 문제가 발생할 수 있는데, --delete 옵션을 사용하면 클라이언트에서 보낸 파일 외에 서버가 별도로 파일을 가지고 있다면 해당 파일을 삭제한다.

Permission denied, please try again.

  • gitlab-runner에서 배포 서버에 접근하기 위해 known_hosts에 서버의 공개 키를 추가했지만, 여전히 권한 문제가 발생했다.
sudo chown -R elice:elice /home/elice/nginx/html
sudo chmod -R 755 /home/elice/nginx/html
  • 위의 명령어로 nginx routing 폴더에 권한을 주었지만, 여전히 해결되지 않았다.
  • 백엔드에서 배포할 때는 ssh -i {비밀키} {계정@주소} "<명령어>"로 한 번에 명령어를 입력했지만 이번에는 rsync 명령어를 사용할 때 해당 명령어를 사용하는 클라이언트가 SSH 인증이 되어야 한다. 아래처럼 명령어를 사용하면 된다.
rsync -avz --delete -e "ssh -i $SSH_KEY" html/ elice@34.64.229.17:/home/elice/nginx/html/
  • ssh-agent를 사용한다면, 아래와 같이 yml을 구성할 수 있다.
- eval "$(ssh-agent -s)"
- ssh-add $SSH_KEY
    
rsync -avz --delete -e "ssh" html/ elice@34.64.229.17:/home/elice/nginx/html/

깃랩 트리거

rules:
    - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"
  • 깃랩에서 트리거를 설정하지 않으면 기본적으로 commit이 발생할 때 파이프라인이 동작한다. 위에 처럼 Merge Request의 target branch가 "main" branch로 트리거 조건을 제한할 수 있다.
  • 스크립트를 작성하며 헷갈릴 수 있는 점은 dev branch에도 gitlab-ci.yml이 존재하고, main branch가 존재할 때 dev -> main으로 Merge Request를 한다면 어떤 gitlab-ci.yml이 우선 적용될지 헷갈릴 수 있다. 테스트를 해보니 dev가 우선권을 가진다. 어떻게 보면 당연하다. Merge Reqeust에서 변경사항을 반영하려는 곳이 dev이니까!
  • 트리거 및 깃랩에서 미리 정의된 환경 변수는 공식 사이트를 참고하자.

참고 자료

profile
꾸준하게

0개의 댓글

관련 채용 정보