(멋쟁이사자처럼 백엔드스쿨플러스) Day10 CI/CD 환경구축
CI/CD 환경구축하기
깃허브 토큰 생성
- EC2 인스턴스에서 private docker image를 받기 위해 로그인이 필요하다.
- read 패키지 권한만 있는 github token을 생성한다.
테라폼을 통한 ec2 인스턴스 생성
- main.tf 파일을 생성한다.
Terraform AWS 기본 설정
이 부분은 Terraform의 기본 설정을 정의한다. AWS 프로바이더를 사용하도록 설정하고, AWS 리전을 지정한다.
1 2 3 4 5 6 7 8 9 10 11 12
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 4.0" } } } provider "aws" { region = var.region }
VPC 네트워크 구성
VPC를 생성하고 DNS 지원을 활성화한다. CIDR 블록은 10.0.0.0/16으로 설정한다.
1 2 3 4 5 6
resource "aws_vpc" "vpc_1" { cidr_block = "10.0.0.0/16" enable_dns_support = true enable_dns_hostnames = true ... }
1) 서브넷 구성
- 세 개의 서브넷이 각각 다른 가용영역(a, b, c)에 생성한다:
subnet_1: 10.0.1.0/24 (a 가용영역)
subnet_2: 10.0.2.0/24 (b 가용영역)
subnet_3: 10.0.3.0/24 (c 가용영역) 각 서브넷은 퍼블릭 IP 자동 할당이 활성화되어 있다.
2) 인터넷 게이트웨이 및 라우팅
1 2 3 4 5 6 7 8 9 10 11
resource "aws_internet_gateway" "igw_1" { vpc_id = aws_vpc.vpc_1.id } resource "aws_route_table" "rt_1" { vpc_id = aws_vpc.vpc_1.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.igw_1.id } }
- 인터넷 게이트웨이를 생성하고 라우팅 테이블을 설정하여 인터넷 접속을 가능하게 한다.
보안 그룹 설정
- 모든 인바운드/아웃바운드 트래픽을 허용하는 보안 그룹을 생성한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
resource "aws_security_group" "sg_1" { name = "${var.prefix}-sg-1" ingress { from_port = 0 to_port = 0 protocol = "all" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "all" cidr_blocks = ["0.0.0.0/0"] } }
EC2 인스턴스 구성
1) IAM 역할 및 정책
- EC2 인스턴스에 대한 IAM 역할을 생성하고 다음 권한을 부여한다:
S3 전체 접근 권한
Systems Manager 접근 권한1 2 3 4
resource "aws_iam_role" "ec2_role_1" { name = "${var.prefix}-ec2-role-1" ... }
2) EC2 인스턴스 설정
- 주요 EC2 설정:
AMI: Amazon Linux 2
인스턴스 유형: t2.micro
루트 볼륨: 30GB (gp3)
퍼블릭 IP 자동 할당1 2 3 4 5 6
resource "aws_instance" "ec2_1" { ami = "ami-04c596dcf23eb98d8" instance_type = "t2.micro" subnet_id = aws_subnet.subnet_1.id ... }
3) User Data 스크립트
- EC2 인스턴스 시작 시 자동으로 실행되는 스크립트가 포함되어 있으며 다음을 설치/설정한다:
Docker 설치 및 활성화
Docker Compose 설치
Git 설치
스왑 파일 설정 (4GB)
서버 DB 설치
MySQL 설치 및 설정
- MySQL 컨테이너를 실행한다.
1 2 3 4 5 6 7 8 9
docker run -d \ --name mysql_1 \ --restart unless-stopped \ -p 3306:3306 \ -e TZ=Asia/Seoul \ -e MYSQL_ROOT_PASSWORD=root123414 \ -v /dockerProjects/mysql_1/volumns/var/lib/mysql:/var/lib/mysql \ mysql \ --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
MySQL 에 새 비밀번호 세팅, 로컬용 비밀번호 세팅
- MySQL 컨테이너에 접속하여 비밀번호를 변경한다.
%
: 모든 호스트에서 접속 할 때 사용하는 비밀번호localhost
: 로컬에서 접속 할 때 사용하는 비밀번호1 2 3 4 5 6
docker exec -it mysql_1 mysql -u root -p # 비번입력 : root123414 [~/mysql] ALTER USER 'root'@'%' IDENTIFIED BY '새로운 비밀번호'; # 새 비번 입력 [~/mysql] ALTER USER 'root'@'localhost' IDENTIFIED BY '1234'; # 로컬에서는 1234 로 접속 할 수 있도록 [~/mysql] FLUSH PRIVILEGES; [~/mysql] exit
redis 설치 및 설정
Redis 컨테이너를 실행한다.
1 2 3 4 5
docker run -d \ --name redis_1 \ --restart unless-stopped \ -p 6379:6379 \ redis
redis 비밀번호 변경
Redis 컨테이너에 접속하여 비밀번호를 변경한다.
1 2 3 4
docker exec -it redis_1 redis-cli [~/redis] AUTH 기존_비밀번호 # 기존에 설정된 비밀번호가 있다면 입력 [~/redis] CONFIG SET requirepass "새로운 비밀번호" [~/redis] exit
Nginx 설치 및 설정
Nginx Proxy Manager 설치
Nginx Proxy Manager 컨테이너를 실행한다.
1 2 3 4 5 6 7 8 9 10
docker run -d \ --name npm_1 \ --restart unless-stopped \ -p 80:80 \ -p 443:443 \ -p 81:81 \ -e TZ=Asia/Seoul \ -v /dockerProjects/npm_1/volumes/data:/data \ -v /dockerProjects/npm_1/volumes/etc/letsencrypt:/etc/letsencrypt \ jc21/nginx-proxy-manager:latest
Docker run 명령어 옵션 설명
1. 컨테이너 실행 관련 옵션
docker run
: Docker 컨테이너를 생성하고 실행하는 기본 명령어-d
: (detached mode) 컨테이너를 백그라운드에서 실행하며, 실행 결과로 컨테이너 ID를 출력--name npm_1
: 컨테이너의 이름을 ‘npm_1’으로 지정2. 재시작 정책
--restart unless-stopped
: 컨테이너가 중지되지 않는 한 항상 재시작-p 80:80
: HTTP 트래픽용 포트 (호스트의 80 포트 → 컨테이너의 80 포트)-p 443:443
: HTTPS 트래픽용 포트 (호스트의 443 포트 → 컨테이너의 443 포트)-p 81:81
: Nginx Proxy Manager의 관리자 웹 UI용 포트 (호스트의 81 포트 → 컨테이너의 81 포트)4. 환경 변수 (-e)
-e TZ=Asia/Seoul
: 컨테이너의 시간대를 ‘Asia/Seoul’로 설정5. 볼륨 마운트 (-v)
-v /dockerProjects/npm_1/volumes/data:/data
:- 호스트의
/dockerProjects/npm_1/volumes/data
디렉토리를 - 컨테이너의
/data
디렉토리에 마운트 - Nginx Proxy Manager의 설정 데이터 저장용
- 호스트의
-v /dockerProjects/npm_1/volumes/etc/letsencrypt:/etc/letsencrypt
:jc21/nginx-proxy-manager:latest
:- 사용할 Docker 이미지를 지정
jc21/nginx-proxy-manager
레포지토리의 최신 버전 사용
nginx proxy manager 관리콘솔에 접속
- 웹서버
- 웹 서버는 클라이언트(브라우저)에서 요청한 정적 콘텐츠(HTML, CSS, 이미지, JavaScript 파일 등)를 제공하는 서버이다.
- 웹 서버는 주로 HTTP 프로토콜을 사용하여 클라이언트와 통신하고, 클라이언트의 요청을 처리하여 정적 파일을 전달하는 역할을 수행한다.
웹서버의 기능
- 정적 콘텐츠 제공: HTML, 이미지, CSS, JavaScript 파일과 같은 정적 리소스를 클라이언트에 전달.
- 로드 밸런싱: 여러 서버 간의 트래픽을 분배하는 기능을 제공하여, 높은 트래픽을 효율적으로 처리.
- HTTP 요청 처리: 클라이언트에서 들어오는 HTTP 요청을 받아 그에 따른 응답을 반환.
http://서버아이피:81
으로 접속한다.초기계정
ID:admin@example.com
PW:changeme- proxy host를 추가한다.
- 도메인:
example.com
- 포트:
80
- SSL:
Force SSL
- SSL Cert:
Let's Encrypt
- 도메인:
- 해당 도메인으로 들어온 통신을 도커아이피의 포트로 포워딩 해준다.
- proxy host 추가 후, DNS 설정을 한다.
도커 컨테이너 실행 및 배포(수동)
수동배포 과정
- EC2 인스턴스에 깃 토큰으로 로그인한다.
1 2
##read 권한이 있는 토큰으로 로그인 docker login ghcr.io -u kknaks -p 토큰
- EC2 인스턴스에 깃 토큰으로 로그인한다.
- EC2 인스턴스에서 깃 레포 클론
1 2 3 4
##레포를 저장할 경로에 디렉토리 생성 mkdir -p /dockerProjects/blog/source cd /dockerProjects/blog/source git clone https://github.com/${git_id}/${repo_name}.git
- EC2 인스턴스에서 깃 레포 클론
- 추가 secret설정파일 생성
1 2
## ex)application-secret.yml생성 vim src/main/resources/application-secret.yml
- 접속 권한 설정
- docker에서 접속 시에 172.17.0.1로 접속을 하는데 spring 내부에서 이 아이피에 대한 권한을 설정해줘야한다.
docker exec -it mysql_1 mysql -u root -p CREATE USER 'root'@'172.17.0.1' IDENTIFIED BY '${password}'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'172.17.0.1'; # 권한부여 # REVOKE ALL PRIVILEGES ON *.* FROM 'root'@'172.17.0.1'; # 이 명령어는 알아두세요. root@172.17.0.1 사용자의 권한을 삭제하는 명령어 입니다. FLUSH PRIVILEGES; exit
- docker에서 접속 시에 172.17.0.1로 접속을 하는데 spring 내부에서 이 아이피에 대한 권한을 설정해줘야한다.
- 접속 권한 설정
- 도커 이미지 빌드
1
docker build -t ${이미지명} .
-t
옵션 : 태그네임.
: 현재 폴더에 도커 파일을 읽고 빌드
- 도커 이미지 빌드
- 도커 실행
- 도메인으로 접속
깃허브 액션을 통한 CI/CD 파이프라인 구축
CI/CD 파이프라인 과정
- 깃허브 저장소에 코드를 푸시한다.
- 깃허브 액션을 통해 코드를 빌드하고 테스트 한다.
- 깃허브 가상서버에서 도커이미지를 만들고 docker registry에 도커 이미지를 푸쉬한다.
- 깃허브 가상서버에서 AWS SSM을 통해 EC2 인스턴스에 배포 스크립트(파이썬코드)를 실행한다.
- 파이썬 코드를 통해 현재 상태를 확인하고, 빈 Port(8082)에 새로운 버전의 컨테이너를 실행한다.
- 포트 전환을 통해(socat사용) 무중단 배포를 구현한다.
socat 설치
- socat은 로컬에서 원격 서버로 TCP 연결을 전달하는 도구이다.
- 로컬에서 원격 서버로 TCP 연결을 전달하여 무중단 배포를 구현한다.
- EC2 인스턴스에 socat을 설치한다.
1
sudo yum install -y socat
- 소켓의 기능
- 소켓은 IP 주소와 포트 번호로 식별된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
def _switch_port(self) -> None:
# 1. 기존 socat 프로세스 찾아서 종료
cmd: str = f"ps aux | grep 'socat -t0 TCP-LISTEN:{self.socat_port}' | grep -v grep | awk '{{print $2}}'"
pid: str = subprocess.getoutput(cmd)
if pid:
os.system(f"kill -9 {pid} 2>/dev/null")
time.sleep(5) # 프로세스 종료 대기
# 2. 새로운 포트로 socat 연결 시작
os.system(
f"nohup socat -t0 TCP-LISTEN:{self.socat_port},fork,reuseaddr TCP:localhost:{self.next_port} &>/dev/null &"
)
python 설치
무중단 배포 스크립트 작성
배포 프로세스
1 2 3 4 5
def update_service(self) -> None: self._find_current_service() self._find_next_service() self._remove_container(self.next_name) self._run_container(self.next_name, self.next_port)
- 현재 서비스 확인
- 다음 배포 서비스 선정
- 새 버전 컨테이너 실행
- 서비스 상태 확인
- 포트 전환
- 이전 버전 컨테이너 제거
1) ServiceManager 클래스
- 초기 설정을 담당하는 클래스이다
- socat_port: 프록시 포트 (기본값: 8081)
- sleep_duration: 상태 체크 대기 시간 (기본값: 3초)
- services: 서비스 컨테이너 정보 (blog_1: 8082, blog_2: 8083)
1 2 3 4 5 6 7 8
class ServiceManager: def __init__(self, socat_port: int = 8081, sleep_duration: int = 3) -> None: self.socat_port: int = socat_port self.sleep_duration: int = sleep_duration self.services: Dict[str, int] = { 'blog_1': 8082, 'blog_2': 8083 }
2) 현재 서비스 확인
- 현재 실행 중인 서비스를 찾는 메서드다
- socat 프로세스를 확인하여 현재 포워딩되는 포트를 찾는다
- 현재 서비스가 없으면 blog_2를 기본값으로 설정한다
1 2 3
def _find_current_service(self) -> None: cmd: str = f"ps aux | grep 'socat -t0 TCP-LISTEN:{self.socat_port}' | grep -v grep | awk '{print $NF}'" current_service: str = subprocess.getoutput(cmd)
3) 다음 서비스 설정
- 다음에 배포할 서비스를 결정하는 메서드다:
- 현재 실행 중이지 않은 서비스를 다음 배포 대상으로 선정한다
1 2 3 4 5
def _find_next_service(self) -> None: self.next_name, self.next_port = next( ((name, port) for name, port in self.services.items() if name != self.current_name), (None, None) )
3) Docker 컨테이너 관리
- Docker 컨테이너 관리 메서드들:
- _remove_container: 기존 컨테이너 중지 및 제거
- _run_container: 새 버전의 컨테이너 실행
1 2 3 4 5 6 7
def _remove_container(self, name: str) -> None: os.system(f"docker stop {name} 2> /dev/null") os.system(f"docker rm -f {name} 2> /dev/null") def _run_container(self, name: str, port: int) -> None: os.system( f"docker run -d --name={name} --restart unless-stopped -p {port}:8090 -e TZ=Asia/Seoul -v /dockerProjects/blog/volumes/gen:/gen --pull always ghcr.io/kknaks/blog")
- 2> /dev/null은 표준 에러(stderr)를 /dev/null로 리다이렉트한다.
2는 stderr를 의미
/dev/null은 모든 출력을 버리는 특수 파일
결과적으로 에러 메시지가 화면에 출력되지 않음
4) 포트 전환
- socat을 이용한 포트 포워딩 전환 메서드다:
- 기존 socat 프로세스 종료
- 새로운 포트로 포워딩 설정
1 2 3
def _switch_port(self) -> None: cmd: str = f"ps aux | grep 'socat -t0 TCP-LISTEN:{self.socat_port}' | grep -v grep | awk '{{print $2}}'" pid: str = subprocess.getoutput(cmd)
5) 서비스 상태 확인
- Spring Boot Actuator를 이용한 서비스 상태 확인 메서드다:
- /actuator/health 엔드포인트로 상태 확인
- 5초 타임아웃 설정
- 200 응답과 ‘UP’ 상태 확인
1 2 3 4 5 6 7 8 9
def _is_service_up(self, port: int) -> bool: url = f"http://127.0.0.1:{port}/actuator/health" try: response = requests.get(url, timeout=5) if response.status_code == 200 and response.json().get('status') == 'UP': return True except requests.RequestException: pass return False
GitHub Actions Workflow 설명
1) Workflow 트리거 설정
- main 브랜치에 특정 파일들이 변경되어 push될 때 워크플로우가 실행된다.
1 2 3 4 5 6 7 8 9 10 11 12
name: 'deploy' on: push: paths: - '.github/workflows/**' - 'src/**' - 'build.gradle' - 'Dockerfile' - 'readme.md' - 'infraScript/**' branches: - 'main'
2) Jobs 구성
- 1. makeTagAndRelease
- GitHub 태그와 릴리즈를 생성하는 작업을 수행한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
jobs: makeTagAndRelease: runs-on: ubuntu-latest outputs: tag_name: $ steps: - uses: actions/checkout@v4 - name: Create Tag id: create_tag uses: mathieudutour/github-tag-action@v6.1 with: github_token: $ - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: $ with: tag_name: $ release_name: Release $ body: $ draft: false prerelease: false
- 2. buildImageAndPush
Docker 이미지를 빌드하고 GitHub Container Registry에 푸시하는 작업을 수행한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
buildImageAndPush: name: 도커 이미지 빌드와 푸시 needs: makeTagAndRelease runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Docker Buildx 설치 uses: docker/setup-buildx-action@v2 - name: 레지스트리 로그인 uses: docker/login-action@v2 with: registry: ghcr.io username: $ password: $
주요 설정:
- Docker Buildx 설치
- GitHub Container Registry 로그인
- application-secret.yml 파일 생성
- 이미지 빌드 및 푸시 (latest 태그와 버전 태그 모두 생성)
- 3. deploy
AWS SSM을 사용하여 EC2 인스턴스에 배포 스크립트를 실행한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
deploy: runs-on: ubuntu-latest needs: [ buildImageAndPush ] steps: - name: AWS SSM Send-Command uses: peterkimzz/aws-ssm-send-command@master id: ssm with: aws-region: $ aws-access-key-id: $ aws-secret-access-key: $ instance-ids: "i-0562e21a9358707e3" working-directory: / comment: Deploy command: | mkdir -p /dockerProjects/blog curl -o /dockerProjects/blog/zero_downtime_deploy.py https://raw.githubusercontent.com/kknaks/deploy-demo-01-25/main/infraScript/zero_downtime_deploy.py chmod +x /dockerProjects/blog/zero_downtime_deploy.py /dockerProjects/blog/zero_downtime_deploy.py
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.