프로젝트에서 Docker와 Jenkins를 활용하여 CI/CD 환경을 구축하였으며, 설정했던 서버배포 과정을 정리하려 합니다.
* AWS EC2 인스턴스 생성 이후 보안 그룹 - 인바운드 규칙 에서
SSH 접속을 위해 22번 포트에 모든 IP를 허용할 수 있도록 0.0.0.0/0 으로 설정
pem키 디렉토리로 이동 → 서버 접속
$ ssh -i [pem키이름].pem ubuntu@[도메인명]
❗️ 권한에러가 발생하여 권한을 변경해주었다.
$ chmod 400 [pem키이름].pem
서버내에서 방화벽 설정
// 방화벽상태 확인
$ sudo ufw status
// 22번포트 open
$ sudo ufw allow 22
$ sudo ufw enable
ufw 설정시에 22번 포트가 막히게 되면 콘솔 접속이 불가능해지기때문에 항상 ALLOW가 되어 있어야 한다.
$ sudo apt-get remove docker docker-engine docker.io containerd run
Docker CE를 설치하기 전에 먼저 도커 저장소(repository)를 설정해야 한다.
이후에 저장소로부터 도커를 설치하거나 업데이트할 수 있다.
$ sudo apt-get update
// 의존성 패키지 설치
$ sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
// Docker 패키지 인증 키 추가
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
// 인증키 확인 (아래그림)
$ sudo apt-key fingerprint 0EBFCD88
// Docker 저장소 추가
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
// 저장소 업데이트
$ sudo apt-get update
fingerprint 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88에서 마지막 8자를 검색하여 fingerprint 인식 키가 있는지 확인한다.
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
// 도커 버전 확인
$ sudo docker --version
도커 버전이 확인되며 도커가 잘 설치된것을 확인할 수 있다.
참고 : https://velog.io/@_nine/Docker-MySQL설치-및-접속하기
다음으로 도커 컨테이너에 mysql을 올려보자
// 도커 mysql 이미지 다운
$ sudo docker pull mysql
// 도커 이미지 확인
$ sudo docker images
// 3306 포트열기
$ sudo ufw allow 3306
// 컨테이너 mysql 적재
$ sudo docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=[패스워드] -p 3306:3306 mysql
// 컨테이너 확인
$ sudo docker ps
// 컨테이너에 접속
$ sudo docker exec -it mysql bash
// 관리자로 접속
$ mysql -u root -p
$ 패스워드 입력
use mysql
CREATE USER '사용자명'@'%' IDENTIFIED BY '비밀번호';
GRANT ALL PRIVILEGES ON *.* TO '사용자명';
FLUSH PRIVILEGES;
여기까지 AWS EC2서버에 Docker 설치 및 설정 & Mysql 컨테이너를 띄워봤다.
다음은 Jenkins 설치 및 설정 & Gitlab 웹훅을 이용한 Jenkins 연동이다.
Jenkins 설치 & 설정
// jnekins image pull
$ sudo docker pull jenkins/jenkins:lts
// image 확인 (아래 사진)
$ sudo docker images
//8080 포트 열기
$ sudo ufw allow 8080
// 이미지를 컨테이너에 적재 (2번째 명령어로 실행하자)
// !!처음시도한 젠킨스 컨테이너인데 docker in docker 이슈가 있었음 (젠킨스 설정쪽에 다시 설명)
// $ sudo docker run --name jenkins -d -p 8080:8080 -p 50000:50000 -v /home/jenkins:/var/jenkins_home -u root jenkins/jenkins:lts
// docker in docker를 해결하기 위해 컨테이너를 삭제하고 다시 시도했음
$ sudo docker run --name jenkins -d -p 8080:8080 -p 50000:50000 -v /home/jenkins:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -e TZ=Asia/Seoul -u root jenkins/jenkins:lts
// 도커 컨테이너 확인 (아래 사진)
$ sudo docker ps
// 초기 비밀번호 확인 (아래 사진)
$ sudo docker logs jenkins
// 도메인:8080으로 젠킨스 접근
좌측의 Dashboard에서 Jenkins관리 → 플러그인 관리
→ 설치가능에서 gitlab과 docker를 검색하여 다음 플러그인들을 설치한다. ( install without restart )
Gitlab의 Settings → Access Tokens에서 토큰명, 만료기간, scope 설정을 하고 create로 토큰 생성
* 한번만 보여주니 복사해놓자
메인화면 좌측 New Item → Freestyle project 클릭하여 backend, frontend 이름으로 2개의 item 생성
* 이하 item project 설정은 backend, frontend 따로 해준다.
생성한 프로젝트 → 구성 → Build Trigger(빌드 유발) 항목에서 Build when a change is pushed to GitLab 선택!
고급 클릭 → Secret Token의 Generate 클릭해서 토큰 생성
각 프로젝트마다 위 사진의 url과 생성한 token을 각각 메모해둔다.
Gitlab 프로젝트의 setting → webhook 클릭
입력후 아래의 Add webhook을 클릭해서 웹훅을 생성해준다.
생성하면 아래 Add webhook 버튼 밑에 등록된 웹훅이 표시된다.
Test 클릭해서 push events로 웹훅 테스트
성공시 아래 사진처럼 확인가능
프로젝트 선택 → 구성 → 소스코드 관리 → Git
Repository URL : https://”깃랩토큰명”:”깃랩액세스토큰”@”gitlab url”
Credentials는 Add를 클릭해서 username with password로 깃랩 아이디와 비밀번호를 입력해서 credential 생성
Jenkins관리 → Manage Credentials설정 → Jenkins → Global → Add Credentials
깃랩 액세스 토큰을 입력하여 Credential을 생성한다.
API token : 깃랩 액세스 토큰
ID : 임의의 ID
backend, frontend 총 2개의 프로젝트를 생성했고, 깃랩에서 웹훅 테스트를 진행했을때 아래그림처럼 build되는것을 확인할 수 있다.
방법1. 젠킨스 내부에 도커 설치
// 젠킨스 컨테이너 접속
docker exec -it -u root jenkins bash
// 젠킨스내에서 도커 설치
curl https://get.docker.com/ > dockerinstall && chmod 777 dockerinstall && ./dockerinstall
방법2. Jenkins가 설치되어있는 서버의 docker 볼륨과 Jenkins 컨테이너 안 docker의 볼륨을 연결해 사용(jenkins를 docker 컨테이너에 올릴때)
https://devbksheen.tistory.com/entry/Jenkins를-이용한-Docker-컨테이너-자동-배포하기Blue-Ocean-NCP
https://jijs.tistory.com/entry/docker로-젠킨스-설치
// 젠킨스 실행할때 옵션추가 (-v /var/run/docker.sock:/var/run/docker.sock)
// docker run -d -it -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 --name jenkins jenkins/jenkins:lts
// Jenkins 컨테이너에 접속
$ docker exec -it -u root jenkins bash
// jenkins계정에 Docker 볼륨을 사용할 수 있는 권한부여
$ chown jenkins:jenkins /var/run/docker.sock
❗️ 1번 방법으로 젠킨스 내부에 도커를 설치해주었는데 다른 에러가 발생한다.
위와 같은 명령어가 뜨면 docker service가 실행이 안되어있는것이다.
! 해결되야하지만, 나는 여기서다시 에러가 발생한다.
// docker daemon 상태 확인
$ sudo systemctl status docker
// docker를 daemon으로 실행
$sudo systemctl start docker
// 새로 시작하거나 부팅시 자동으로 docker daemon을 실행
$sudo systemctl enable docker
아래와 같은 에러 발생
방법1과 2를 모두 사용했다.
먼저 기존의 젠킨스 컨테이너를 삭제하고, docker in docker가 가능하게 젠킨스를 실행했다. (방법2)
// 젠킨스 컨테이너 정지
$ sudo docker stop jenkins
// 젠킨스 컨테이너 삭제
$ sudo docker rm jenkins
// docker in docker를 추가하여 젠킨스 컨테이너 실행
$ sudo docker run --name jenkins -d -p 8080:8080 -p 50000:50000 -v /home/jenkins:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -e TZ=Asia/Seoul -u root jenkins/jenkins:lts
// Jenkins 컨테이너에 접속
$ docker exec -it -u root jenkins bash
// jenkins계정에 Docker 볼륨을 사용할 수 있는 권한부여
$ chown jenkins:jenkins /var/run/docker.sock
방법2로 진행해도 docker가 실행되지 않았다.
참고 : https://devbksheen.tistory.com/entry/Jenkins를-이용한-Docker-컨테이너-자동-배포하기Blue-Ocean-NCP
이 상태에서 방법1의 도커설치까지 진행해주었다.
// (젠킨스 컨테이너 내부에서) 젠킨스내에서 도커 설치
$ curl https://get.docker.com/ > dockerinstall && chmod 777 dockerinstall && ./dockerinstall
젠킨스 컨테이너 내부에서 docker 명령어가 잘 실행된다!!
이제 젠킨스에서 쉘 스크립트로 프로젝트의 도커파일을 실행시켜서 도커 컨테이너에 띄워보자
cd backend
docker build -t backend .
docker ps -q --filter "name=backend" | grep -q . && docker stop backend && docker rm backend | true
docker run -p 8081:8080 -d -e TZ=Asia/Seoul --name=backend backend
docker rmi -f $(docker images -f "dangling=true" -q) || true
FROM openjdk:8-jdk-slim as builder
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src
RUN chmod +x ./gradlew
RUN ./gradlew bootJar
FROM openjdk:8-jdk-slim
COPY --from=builder build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=gcp","/app.jar"]
EXPOSE 8081
❗️도커에서 backend 컨테이너가 실행되자마자 exit 되는 에러가 발생했다.
docker ps -a 로 전체 컨테이너를 확인하니 exit 상태이고, start 명령어로 컨테이너를 실행해도 바로 exit된다
$ docker logs backend 명령어로 로그확인 결과 환경변수 설정파일 env.properties 가 없어서 생긴 에러
젠킨스 컨테이너 내부에 env.properties를 생성하여 해결한다.
// 젠킨스 컨테이너 내부 접속
$ docker exec -it -u root jenkins bash
// workspace의 backend로 이동
$ cd /var/jenkins_home/workspace/backend/backend/src/main
// properties폴더와 env.properties만들어서 설정추가
$ cd resources
$ mkdir properties
$ cd properties
$ cat > env.properties
환경변수 설정파일(env.properties) 복붙
ctrl+c로 저장후 종료
// 저장된 내용 확인
$ cat env.properties
환경변수파일을 젠킨스 컨테이너안에 넣었는데 계속 동일한 에러가 발생한다. → 컨테이너를 삭제하고 다시 빌드했다
// backend 컨테이너 삭제
$ docker rm backend
삭제이후 젠킨스에서 다시 빌드해주었고 이상없이 도커에 backend 컨테이너가 올라갔다
// ver1 쉘 스크립트로 nginx 설정
cd frontend
docker build -t frontend .
docker ps -q --filter "name=frontend" | grep -q . && docker stop frontend && docker rm frontend | true
docker run -d -p 80:80 -p 443:443 -v /home/ubuntu/certbot/conf:/etc/letsencrypt/ -v /home/ubuntu/certbot/www:/var/www/certbot -v /home/ubuntu/nginx/conf/default.conf:/etc/nginx/conf.d/default.conf --name frontend frontend
docker rmi -f $(docker images -f "dangling=true" -q) || true
// ver2 도커파일로 nginx 설정
cd frontend
docker build -t frontend .
docker ps -q --filter "name=frontend" | grep -q . && docker stop frontend && docker rm frontend | true
docker run -d -p 80:80 -p 443:443 -v /home/ubuntu/certbot/conf:/etc/letsencrypt/ -v /home/ubuntu/certbot/www:/var/www/certbot --name frontend frontend
docker rmi -f $(docker images -f "dangling=true" -q) || true
# build stage
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN yarn install
COPY . .
RUN npm run build
# production stage
FROM nginx:stable-alpine as production-stage
~~COPY ./default.conf /etc/nginx/conf.d~~
COPY --from=build-stage /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
❗️도커에서 frontend 컨테이너가 실행되자마자 exit 되는 에러가 발생했다.
docker ps -a 로 전체 컨테이너를 확인하니 exit 상태이고, start 명령어로 컨테이너를 실행해도 바로 exit된다.
$ docker logs frontend 로 로그확인
아래와 같은 에러 발생
nginx: [emerg] "server" directive is not allowed here in /etc/nginx/nginx.conf:1 에러
→ dockerfile에서 nginx설정파일이 제대로 COPY되지 않은것으로 예상된다
(이때는 도커파일에 경로가 잘못되었고, COPY ./default.conf /etc/nginx/conf.d 로 수정해야했다)
여기서 nginx설정파일을 도커파일이 아닌 쉘 스크립트에서 docker volume으로 설정파일을 지정하는 방법으로 바꿔봤다. (쉘 스크립트 ver1)
volume으로 지정해줄 경로상에 nginx설정파일을 저장한다.
// 경로상의 폴더로 이동
$ cd nginx/conf
// nginx 설정파일 생성 및 저장
$ sudo vi default.conf
server {
listen 80;
server_name [도메인명];
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name [도메인명];
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
ssl_certificate /etc/letsencrypt/live/[도메인명]/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/[도메인명]/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 SSLv3;
ssl_ciphers ALL;
location / {
root /usr/share/nginx/html;
index index.html index.htm
proxy_redirect off;
charset utf-8;
try_files $uri $uri/ /index.html;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Nginx-Proxy true;
}
}
Jenkins에서 사용한 쉘 스크립트와 작성한 DockerFile을 자세히 알아보자
FROM openjdk:8-jdk-slim as builder
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src
RUN chmod +x ./gradlew
RUN ./gradlew bootJar
FROM openjdk:8-jdk-slim
COPY --from=builder build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=gcp","/app.jar"]
EXPOSE 8081
# build-stage
FROM node:lts-alpine as build-stage
# 기본 이미지는 node:lts-alpine 이며, build-stage라는 별칭을 가진다.
WORKDIR /app
# 작업디렉토리를 /app 으로 이동한다
COPY package*.json ./
# package.json 파일을 작업 디렉토리로 복사한다. npm의 모든 필수 종속성 설치를 위해 해당 파일이 필요
RUN yarn install
# yarn install 명령어로 종속성들을 설치한다.
COPY . .
# 나머지 파일들을 작업 디렉토리에 복사한다. 실제 어플리케이션 코드가 포함되어 있다.
RUN npm run build
# npm run build 명령어로 빌드파일을 준비한다
# production-stage
FROM nginx:stable-alpine as production-stage
# stable-alpine 버전의 nginx 기본 베이스로 설정
COPY ./default.conf /etc/nginx/nginx.conf
# 로컬에서 만든 default.conf(nginx 설정)을 nginx의 서버에 복사
COPY --from=build-stage /app/build /usr/share/nginx/html
# nginx가 동작하기위해 필요한 파일들을 시스템으로 복사한다
# --from=builder-stage 태그를 이용하여 위의 빌드 단계의 이미지의 작업 디렉토리(/app/build)에서 nginx의 디렉토리로 파일을 복사한다.
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
// 443 포트열기
$ sudo ufw allow 443
// /home/ubuntu에 certbot 디렉토리 생성
$ cd
$ sudo mkdir certbot
// certbot 디렉토리에서 conf, www 디렉토리 생성
$ cd certbot
$ sudo mkdir conf www logs
// 80번포트 서버 정지(encrypt 인증방식인 standalone을 위해)
$ sudo service nginx stop
$ sudo docker pull certbot/certbot
$ sudo docker run -it --rm --name certbot -p 80:80 -v "/home/ubuntu/certbot/conf:/etc/letsencrypt" -v "/home/ubuntu/certbot/log:/var/log/letsencrypt" -v "/home/ubuntu/certbot/www:/var/www/certbot" certbot/certbot certonly
certbot 컨테이너에서 만든 인증서를 외부에서 사용하기 위해 생성한 디렉토리를 certbot 컨테이너와 연동
* Let's Encrypt 의 인증 방식인 Standalone plugin 은 서버 인증을 위해서 80 포트를 이용한다.
따라서 nginx, apache 와 같이 80 포트를 이용하는 서비스가 있다면 서버를 일시적으로 정지시켜줘야 한다.
standalone방식,<이메일>, agree, no, <domain_name>으로 작성하여 인증서를 발급한다.
인증서 발급 성공시 2개의 pem파일 생성완료! (아래는 위의 ssl인증서 발급사진에서 성공부분 확대사진)
* 젠킨스 쉘 스크립트쪽을 하다가 pem파일을 확인하려 다시 돌아왔는데 /etc 안에 letsencrypt가 없었다. 그래서 다시 nginx서버를 정지하고 인증서 발급 시도
다시 받아도 똑같이 저장되었다는 경로상에서 pem파일을 확인할 수 없다.
certbot image와 디렉토리까지 전부 삭제하고 다시 시도해보았다.
// certbot 이미지삭제
$ docker rmi [이미지id]
// 생성한 certbot 디렉토리 전체 삭제(-r, -f 옵션으로 안에 파일이 있어도 전부 삭제)
$ sudo rm -rf certbot
결과는 동일하게 관련 폴더를 찾을 수 없었고, 폴더명(letsencrypt)을 넣어서 찾아봤다.
// 폴더 찾아보기
$ find / -name 폴더명 -type d
etc/letsencrypt 경로가 아닌 /certbot/conf 내부에 존재했다. certbot 인증서를 받을때 설정한 도커 volume 으로 해당경로에 저장된 것 같다.
.pem파일을 openssl을 이용하여 .p12(PKCS12)파일로 변환하자
참고 : https://veneas.tistory.com/entry/Spring-Boot-스프링-부트-SSL-인증서-적용-TLS-p12
// 해당 pem 파일들을 vi 에디터로 들어가서 복사 후 로컬서버로 가져온다.
$ vi [privatekey pem파일]
$ vi [certificate pem파일]
// 복사 후 로컬서버에 동일하게 파일들 생성 후 복사한 내용 입력
// [PEM -> PKCS12]
$ sudo openssl pkcs12 -inkey [privatekey] -in [certificate] -export -out [출력할파일명]
ex) sudo openssl pkcs12 -inkey privkey1.pem -in fullchain1.pem -export -out spring_key.p12
// 비밀번호 입력, 확인
변환한 p12파일을 스프링부트 프로젝트 내부에 넣어준다.
// home으로 파일 이동
$ sudo mv spring_key.p12 /home/ubuntu
$ cd
// 젠킨스 컨테이너안의 경로로 파일 복사
$ docker cp spring_key.p12 jenkins:/var/jenkins_home/workspace/backend/backend/src/main/resources
젠킨스 컨테이너 내부에서 확인결과 잘 복사된것을 확인할 수 있다.
변환해서 나온 PKCS12 파일을 스프링부트 프로젝트에 포함시킨다.
* 프로젝트 내에 포함시킬 경우 따로 경로를 지정하지 않아도되지만,
스프링부트 프로젝트 내에 포함시키지 않을경우 .p12파일 경로를 지정해야한다.
ssl 적용을 위해 application.properties 설정을 추가한다
# ssl
security.require-ssl=true
server.ssl.key-store=classpath:[p12파일명].p12
server.ssl.key-store-type=PKCS12
server.ssl.key-store-password=[ssl비밀번호]
server.ssl.enabled=true
Spring Boot 실행시 https로 시작되는것을 확인할 수 있다.
ssl비밀번호를 환경변수로 설정했기때문에, 젠킨스 컨테이너내부에 env.properties에 ssl-pw을 추가해주고, Git ignore에 p12파일을 추가해준다.