Home AWS CodePipeline + EC2 CI/CD 파이프라인 구축 가이드
Post
Cancel

AWS CodePipeline + EC2 CI/CD 파이프라인 구축 가이드

개요

PuppyNote 서버를 GitHub에 push하면 자동으로 EC2에 배포되는 CI/CD 파이프라인을 구축했습니다.

AWS CodePipeline, CodeBuild, CodeDeploy를 활용했고 서버는 t4g.small(ARM64) EC2 인스턴스를 사용합니다.


전체 아키텍처

1
2
3
4
5
GitHub (main push)
  → CodePipeline (자동 트리거)
    → CodeBuild (Gradle 빌드 + JAR 생성)
      → S3 (아티팩트 업로드)
        → CodeDeploy (EC2에 JAR 배포 + 재시작)

핵심 파일 3개

CI/CD 파이프라인은 아래 3개 파일로 동작합니다.

파일역할사용 주체
buildspec.yml빌드 명세CodeBuild
appspec.yml배포 명세CodeDeploy
scripts/배포 훅 스크립트CodeDeploy

buildspec.yml

CodeBuild가 읽는 빌드 설정 파일입니다.

SSM Parameter Store에서 환경변수를 읽어와 Gradle 빌드 시 주입합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
version: 0.2

env:
  parameter-store:
    DB_HOST:         "/puppynote/prd/DB_HOST"
    DB_USERNAME:     "/puppynote/prd/DB_USERNAME"
    DB_PASSWORD:     "/puppynote/prd/DB_PASSWORD"
    JWT_SECRET_KEY:  "/puppynote/prd/JWT_SECRET_KEY"
    # ... 나머지 환경변수
    FIREBASE_CONFIG: "/puppynote/prd/FIREBASE_CONFIG"

phases:
  pre_build:
    commands:
      - echo "$FIREBASE_CONFIG" > src/main/resources/puppynote-firebase.json

  build:
    commands:
      - chmod +x gradlew
      # clean: QueryDSL Q클래스 충돌 방지
      - ./gradlew clean bootJar -x test -x asciidoctor --no-daemon

artifacts:
  files:
    - build/libs/*.jar
    - appspec.yml
    - scripts/**/*
  discard-paths: no

cache:
  paths:
    - '/root/.gradle/caches/**/*'
    - '/root/.gradle/wrapper/**/*'

빌드 환경 설정 (ARM64)

t4g.small은 ARM64 아키텍처이므로 CodeBuild 환경도 ARM으로 맞춥니다.

  • OS: Amazon Linux 2023
  • Image: aws/codebuild/amazonlinux2023-aarch64-standard:1.0
  • Environment type: ARM

appspec.yml

CodeDeploy가 읽는 배포 설정 파일입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: 0.0
os: linux
files:
  - source: build/libs/
    destination: /home/ec2-user/app/
permissions:
  - object: /home/ec2-user/app/
    owner: ec2-user
    group: ec2-user
hooks:
  AfterInstall:
    - location: scripts/stop.sh
      timeout: 60
      runas: ec2-user
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 120
      runas: ec2-user
  ValidateService:
    - location: scripts/health.sh
      timeout: 60
      runas: ec2-user

배포 순서는 다음과 같습니다.

1
2
3
4
JAR 파일 EC2 복사
  → stop.sh  (기존 프로세스 종료)
  → start.sh (새 JAR 실행)
  → health.sh (헬스체크 통과 확인)

배포 스크립트

stop.sh

1
2
3
4
5
6
7
8
#!/bin/bash
PID=$(pgrep -f 'puppynote-server')
if [ -n "$PID" ]; then
  echo "기존 프로세스 종료: PID=$PID"
  kill -15 $PID
  sleep 5
fi
echo "종료 완료"

start.sh

SSM Parameter Store에서 환경변수를 읽어 앱을 실행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
APP_DIR=/home/ec2-user/app
LOG_DIR=/home/ec2-user/logs
REGION="ap-northeast-2"
SSM_PREFIX="/puppynote/prd"

get_ssm() {
  aws ssm get-parameter \
    --name "${SSM_PREFIX}/$1" \
    --with-decryption \
    --region $REGION \
    --query "Parameter.Value" \
    --output text
}

export DB_HOST=$(get_ssm DB_HOST)
export DB_USERNAME=$(get_ssm DB_USERNAME)
# ... 나머지 환경변수

JAR_FILE=$(ls $APP_DIR/*.jar | head -1)

nohup java \
  -XX:+UseContainerSupport \
  -XX:MaxRAMPercentage=75.0 \
  -XX:+UseG1GC \
  -Dspring.profiles.active=prd \
  -jar $JAR_FILE \
  > $LOG_DIR/app.log 2>&1 &

health.sh

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
for i in {1..12}; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health-check)
  if [ "$STATUS" = "200" ]; then
    echo "헬스체크 성공"
    exit 0
  fi
  echo "대기 중... ($i/12)"
  sleep 5
done
echo "헬스체크 실패"
exit 1

AWS 콘솔 구축 순서

1. IAM 역할 생성

역할명Trusted Entity용도
ecsTaskExecutionRoleEC2ECS Task 실행
puppynote-ec2-roleEC2EC2 인스턴스 (SSM, S3 접근)
puppynote-codebuild-roleCodeBuild빌드 서비스
puppynote-codedeploy-roleCodeDeploy배포 서비스

EC2 역할에는 아래 정책이 필요합니다.

  • AmazonSSMManagedInstanceCore
  • AmazonS3ReadOnlyAccess
  • SSM Parameter Store 접근 인라인 정책

2. SSM Parameter Store 환경변수 등록

민감한 정보는 모두 SecureString 타입으로 저장합니다.

1
2
3
4
/puppynote/prd/DB_HOST
/puppynote/prd/DB_PASSWORD
/puppynote/prd/JWT_SECRET_KEY
... (총 18개)

Firebase 설정 파일은 JSON을 한 줄로 압축해서 저장합니다.

1
2
3
4
aws ssm put-parameter \
  --name "/puppynote/prd/FIREBASE_CONFIG" \
  --value "$(cat puppynote-firebase.json | tr -d '\n')" \
  --type "SecureString"

3. S3 아티팩트 버킷 생성

CodePipeline이 빌드 산출물을 저장하는 버킷입니다.

  • Bucket name: puppynote-pipeline-artifacts-{계정ID}
  • Versioning: Enable
  • Block public access: 전부 체크

4. EC2 인스턴스 설정

1
2
3
4
5
6
7
8
9
# CodeDeploy 에이전트 설치
sudo dnf install -y ruby wget
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
chmod +x install
sudo ./install auto
sudo systemctl enable codedeploy-agent

# Java 17 설치
sudo dnf install -y java-17-amazon-corretto

5. GitHub 연결 (CodeStar Connections)

Developer Tools → Settings → Connections에서 GitHub 연결을 생성합니다.

생성 후 반드시 콘솔에서 수동 승인(Pending → Available) 이 필요합니다.

6. CodeBuild 프로젝트 생성

  • Source: GitHub 연결
  • Environment: ARM / Amazon Linux 2023
  • Buildspec: buildspec.yml 자동 인식

7. CodeDeploy 설정

  • Application: EC2/On-premises 플랫폼
  • Deployment group: EC2 인스턴스 태그로 타겟 지정

8. CodePipeline 생성

1
Source (GitHub) → Build (CodeBuild) → Deploy (CodeDeploy)

트러블슈팅

QueryDSL Q클래스 충돌

1
error: Attempt to recreate a file for type QBaseTimeEntity

src/main/generated에 Q클래스가 커밋되어 있는 상태에서 어노테이션 프로세서가 재생성을 시도할 때 발생합니다.

해결: clean 태스크 추가

1
./gradlew clean bootJar -x test -x asciidoctor

asciidoctor 빌드 실패

1
property '$3' specifies directory 'build/generated-snippets' which doesn't exist

bootJarasciidoctor에 의존하는데, CI 환경에서는 테스트 스니펫이 없어서 실패합니다.

해결: -x asciidoctor 옵션으로 스킵

CodeBuild SSM 접근 거부

1
AccessDeniedException: not authorized to perform ssm:GetParameters

해결: CodeBuild 역할에 SSM 인라인 정책 추가

1
2
3
4
5
{
  "Effect": "Allow",
  "Action": ["ssm:GetParameters", "ssm:GetParameter"],
  "Resource": "arn:aws:ssm:ap-northeast-2:계정ID:parameter/puppynote/*"
}

stop.sh가 프로세스를 못 찾는 경우

JAR 파일명으로 pgrep을 해야 합니다. app.jar 같은 고정 이름이 아닌 실제 JAR 파일명 패턴을 사용합니다.

1
2
3
4
5
# 잘못된 예
PID=$(pgrep -f 'app.jar')

# 올바른 예
PID=$(pgrep -f 'puppynote-server')

서버 자동 복구 설정

EC2 재시작 시에도 앱이 자동으로 올라오도록 systemd 서비스로 등록합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sudo tee /etc/systemd/system/puppynote.service << 'EOF'
[Unit]
Description=PuppyNote Server
After=network-online.target

[Service]
Type=forking
User=ec2-user
ExecStart=/home/ec2-user/scripts/start.sh
ExecStop=/home/ec2-user/scripts/stop.sh
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable puppynote.service

마무리

전체 파이프라인이 완성되면 main 브랜치에 push하는 것만으로 자동 배포가 이루어집니다.

1
2
git push origin main
  → 2~3분 후 자동 배포 완료

ECS 대비 관리 포인트가 적고, 소규모 서비스에서는 EC2 직접 배포가 훨씬 단순하고 비용 효율적입니다.

This post is licensed under CC BY 4.0 by the author.

Elasticsearch + Kibana Docker 설치 가이드 (with 인증 활성화)

EC2 단일 서버 Blue/Green 무중단 배포 (Nginx + 포트 스위칭)