개요
Docker를 통한 CI/CD 방법 중 GitLab를 통한 CI/CD 방법에 대해 배워본다.
CI/CD 개념
GitHub Actions를 통한 CI/CD
Docker?
애플리케이션을 컨테이너라는 단위로 패키징, 배포, 실행할 수 있도록 도와주는 오픈 소스 플랫폼.
컨테이너는 애플리케이션과 필요한 모든 종속성(라이브러리, 설정 파일 등)을 하나의 독립된 환경에 담아 실행할 수 있게 하며, 이를 통해 어디서든 동일한 환경에서 애플리케이션을 실행할 수 있다.
주요 특징
- 경량 컨테이너: 가상 머신(VM)보다 가벼운 환경으로, 호스트 OS의 커널을 공유해 더 빠르고 효율적으로 실행된다.
- 이식성: 개발 환경과 운영 환경의 불일치 문제를 줄여, 동일한 이미지를 여러 플랫폼에서 실행 가능.
- 자동화된 빌드: Dockerfile을 사용해 이미지를 코드로 정의하고 버전 관리 가능.
- 확장성: 여러 컨테이너를 오케스트레이션 도구(Kubernetes, Docker Swarm 등)와 함께 사용하여 대규모 애플리케이션 배포 가능.
주요 용도
- 개발 환경 표준화
- CI/CD 파이프라인 구축
- 마이크로서비스 아키텍처 구현
- 애플리케이션 배포 및 관리 간소화
Docker 사용 실습
Docker의 기능을 활용해 2개의 애플리케이션을 컨테이너로 만들어 컨테이너의 컨트롤러로 다른 컨테이너를 호출할 수 있도록 한다.
1. Project 생성
- SpringInitializr를 사용해 web, lombok, open feign를 추가한 프로젝트를 2개 생성한다.
- 생성한 프로젝트를 실습에 사용할 폴더에 넣고 인텔리제이를 통해 프로젝트를 연다.
2. Project 코드 생성
- Application.java
@EnableFeignClients
@SpringBootApplication
public class AApplication {
public static void main(String[] args) {
SpringApplication.run(AApplication.class, args);
}
}
- 각 Application 코드에 @EnableFeignClients 어노테이션을 추가한다.
- BController.java
@RestController
public class BController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
- /hello 엔드포인트를 호출하면 "hello"를 반환하도록 한다.
- AController.java
@RestController
@RequiredArgsConstructor
public class AController {
private final BServiceClient bServiceClient;
@GetMapping("/hi")
public String hi() {
String hello = bServiceClient.hello();
return "service-a : hi ####### service-b: " + hello;
}
}
- OpenFeign을 사용해 /hi 엔드포인트를 호출하면 B 애플리케이션의 /hello도 호출하도록 한다.
- BServiceClient.java
@FeignClient(name = "service-b", url = "${service.b.url}")
public interface BServiceClient {
@GetMapping("/hello")
String hello();
}
- Service-b와 통신하기 위해 FeignClient 구현(서비스 A에서 서비스 B의 기능을 호출하기 위함)
- service b application.yml
spring:
application:
name: b
server:
port: 8080
- 애플리케이션 이름과 사용할 포트만 지정한다.
- service a application.yml
spring:
application:
name: a
service:
b:
url: http://service-b:8080
server:
port: 8080
- 추가적으로 서비스 b의 URL을 등록한다.
참고
Docker 컨테이너를 통해 서비스 간 호출을 할 때 서비스의 yml 파일에서 port 번호가 동일해도 괜찮은 이유?
- 컨테이너 간 네트워크 격리와 호스트-컨테이너 포트 매칭 구조
1. 컨테이너 간 네트워크 격리:
- Docker는 각 컨테이너를 독립된 네트워크 네임스페이스에서 실행.
- 각 컨테이너는 자신의 내부 네트워크에서 localhost:8080으로 서비스를 실행하지만, 다른 컨테이너와 충돌하지 않는다.
- 두 컨테이너는 독립된 네트워크 공간에서 실행되므로 문제되지 않음.
2. 컨테이너 외부와의 포트 매핑:
- Docker는 컨테이너 내부 포트(8080)를 외부 호스트에 노출할 때 다른 포트로 매핑할 수 있음.
3. 컨테이너 간 이름 기반 통신:
- Docker Compose나 Docker 네트워크를 사용하면, 서비스 간 통신에서 컨테이너 이름을 사용해 서로 접근한다.
3. 각 서비스 코드 이미지 등록
- Window 환경의 경우 Docker Desktop을 실행해야 Docker 관련 명령어를 인텔리제이 터미널에서 사용 가능했다.
- Dockerfile 생성
FROM openjdk:17-jdk-slim
VOLUME /tmp
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
- 각 프로젝트 루트 폴더에 Dockerfile 코드를 추가해 Docker 이미지로 만들 수 있도록 준비한다.
- Docker 네트워크 생성
docker network create my-network
- 인텔리제이 터미널에 해당 명령어를 입력해 도커 네트워크를 생성한다.
- Window에서 실습을 진행했기 때문에 편의성을 위해 인텔리제이의 터미널을 사용했다.
- 프로젝트 빌드 및 이미지 생성
-- 위치 이동
cd com.service.b
-- 빌드 명령어
./gradlew clean bootJar
-- 이미지 생성
docker build -t img-service-b .
-- 마지막의 . 은 도커파일의 위치
- 빌드할 프로젝트의 루트 폴더로 진행 후 프로젝트를 빌드 후 명령어를 통해 이미지를 생성했다.
- 컨테이너 생성
docker run -d --name service-b --network my-network -p 18081:8080 img-service-b
- 위 명령어를 통해 생성한 이미지를 사용해 컨테이너를 생성했다.
- 컨테이너 이름, 네트워크, 포트를 설정
- 컨테이너 확인 및 접속
docker ps
- 컨테이너가 정상적으로 생성되었으면, 주소로 접속해 service-b가 호출되는지 확인
다른 서비스에 대해서도 위와 같은 과정을 반복한 후 주소로 접속해 다른 서비스를 호출할 수 있는지 확인
Docker Compose 활용
- 기존 컨테이너 삭제
docker stop 서비스_A_컨테이너_아이디
docker rm 서비스_A_컨테이너_아이디
- 명령어를 통해 컨테이너를 정지한 후 삭제한다.
- 아이디 앞 2개만 입력해도 명령이 정상 동작한다.
- docker rm -f 를 사용하면 실행 중인 컨테이너도 삭제가 가능하다.
- docker-compose.yml 파일 생성
- 2개의 프로젝트의 상위 폴더에서 생성한다.
version: '3.8'
services:
service-a:
image: img-service-a
ports:
- "18080:8080"
environment:
- SERVICE_B_URL=http://service-b:8080
depends_on:
- service-b
service-b:
image: img-service-b
ports:
- "18081:8080"
networks:
default:
driver: bridge
- docker compose를 통해 사용할 서비스와 네트워크를 정의한다.
- Docker Compose 실행 및 확인
docker compose up -d
docker compose ps
- 정상적으로 생성된 것을 확인한 후 주소로 접속해서 서비스를 호출한다.
Docker 네트워크, 컨테이너, 이미지 삭제
실습 후 필요 없어진 컨테이너, 네트워크, 이미지를 삭제한다.
- 컨테이너 목록 확인
docker ps
- 삭제할 컨테이너 중지
docker stop <container_name_or_id>
- 컨테이너 삭제
docker rm <container_name_or_id>
- 모든 중지된 컨테이너 삭제
docker container prune
- 전체 네트워크 확인
docker network ls
- 특정 네트워크 삭제
docker network rm <network_name_or_id>
- 모든 사용자 네트워크 삭제(미 사용)
docker network prune
- 이미지 확인
docker images
- 특정 이미지 삭제
docker rmi <image_name_or_id>
- 모든 이미지 삭제
docker rmi $(docker images -q)
GitLab을 통한 CI/CD
- GitLab을 사용해 CI/CD 환경을 구축하고 CI 과정을 자동화.
- GitLab 프로젝트를 만들고 개발한 코드를 Push
- GitLab이 Push 된 코드를 Docker Image로 생성 ECR에 등록
- Docker Image를 통해 Docker container를 ECS에 실행
1. GitLab에 빈 프로젝트 생성
2. Spring 프로젝트 생성 및 GitLab 연동
Spring 프로젝트를 생성(인텔리제이 or initializr 사용) 후 인텔리제이를 통해 GitLab과 연동한다.
- GitLab 프로젝트의 Code → Clone with HTTPS 복사
인텔리제이 상단 메뉴 VCS → Enable Version Control Integration 선택
상단 메뉴 Git → Manage Remotes 선택
Git Remotes 메뉴에 복사한 URL 붙여넣고 OK 선택
GitLab 연동 완료 후 Git 메뉴에서 Pull을 눌러 프로젝트를 풀한다.
3. 프로젝트 코드 생성
@RestController
public class SampleController {
@GetMapping("/sample")
public String sample() {
return "Hello World";
}
}
spring.application.name=sample
server.port=8080
FROM openjdk:17-jdk-slim
VOLUME /tmp
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
- 테스트를 위한 간단한 파일만 작성한다.
AWS 설정
1. 보안 그룹 추가
- 보안 그룹 생성 후 인바운드 규칙을 위와 같이 편집한다.
2. ECR 리포지토리 생성
3. ECS 클러스터 생성
- 서버리스 선택 후 생성
4. 태스크 정의 메뉴에서 새 태스크 정의 생성
5. 컨테이너 정보 입력
- 이미지 URL에 ECR 리포지토리 URL 입력
6. 생성된 클러스터에서 서비스 생성
7. 로드 밸런싱 설정
8. AWS IAM에서 사용자 생성
- 사용자 상세페이지에서 엑세스 키를 만들고 기타 선택 → 생성된 액세스 키와 비밀 액세스 키 저장(메모장 등에)
9. 깃랩 CI 파일 작성
- Settings > CI/CD > Variables에서 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY 추가
- Key 값은 8번 과정에서 저장한 값을 입력
10. 프로젝트 루트 폴더에 .gitlab-ci.yml 생성
services:
- docker:stable-dind
stages:
- build jar
- build and push docker image
- deploy
variables:
APPLICATION_NAME: "01"
TAG_NAME: "latest"
DOCKER_IMAGE: "group-18934028/project-01"
build:
image: openjdk:17-jdk-slim
stage: build jar
script:
- chmod +x gradlew
- ./gradlew clean build
artifacts:
paths:
- build/libs/*.jar
docker build:
image: docker:latest
stage: build and push docker image
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_REF_NAME == "main"
variables:
TAG_NAME: "latest"
- if: $CI_COMMIT_BRANCH == "develop" || $CI_COMMIT_REF_NAME == "develop"
variables:
TAG_NAME: "develop"
script:
- apk add --update --no-cache curl py3-pip py3-virtualenv
- python3 -m venv /tmp/venv
- source /tmp/venv/bin/activate
- pip install awscli
- aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
- aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
- aws configure set region ap-northeast-2
- docker build -t $DOCKER_IMAGE .
- docker tag $DOCKER_IMAGE:latest 730335597998.dkr.ecr.ap-northeast-2.amazonaws.com/$DOCKER_IMAGE:$TAG_NAME
- aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 730335597998.dkr.ecr.ap-northeast-2.amazonaws.com
- docker push 730335597998.dkr.ecr.ap-northeast-2.amazonaws.com/$DOCKER_IMAGE:$TAG_NAME
deploy:
image: python:3.9-slim
stage: deploy
script:
- python3 -m venv /tmp/venv
- source /tmp/venv/bin/activate
- pip install awscli
- aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
- aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
- aws configure set region ap-northeast-2
- aws ecs update-service --cluster project-cluster --service project-01-service --task-definition project-01-task:1 --force-new-deployment
11. 프로젝트 Commit, Push 후 파이프라인 확인
12. 확인
ECS 클러스터 상세 페이지의 로드 밸런서의 DNS 이름 복사 후 웹에 입력하고 엔드포인트에 접근해 결과 확인
정리
- Docker를 통한 프로젝트 컨테이너 관리 방법과 GitLab을 통한 CI/CD 방법을 배우게 되었다.
'자바 심화 > TIL' 카테고리의 다른 글
Redis - Cache (0) | 2024.12.02 |
---|---|
Redis - Redis Template (0) | 2024.11.29 |
MSA - 기초 5 (0) | 2024.11.27 |
MSA - 기초 4 (0) | 2024.11.26 |
MSA - 기초 3 (0) | 2024.11.25 |