[TIL] BE-16 - Git과 Docker, AWS EC2, GitHub Actions CI/CD

W L·2026년 9월 10일

TIL

목록 보기
28/32

1. 오늘 학습한 주제

  • DevOps와 CI/CD
  • Git 기본 명령어 — init, add, commit, push, fetch, pull
  • .gitignore와 커밋 메시지 작성법
  • Docker — 이미지와 컨테이너, 기본 명령어, Dockerfile
  • AWS — IAM, EC2, 보안 그룹
  • GitHub Actions — Workflow 구조, secrets
  • 백엔드 CI/CD 구축 — EC2 빌드 / GitHub Actions 빌드 + SCP / Docker + ECR

2. 배운 내용

DevOps

기존 방식은 단계마다 담당자가 나뉘어 있다.

고객 요구 → PM(요구사항 정의서) → 개발자(구현, 빌드) → 테스터 → 배포 관리자

그룹마다 업무를 보는 관점이 다르고 각자 자기 담당 일만 아는 경우가 많아서, 실질적인 협업이 이루어지지 않는다. 중간에 문제가 생기면 누가 책임을 져야 하는지도 불분명하다.

DevOps는 이 문제를 해결하기 위한 관행 또는 문화다. 개발과 운영을 단순히 합친 것이 아니라, 개발자가 운영까지 고려하고 운영자가 개발의 흐름을 이해하며 그 과정을 도구로 자동화하는 것이다.

핵심은 자동화다. 빌드 → 테스트 → 배포처럼 수동으로 반복하는 작업은 생산성을 떨어뜨린다. 계획부터 운영, 모니터링까지를 멈추지 않고 연결해 두고, 새 버전이 나올 때마다 같은 단계를 반복한다.

CI/CD

빌드, 테스트, 배포 과정을 자동화하는 것이다.

구분의미흐름
CI (Continuous Integration)지속적 통합. 코드를 자동으로 합치고 검증각자 작성 → GitHub push → 자동 빌드 + 테스트 → 문제 있으면 즉시 알림
CD (Continuous Delivery/Deployment)CI를 통과한 코드를 서버에 배포CI 통과 → 서버에 자동 배포 → 사용자가 새 버전 사용

CD는 Delivery와 Deployment 두 의미로 쓰인다. Delivery는 배포 가능한 상태까지 자동화하고 실제 반영은 사람이 결정하는 쪽, Deployment는 서버 반영까지 자동으로 하는 쪽을 가리키는 경우가 많다. 이번 특강의 실습은 서버 반영까지 자동화하는 형태였다.

일반적인 CI/CD 과정은 이렇다.

기능 개발 완료 후 commit & push
→ 변경 사항을 감지해 빌드
→ 작성한 테스트 코드 실행 (테스트 코드가 없으면 생략)
→ 테스트 통과 시 서버에 최신 코드 배포

Git 기본 명령어

Git은 소스 코드의 이력을 관리하고 협업을 지원하는 분산 버전 관리 시스템이다.

명령어역할
git init변경사항 추적 시작. 로컬 저장소 생성 (.git 폴더)
git add작업 디렉토리의 변경 사항을 스테이징 영역에 추가
git commit스테이징된 변경 사항을 로컬 저장소에 기록 (snapshot)
git push로컬 저장소의 커밋을 원격 저장소에 업로드
git fetch원격 저장소의 최신 내용을 가져오기만 하고 병합하지 않음
git pull원격 저장소의 최신 내용을 가져와 현재 브랜치에 병합
# 변경 사항을 추적할 특정 파일을 추가
git add index.html

# 현재 경로의 모든 파일의 변경 사항을 추적하도록 추가
git add .

# 메시지(-m)와 함께 버전(커밋)을 생성
git commit -m "Initial commit: 프로젝트 초기 구조 생성"

fetch는 변경 사항을 확인만 할 때 쓴다. pull은 기본적으로 fetch와 merge를 한 번에 수행한다. 협업 시 충돌을 줄이기 위해 작업 전에 pull을 먼저 받는다.

# 원격 저장소(origin)의 모든 브랜치 정보 가져오기
git fetch origin

# 원격 저장소(origin)의 main 브랜치 변경 사항을 가져와 현재 브랜치에 병합
git pull origin main

원격 저장소 정보는 git remote -v, 현재 브랜치의 추적 정보는 git branch -vv로 확인한다.

시나리오 1 — 로컬에서 시작 → GitHub 업로드

# 1단계: 프로젝트 폴더 생성 및 초기화
mkdir my-new-project
cd my-new-project
git init

# 2단계: 기본 파일 생성
echo "# My New Project" > README.md
echo "node_modules/" > .gitignore

# 3단계: 첫 번째 커밋
git add .
git commit -m "Initial commit: 프로젝트 초기 설정"

# 4단계: GitHub에서 새 저장소 생성 후 연결
# (GitHub 웹사이트에서 New Repository 클릭하여 생성)
git remote add origin https://github.com/username/my-new-project.git

# 5단계: GitHub에 업로드
git branch -M main
git push -u origin main

-u로 추적할 원격 브랜치를 지정해 두면 이후에는 git push만으로 올릴 수 있다.

시나리오 2 — GitHub 저장소 먼저 생성 → 로컬에 클론

# 1단계: GitHub에서 저장소 생성 후 클론
git clone https://github.com/username/existing-project.git
cd existing-project

# 2단계: 작업 수행
# (파일 생성 및 수정)

# 3단계: 변경사항 저장 및 업로드
git add .
git commit -m "feat: 새로운 기능 추가"
git push -u origin main

.gitignore

설정 파일, 빌드 산출물, 라이브러리 파일, API Key처럼 보안이 중요하거나 불필요한 파일은 저장소에 올리지 않는다.

# .gitignore 추가하기

.idea
.vscode
.DS_Store

.env
application.yaml

커밋 메시지 작성법

  • 첫번째 라인: 제목 (필수)
  • 두번째 라인: 공란
  • 세번째 라인부터: 무엇을 왜 했는지 (선택)
  • 마지막 라인: 꼬리말, 이슈 번호 (선택)

최대한 구체적으로 쓴다. 주간 보고에 그대로 옮겨 쓸 수 있을 정도가 기준이다.

fix: Safari에서 모달을 띄웠을 때 스크롤 이슈 수정

모바일 사파리에서 Carousel 모달을 띄웠을 때,
모달 밖의 상하 스크롤이 움직이는 이슈 수정.

resolves: #1137

Docker 개요

Docker를 쓰는 가장 큰 이유는 이식성이다. 특정 프로그램을 다른 곳으로 쉽게 옮겨서 설치하고 실행할 수 있다.

  • 설치 과정을 매번 일일이 거치지 않아도 된다
  • 버전, 환경 설정, 옵션, 운영 체제가 달라도 항상 일관되게 설치된다
  • 각 프로그램이 독립된 환경에서 실행되어 서로 충돌하지 않는다

Docker는 컨테이너를 사용해 각 프로그램을 격리된 공간에서 실행하고 관리하는 도구다.

컨테이너는 호스트 컴퓨터 안에서 돌아가는 격리된 실행 환경이다. 수업에서는 "미니 컴퓨터"에 비유했다. 다만 VM처럼 OS 전체를 따로 갖는 것은 아니고 호스트의 커널을 공유한다. 컨테이너끼리는 서로 영향을 주지 않는다.

내 컴퓨터 (호스트)
├── 컨테이너 A  (Java 11 환경)
├── 컨테이너 B  (Java 21 환경)
└── 컨테이너 C  (Python 환경)

호스트는 컨테이너를 품고 있는 컴퓨터다. 컨테이너와 구분하기 위해 이렇게 부른다.

이미지는 컨테이너를 만들기 위한 설계도다. 코드, 설정, 환경 등 실행에 필요한 것이 모두 담겨 있다.

설계도 → 실제 건물
클래스 → 객체
이미지 → 컨테이너

하나의 이미지로 동일한 컨테이너를 여러 개 찍어낼 수 있음

이미지는 읽기 전용이라 수정할 수 없다. 바꿔야 하면 새 이미지를 만든다.

Docker 기본 명령어

명령어역할
docker pull레지스트리(Docker Hub 등)에서 이미지 다운로드
docker images로컬 이미지 목록
docker rmi이미지 삭제
docker run이미지로 컨테이너 생성 + 실행 (create + start)
docker ps컨테이너 목록
docker stop / start / kill정상 종료 / 재시작 / 강제 종료
docker rm컨테이너 삭제
# docker pull 이미지명
# Nginx 최신 버전 이미지 다운로드
docker pull nginx # docker pull nginx:latest와 동일하게 작동

# 이미지 목록 확인
docker image ls

# 또는
docker images

# 이미지 삭제
docker image rm [이미지 ID 또는 이미지명]

# 또는
docker rmi [이미지 ID 또는 이미지명]

이미지 ID는 일부만 입력해도 된다. 단, 입력한 일부를 가진 이미지가 하나뿐이어야 한다.

run은 옵션을 하나씩 붙여가며 확인했다.

옵션의미
-d백그라운드 실행
-p 8080:80호스트 포트 8080을 컨테이너 포트 80에 연결
--name컨테이너 이름 지정
# 백그라운드(-d)에서 실행하고 호스트 포트 8080을 컨테이너 포트 80에 연결(-p)
# 공식 nginx 이미지는 기본 설정이 80 포트
docker run -d -p 8080:80 --name my-web-server nginx

http://localhost:8080/에 접속하면 nginx 기본 페이지가 나온다.

# 실행 중인 컨테이너 확인
docker ps

# 중지된 컨테이너를 포함한 모든 컨테이너 확인
docker ps -a

# 컨테이너 중지 (정상 종료)
docker stop my-web-server

# 컨테이너 강제 종료
docker kill 컨테이너명[또는 컨테이너 ID]

# 실행 중인 컨테이너 전체 종료
docker stop $(docker ps -q)

# 컨테이너 재시작
docker start my-web-server
# 중지되어 있는 특정 컨테이너 삭제
# 실행 중인 컨테이너는 중지한 후에만 삭제가 가능하다.
docker rm 컨테이너명[또는 컨테이너 ID]

# 실행되고 있는 특정 컨테이너 삭제
docker rm -f 컨테이너명[또는 컨테이너 ID]

# 전체 삭제 (중지된 것 포함)
docker rm -f $(docker ps -aq)

Dockerfile과 이미지 빌드

Dockerfile은 Docker 이미지를 만들게 해주는 파일이다. 여기에 정의된 설정대로 새 이미지가 생성된다.

# 베이스 이미지 지정 (Node.js 14 버전)
FROM node:14

# 컨테이너 안에서 작업할 디렉토리 설정
# 이후 명령어들은 모두 /app 안에서 실행됨
WORKDIR /app

# package.json, package-lock.json을 컨테이너로 복사
# (COPY 호스트경로 컨테이너경로)
COPY package*.json ./
# package.json이 안 바뀌면 캐시 재사용, npm install 건너뜀

# 의존성 패키지 설치
RUN npm install

# 나머지 소스코드 전체를 컨테이너로 복사
COPY . .

# 컨테이너 실행 시 시작 명령어
CMD ["node", "app.js"]
명령어실행 시점
RUN이미지를 생성하는 과정
CMD, ENTRYPOINT이미지로 컨테이너가 시작될 때
# 현재 디렉토리(.)의 Dockerfile을 사용하여 'my-app:v1'이라는 '이미지이름:태그'로 이미지 빌드
docker build -t my-app:v1 .

태그를 생략하면 latest가 붙는다.

샘플 프로젝트(devops-front, devops-backend)로 이미지를 만들고 컨테이너를 띄웠다.

# 백엔드
docker build -t backend:local .
docker run -d -p 8088:8088 backend:local

# 프론트엔드
docker build -t front:local .
docker run -d -p 80:80 front:local

샘플의 Dockerfile에는 FROM이 여러 개 있다. 각 FROM은 새 이미지를 시작하고, 최종 이미지는 마지막 스테이지 하나로만 구성된다.

AWS — IAM, EC2

AWS는 아마존의 클라우드 서비스 플랫폼이다. 내 컴퓨터 대신 아마존 데이터센터의 컴퓨터를 빌려서 서버, 저장소, 네트워크를 필요한 만큼만 쓴다.

예전에는 서버를 직접 사서 자사 건물에 설치해 운영했다(온프레미스). 초기 비용이 크고 확장이 어렵다. AWS는 필요할 때 켜고 필요 없으면 끈다.

서비스내용
IAM권한 관리. AWS 계정 안에서 누가 무엇을 할 수 있는지 정의
EC2가상 서버. 사양 조절 가능(Elastic), 켜둔 시간만큼 과금

IAM 사용자의 Access Key와 Secret Access Key는 따로 저장해 둔다.

EC2 인스턴스 생성 시 설정은 다음과 같다. 리전을 먼저 확인한다.

항목설정비고
이름devops-server역할을 알아볼 수 있게 짓는다
AMI (OS)Ubuntu Server 최신 LTSWindows, macOS는 기능이 많은 만큼 무겁다. Ubuntu는 서버에 필요한 기능만 있어 가볍다
인스턴스 유형t3.medium (최소 t3a.small)인스턴스는 빌린 컴퓨터 1대, 유형은 그 사양
키 페어새 .pem 생성 후 다운로드EC2 접근용 열쇠. 유출되면 위험. 이름은 devops-server-key처럼 대상이 보이게

보안 그룹

보안 그룹은 AWS에서의 네트워크 보안이다. EC2를 집이라고 하면 보안 그룹은 집 바깥의 울타리와 대문에 해당한다. 들어오는 트래픽(인바운드)과 나가는 트래픽(아웃바운드) 중 무엇을 허용할지 규칙으로 정한다.

유형프로토콜포트 범위소스용도
SSHTCP22Anywhere (0.0.0.0/0)원격 접속용
HTTPTCP80Anywhere (0.0.0.0/0)웹 서비스용
사용자 지정 TCPTCP8080Anywhere (0.0.0.0/0)backend 접속용
사용자 지정 TCPTCP8088Anywhere (0.0.0.0/0)backend 접속용

인스턴스 상태 중 종료는 컴퓨터를 삭제하는 것이다. 한 번 실행하면 취소할 수 없다. 잠시 끄는 것은 중지다.

외부에서는 퍼블릭 IPv4 주소로 접근한다. 접속은 콘솔의 "EC2 인스턴스 연결"이나 SSH 클라이언트로 한다. Windows는 git bash나 WSL, macOS는 기본 터미널을 쓴다.

chmod 400 "~~~.pem"
ssh -i "~~~.pem" ubuntu@ec2-...

chmod 400은 키 파일을 소유자만 읽을 수 있게 만든다. 권한이 열려 있는 키 파일은 ssh가 거부한다.

GitHub Actions

GitHub Actions는 CI/CD의 빌드, 테스트, 배포 로직을 Workflow로 정의해 실행하는 기능이다. 실행할 서버를 GitHub가 제공하므로 빌드용 서버를 따로 둘 필요가 없다.

Jenkins로도 같은 CI/CD를 구성할 수 있다. Jenkins는 별도 서버에 구축해야 해서 서버 비용과 세팅 시간이 든다. 그래도 Jenkins가 필요한 경우가 있다.

  • GitHub 외 다른 VCS를 쓰는 경우 (GitLab, Bitbucket, 사내 Gerrit/SVN)
  • 온프레미스/폐쇄망(에어갭) 환경
  • 복잡한 파이프라인 오케스트레이션이 필요할 때
  • 대규모 빌드가 많을 때. GitHub Actions는 private 저장소 기준 분당 과금이라, 자체 서버에 Jenkins를 고정비로 운영하는 쪽이 더 저렴할 수 있다

흐름은 이렇다.

코드 작성 후 Commit → GitHub에 Push
→ Push를 감지해 Workflow 실행 (빌드 → 테스트 → 서버로 배포)
→ 서버에서 최신 코드로 재실행

Workflow 작성

파일은 반드시 프로젝트 루트의 .github/workflows 디렉터리에 .yml 또는 .yaml 확장자로 둔다. 파일명은 자유다.

# Workflow의 이름
# Workflow: 하나의 yaml 파일을 하나의 Workflow라고 부름
name: Github Actions 기본 사용법 실습

# Event: 어느 시점에 실행시킬지 실행되는 시점을 설정
# main 브랜치에 push 될 때 아래 Workflow를 실행
on:
  push: # Event 예시: pull_request, push, workflow_dispatch 등
    branches:
      - main
#      - develop

# 하나의 Workflow는 1개 이상의 Job으로 구성됨
# 여러 Job들은 기본적으로 병렬적으로 수행됨
jobs:
  # Job을 식별하기 위한 id
  My-Deploy-Job:
    # Github Actions를 실행시킬 서버의 OS 종류 선택
    runs-on: ubuntu-latest # ubuntu 환경 / 가장 최신 버전(latest)

    # Step: 특정 작업을 수행하는 가장 작은 단위
    # Job은 여러 Step들로 구성되고, 순차적으로 실행됨
    steps:
      - name: Hello World 출력  # Step별 이름 지정
        run: echo "Hello World" # 실제 실행시킬 명령어 작성
단위내용
Workflowyaml 파일 하나
Event실행 시점 (push, pull_request, workflow_dispatch 등)
JobWorkflow를 구성. 기본적으로 병렬 실행
StepJob을 구성하는 가장 작은 단위. 순차 실행

push 후 Actions 탭에서 Workflow 이름, 커밋 메시지, Job ID, 각 Step의 콘솔 출력을 확인할 수 있다. 실행 중인 Workflow는 주황색으로 표시된다.

Step을 추가하며 여러 줄 명령어, 기본 제공 변수, Secret을 차례로 확인했다.

      - name: 여러 명령어 문장 작성하기 # 여러 줄 입력을 위해 | 기호 사용
        run: |
          echo "Good"
          echo "Morning"

      # 참고: https://docs.github.com/en/actions/learn-github-actions/variables
      - name: Github Actions 자체에 저장되어 있는 변수 사용해보기 # 변수를 의미하는 $ 기호
        run: |
          echo $GITHUB_SHA
          echo $GITHUB_REPOSITORY

      - name: Github Actions Secret 변수 사용해보기 # 아무한테나 노출이 되면 안 되는 값
        run: |
          echo ${{ secrets.MY_NAME }}
          echo ${{ secrets.MY_HOBBY }}

$GITHUB_SHA는 commit id, $GITHUB_REPOSITORY는 리포지터리 명이다.

Secret은 [Settings] → [Secrets and variables] → [Actions] → [Secrets] → [New repository secret]에서 저장한다. 로그에는 값이 ***로 가려져 출력된다.

백엔드 CI/CD 1 — EC2에서 직접 빌드 (개인 프로젝트)

가장 단순한 방식이다. GitHub Actions는 EC2에 원격 접속만 하고, 빌드는 EC2에서 한다.

Spring Boot 프로젝트에 컨트롤러 하나를 만들고 GitHub에 올렸다.

@RestController
public class AppController {
  @GetMapping("/")
  public String home() {
    return "Hello, World!";
  }
}

EC2에 JDK를 설치하고 clone 받아 직접 실행해 본다.

# 1. 패키지 업데이트 및 JDK 설치
sudo apt update
sudo apt install -y openjdk-21-jdk

# 2. 설치 확인
java -version
git clone {github repository 주소}
cd {프로젝트 경로}
./gradlew clean build
cd build/libs
nohup java -jar XXXXX-XXX-SNAPSHOT.jar &

sudo lsof -i:8080 # 8080번 포트에 Spring Boot가 실행되고 있는 지 확인

nohup ... &는 터미널을 닫아도 프로세스가 백그라운드에서 계속 돌게 한다. [EC2 Public IP]:8080으로 접속해 확인한다.

자동화 전에는 배포할 때마다 이 과정을 손으로 반복해야 한다.

$ cd {프로젝트 경로}
$ git pull origin main
$ sudo lsof -i:8080 # 8080번 포트에 Spring Boot가 실행되고 있는 지 확인
$ sudo fuser -k -n tcp 8080 # 8080번 포트에 실행되고 있는 프로세스 종료
$ ./gradlew clean build
$ cd build/libs
$ nohup java -jar XXXXX-XXX-SNAPSHOT.jar &

private 저장소는 clone/pull 때마다 GitHub 계정과 토큰을 물어본다. 비밀번호 자리에는 실제 계정 비밀번호가 아니라 GitHub 토큰을 넣는다. 한 번만 입력하게 하려면 credential.helper를 쓴다.

git config --global credential.helper store
git pull origin main
# Github 계정 및 비밀번호 입력 (최초 한 번만 작성)

git pull origin main # 이후 더 이상 비밀번호를 안 묻는 걸 확인할 수 있다.

# 확인
cd ~
cat .git-credentials

이 수동 과정을 Workflow로 옮긴다.

name: Deploy To EC2

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # appleboy/ssh-action의 공식 문서
      # https://github.com/marketplace/actions/ssh-remote-commands
      - name: SSH(원격 접속)로 EC2에 접속하기
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }} # EC2의 주소
          username: ${{ secrets.EC2_USERNAME }} # EC2 접속 username (기본값: ubuntu)
          key: ${{ secrets.EC2_PRIVATE_KEY }} # EC2의 Key 파일의 내부 텍스트
          script_stop: true # 아래 script 중 실패하는 명령이 하나라도 있으면 실패로 처리
          script: |
            cd /home/ubuntu/devops-service # 경로는 자신의 EC2에 맞는 경로로 재작성하기
            git pull origin main
            ./gradlew clean build
            sudo fuser -k -n tcp 8080 || true # || true를 붙인 이유는 8080에 종료시킬 프로세스가 없더라도 실패로 처리하지 않기 위함 
            # jar 파일을 실행시키는 명령어. 그리고 발생하는 로그들을 ./output.log 파일에 남기는 명령어.
            nohup java -jar build/libs/*SNAPSHOT.jar > ./output.log 2>&1 & 

script_stop: true라서 명령 하나라도 실패하면 전체가 실패한다. 처음 배포할 때처럼 8080에 떠 있는 프로세스가 없으면 fuser가 실패하므로 || true로 넘긴다. 2>&1은 에러 출력도 같은 로그 파일로 보낸다.

GitHub Secret에 넣는 값은 세 가지다.

Secret값
EC2_HOSTEC2의 Public IP 주소
EC2_USERNAMEEC2 접속 username (기본값: ubuntu)
EC2_PRIVATE_KEY.pem 키 파일의 내부 텍스트

키 파일 내용은 cat devops-service.pem으로 확인한다. 출력 맨 끝에 붙는 %는 제외한다. 파일 끝에 개행이 없을 때 터미널(zsh 등)이 표시하는 기호라 키 내용이 아니다.

백엔드 CI/CD 2 — 설정 파일 관리

API Key나 비밀번호 같은 값을 application.yaml로 분리하고 .gitignore에 넣으면, 저장소에는 이 파일이 없다. 배포할 때마다 파일을 따로 넣어줘야 하는 문제가 생긴다.

aws:
  access-key: ABCD
  secret-key: 1234

이미 GitHub에 올라가 있는 파일은 .gitignore에 추가해도 계속 추적된다. 로컬 파일은 두고 Git 추적만 제거한다.

git status
git rm --cached -r .
git add .
git commit -m "add .yaml"
git push
옵션의미
--cached로컬 파일은 삭제하지 않고 Git 추적만 제거
-r디렉토리(하위 파일 포함) 대상

단일 파일은 git rm --cached file.txt, 디렉토리는 git rm --cached -r build/처럼 쓴다. 브랜치마다 따로 적용되므로 기준 브랜치(보통 develop 또는 main)에서 처리한다.

파일 내용을 Secret(APPLICATION_PROPERTIES)에 통째로 저장하고, 배포 스크립트에서 파일로 만든다.

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: SSH(원격 접속)로 EC2에 접속하기
        uses: appleboy/ssh-action@v1.0.3
        env:
          APPLICATION_PROPERTIES: ${{ secrets.APPLICATION_PROPERTIES }}
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          envs: APPLICATION_PROPERTIES
          script_stop: true
          script: |
            cd /home/ubuntu/devops-service
            rm -rf src/main/resources/application.yaml
            git pull origin main
            echo "$APPLICATION_PROPERTIES" > src/main/resources/application.yaml
            ./gradlew clean build
            sudo fuser -k -n tcp 8080 || true
            nohup java -jar build/libs/*SNAPSHOT.jar > ./output.log 2>&1 & 

Secret을 env로 Step의 환경 변수에 올리고, envs로 원격 스크립트에 전달한다. 스크립트에서는 echo로 그 값을 파일에 쓴다.

EC2에서 파일이 생겼는지 확인한다.

cd src/main/resources
cat application.yaml

백엔드 CI/CD 3 — GitHub Actions에서 빌드 (일반 프로젝트)

앞의 방식에는 단점이 있다.

  • 빌드를 운영 중인 EC2에서 하므로 서버 성능에 영향을 준다
  • GitHub 계정 정보가 EC2에 저장되므로 개인/토이 프로젝트에서만 써야 한다

그래서 빌드와 테스트를 GitHub Actions 안에서 돌리고, 산출물(jar)만 EC2로 보낸다.

name: Deploy To EC2

on:
  push:
    branches:
      - main

jobs:
  Deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Github Repository에 push한 코드 불러오기
        uses: actions/checkout@v4 # Repository에 저장된 코드를 불러오는 라이브러리

      # GitHub Actions에서 빌드하기 위해 JDK 필요
      - name: JDK 21버전 설치
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21

      - name: application.yml 파일 만들기
        run: |
          mkdir -p src/main/resources 
          echo "${{ secrets.APPLICATION_PROPERTIES }}" > src/main/resources/application.yaml

      # 실행 권한이 없는 경우를 대비
      - name: gradlew 실행 권한 부여
        run: chmod +x gradlew

      - name: 테스트 및 빌드하기
        run: ./gradlew clean build

      # 편의를 위해 파일 이름 및 위치 변경 (필수X)
      - name: 빌드된 파일 이름 변경하기
        run: mv ./build/libs/*SNAPSHOT.jar ./project.jar

      # SCP: EC2로 파일을 전송할 때 쓰는 방식
      - name: SCP로 EC2에 빌드된 파일 전송하기
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          source: project.jar
          target: /home/ubuntu/devops-server/output

      # https://github.com/marketplace/actions/ssh-remote-commands
      - name: SSH(원격 접속)로 EC2에 접속하기
        uses: appleboy/ssh-action@v1.0.3 # GitHub Actions 라이브러리
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          script_stop: true
          script: |
            rm -rf /home/ubuntu/devops-server/current
            mkdir /home/ubuntu/devops-server/current
            mv /home/ubuntu/devops-server/output/project.jar /home/ubuntu/devops-server/current/project.jar
            cd /home/ubuntu/devops-server/current
            sudo fuser -k -n tcp 8080 || true
            nohup java -jar project.jar > ./output.log 2>&1 & 
            rm -rf /home/ubuntu/devops-server/output

GitHub Actions의 서버에는 코드도 JDK도 없는 상태로 시작하므로 checkout과 setup-java가 먼저 온다. EC2에는 git clone도 빌드도 필요 없고, 받은 jar를 실행만 한다.

백엔드 CI/CD 4 — Docker 환경

Docker 기반으로 운영할 때의 가장 간단한 구조다.

구성역할
GitHub코드 저장소
Docker HubDocker 이미지 저장소
AWS ECRAWS가 제공하는 Docker 이미지 저장소

GitHub Actions가 빌드한 jar로 이미지를 만들어 ECR에 올리고, EC2는 ECR에서 이미지를 받아 컨테이너로 실행한다.

EC2에 Docker와 Docker Compose를 설치한다.

# Docker (Ubuntu 기본 저장소 기준)
# 1. 패키지 업데이트 및 Docker 설치
sudo apt update
sudo apt install -y docker.io docker-compose-v2

# 2. sudo 없이 docker 명령어 사용하기 위해 ubuntu 사용자를 docker 그룹에 추가
sudo usermod -aG docker ubuntu
# 그룹 변경 즉시 적용
newgrp docker

# 3. 설치 확인
docker --version
docker compose version

# 4. Docker가 실행되고 있는지 상태 확인
systemctl status docker

ubuntu는 EC2 기본 사용자명이다. 다른 사용자명이면 $USER를 쓴다.

ECR에 접근하는 쪽은 GitHub Actions(push)와 EC2(pull) 둘이고, 권한을 주는 방식이 다르다.

접근 주체권한 부여 방식
GitHub ActionsIAM 사용자에 AmazonEC2ContainerRegistryFullAccess 추가 → 액세스 키 발급 → GitHub Secret(AWS_ACCESS_KEY, AWS_SECRET_ACCESS_KEY) 등록
EC2EC2에 연결된 IAM Role에 AmazonEC2ContainerRegistryFullAccess 추가

EC2가 private ECR에서 이미지를 받으려면 Amazon ECR Docker Credential Helper도 설치한다.

# Ubuntu일 경우
$ sudo apt update
$ sudo apt install amazon-ecr-credential-helper

~/.docker/config.json을 만들어 Docker가 이 helper로 인증하게 한다.

cd ~
mkdir .docker
cd .docker
vi config.json
{
	"credsStore": "ecr-login"
}

프로젝트 루트에 Dockerfile을 둔다. 빌드된 jar를 이미지에 복사해 실행만 한다.

FROM eclipse-temurin:21-jdk-alpine
COPY ./build/libs/*SNAPSHOT.jar project.jar
ENTRYPOINT ["java", "-jar", "project.jar"]

.github/workflows/deploy.yml의 jobs 부분이다. 빌드까지는 앞의 방식과 같고, 그 뒤가 Docker로 바뀐다.

jobs:
  Deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Github Repository에 push한 코드 불러오기
        uses: actions/checkout@v4 # Repository에 저장된 코드를 불러오는 라이브러리

      # GitHub Actions에서 빌드하기 위해 JDK 필요
      - name: JDK 21버전 설치
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21

      - name: application.yml 파일 만들기
        run: |
          mkdir -p src/main/resources 
          echo "${{ secrets.APPLICATION_PROPERTIES }}" > src/main/resources/application.yaml

      # 실행 권한이 없는 경우를 대비
      - name: gradlew 실행 권한 부여
        run: chmod +x gradlew

      - name: 테스트 및 빌드하기
        run: ./gradlew clean build

      # ECR에 접근할 수 있는 권한이 필요, 권한을 허용받기 위해 사용
      - name: AWS Resource에 접근할 수 있게 AWS credentials 설정
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ap-northeast-2
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: ECR에 로그인하기
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      # Dockerfile 기반으로 이미지 생성
      - name: Docker 이미지 생성
        run: docker build -t devops-server .

      # ${{ steps.login-ecr.outputs.registry }}는 ECR(도커 이미지 저장소) URI 주소
      - name: Docker 이미지에 Tag 붙이기
        run: docker tag devops-server ${{ steps.login-ecr.outputs.registry }}/devops-server:latest

      - name: ECR에 Docker 이미지 Push하기
        run: docker push ${{ steps.login-ecr.outputs.registry }}/devops-server:latest

      - name: SSH로 EC2에 접속하기
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          script_stop: true
          script: |
            docker stop devops-server || true
            docker rm devops-server || true
            docker pull ${{ steps.login-ecr.outputs.registry }}/devops-server:latest
            docker run -d --name devops-server -p 8080:8080 ${{ steps.login-ecr.outputs.registry }}/devops-server:latest

id: login-ecr로 지정한 Step의 출력값을 뒤의 Step에서 steps.login-ecr.outputs.registry로 꺼내 쓴다. EC2 쪽 스크립트는 기존 컨테이너를 지우고 새 이미지를 받아 다시 띄운다. 처음 배포할 때는 지울 컨테이너가 없으므로 || true를 붙인다.

EC2에서 docker ps로 컨테이너가 떠 있는지 확인한다.

profile
한량

0개의 댓글