DevOps

GitHub Actions 와 CodeDeploy 를 이용한 CI/CD 구현

BaekSJ 2024. 5. 10. 23:11

💡 GitHub Actions 란 무엇인가?

  • GitHub 저장소 내에서 직접 워크 플로우를 자동화할 수 있도록 GitHub 에서 제공하는 자동화 플랫폼
  • 이를 통해 GitHub 를 떠나지 않고도 코드를 빌드, 테스트 및 배포할 수 있음
  • YAML 구문을 사용하여 push, pull 요청 또는 예약된 이벤트와 같은 이벤트를 기반으로 작업을 트리거하는 사용자 지정 워크 플로우를 정의할 수 있음

 

💡CodeDeploy 란 무엇인가?

  • Amazon EC2 인스턴스, AWS Lambda 함수, 온프레미스 서버 등 다양한 컴퓨팅 환경의 인스턴스에 애플리케이션을 배포하는 프로세를 자동화하는 배포 서비스

 

더보기

💡 CI/CD 란 무엇인가?

  • 지속적인 통합과 지속적인 전달/지속적인 배포를 나타냄
  • CI (지속적 통합)
    • 개발자가 코드 변경 사항을 공유 저장소에 자주 통합하는 개발 방식
    • 각 통합은 자동화된 빌드 및 테스트를 트리거하여 팀이 개발 프로세스 초기에 오류를 감지하고 해결할 수 있도록 함
  • CD (지속적 전달)
    • CI 의 확장으로, 자동화된 테스트를 통과한 코드 변경 사항이 자동으로 릴리스 준비됨
    • 이 프로세스에는 프로덕션에 배포하기 전에 추가 테스트 또는 승인 단계가 발생할 수 있는 사전 프로덕션 환경에 코드를 배포하는 작업이 포함됨
  • CD (지속적 배포)
    • 수동 개입 없이 모든 자동화된 테스트 및 검사를 통과한 후 코드 변경 사항을 프로덕션 환경에 자동으로 배포함으로써 개념을 발전
  • CI/CD 방식은 소프트웨어 개발 수명주기를 자동화하고 간소화하여 고품질 소프트웨어를 더 자주 안정적으로 제공할 수 있도록 하는 것을 목표로 함

 


 

아키텍쳐 구조

  • Local 에서 GitHub 레포지토리로 push 가 되면 GitHub Actions 가 트리거 되어 자동 실행
    • build 를 통해 .jar 생성
  • 생성된 .jar 파일과 script 파일을 압축해 AWS S3 로 업로드
  • Public Subnet 의 EC2 에 설치된 CodeDeploy Agent 가 AWS S3 에 업로드 된 압축 파일(.zip)을 받아와 배포 시작
    • appspec.yml 을 읽어 설정된 script 를 실행시켜 배포

 

1. IAM 역할 생성

  • EC2 에서 CodeDeploy 와 S3 에 접근할 수 있도록 IAM 권한 추가

  • 신뢰할 수 있는 엔터티 유현으로 AWS 서비스 선택
  • EC2 애서 연결해 줄 IAM 이므로 사용 사례로 EC2 선택

 

  • 권한 정책은 위 두 개를 부여

 

2. S3 버킷 생성

 

3. CodeDeploy 애플리케이션 생성

  • 애플리케이션 이름 설정
  • 컴퓨팅 플랫폼은 'EC2/온프레미스'로 설정

 

3-1. CodeDeploy IAM 역할 생성

 

3-2. 배포 그룹 생성

  • 위에서 생성한 애플리케이션의 배포 그룹 생성

 

  • 배포 그룹의 이름을 설정
  • 서비스 역할로 위에서 생성한 CodeDeploy IAM 을 선택

 

  • Amazon EC2 인스턴스 선택
  • 키는 Name 으로, 값은 연결하고자 하는 EC2 인스턴스를 선택

 

4. IAM 사용자 생성

  • GitHub Actions 를 통해 S3 버킷에 업로드하기 위해서는 권한이 필요합니다
  • AWS IAM → 사용자 → 사용자 추가

 

 

4-1. 엑세스 키 생성

  • GitHub Actions 에서 사용자를 인증하기 위한 엑세스 키 생성
  • 생성 후 하단의 '.csv 파일 다운로드' 를 클릭하여 안전하게 보관
    • 외부 노출 시 해킹의 위험 있음

 

5. EC2 인스턴스에 CodeDeploy Agent 설치

  • S3 버킷에 있는 파일을 CodeDeploy 를 통해 특정 EC2 에 배포하기 위해서 EC2 에 CodeDeploy Agent 설치

 

5-1. Ruby 패키지 설치

  • CodeDeploy Agent 가 Ruby 로 작성되어 있기에 Ruby 패키지 설치
$: sudo apt update
$: sudo apt install ruby-full
$: sudo apt install wget

 

5-2. CodeDeploy Agent 설치

  • Ubuntu 의 경우 /home/ubuntu 위치에서 다운로드 (EC2 인스턴스에 접속 시 초기 위치 '~$' 로 되어 있음)
# S3 버킷에서 설치 스크립트 다운로드
$: wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install

# 스크립트를 실행 가능하도록 권한 변경
$: chmod +x ./install

# 설치 스크립트 실행
$: sudo ./install auto

 

5-3. CodeDeploy 실행 확인

$: sudo service codedeploy-agent status

  • 위와 같이 active 라면 실행 성공

 

5-4. EC2 인스턴스 실행 시 자동 스크립트 실행하도록 설정

$: sudo vim /etc/init.d/codedeploy-startup.sh

 

sudo service codedeploy-agent restart
  • 스크립트 내용

 

$: sudo chmod +x /etc/init.d/codedeploy-startup.sh
  • 권한 부여

 

6. NGINX 설치

 

6-1. EC2 에 nginx 설치

# yum 을 사용한다면 sudo yum install nginx
sudo apt install nginx

 

6-2. nginx 버전 확인

sudo nginx -v

 

6-3. nginx 실행

sudo service nginx start

 

6-4. nginx 상태 확인

sudo service nginx status

  • 위 처럼 나온다면 실행 성공

 

6-5. nginx 접속해보기

  • EC2 의 인바운드 규칙으로 80번 포트를 허용하고 접속한다면 위처럼 페이지로 이동하여 확인 가능합니다.

 

6-6. $server_url 변수 설정

/home/ubuntu# sudo vi service_url.inc

 

set $service_url "http://[탄력적 IP]:서버포트 번호";
  • 'home/ubuntu'service_url.inc 작성

 

6-7. nginx 에 Symbolic link 걸기

  • `/etc/nginx` 폴더 내에 'sites-available'' sites-enabled' 폴더가 있는 지 확인 (없다면 mkdir 을 통해 생성해 줍니다)

 

# default 폴더 확인
cd /etc/nginx/sites-available
  • 'sites-available' 폴더 안에 default 가 있다면 지워주고 다시 생성해줍니다

 

# default 작성
sudo vi default
  • default 를 새로 작성해줍니다

 

server {
    listen          80;
    listen          [::]:80;
    server_name     [탄력적 IP];
    root            /usr/share/nginx/html;

    include /home/ubuntu/service_url.inc;
    
    location / {
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass $service_url;  # $service_url은 /home/ubuntu/service_url.inc 파일에 정의되어야 합니다.
    }

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;

    error_page 404 /404.html;
            location = /404.html {
            }

    error_page 500 502 503 504 /50x.html;
    		location = /50x.html {
            }
}
  • default 내용
    • include : 설정 파일 불러오기
    • proxy_pass : $service_url 로 요청을 보낼 수 있도록 하는 프록시 설정

 

sudo ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
  • 'Symbolic link' 만들기
  • 오류 메시지 "ln: failed to create symbolic link '/etc/nginx/sites-enabled/default': File exists가 발생했다면 이미 심볼릭 링크가 걸려 있다는 의미입니다.

 

  • 위와 같이 해당 디렉토리에서 'ls' 명령어를 실행하면 'default' 파일이 하늘색으로 표기되는 걸 확인 가능한데 이는 심볼릭 링크(symlink)를 나타냅니다

  • 'ls -l' 명령어를 통해 해당 'default' 파일이 어느 파일이나 디렉토리와 연결되어 있는지 확인이 가능합니다.

 

sudo nginx -t
  • Nginx 가 정상 동작하는지 테스트

 

  • 위 처럼 나오면 성공입니다

 

sudo service nginx restart

 

  • 위 작업이 끝났다면 Nginx 를 재시작 해주시면 되겠습니다

 

7. Docker , Docker-Compose 설치 및 설정  🐳

 

7-1. Docker , Docker-Compose 설치

# Docker 설치
sudo apt install docker.io

# Docker 실행
sudo systemctl start docker

# Docker 컨테이너 확인
sudo docker ps

# Docker-Compose 설치
sudo curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

# 접근 권한 설정
sudo chmod +x /usr/local/bin/docker-compose

# Docker-Compose 버전 확인
docker-compose version

 

8. 자바 파일을 실행하기 위한 JDK 설치

sudo apt install openjdk-11-jdk -y
  • 본 포스팅에서는 JAVA 11 버전을 사용하여 11 버전을 설치했지만 본인이 사용하는 버전에 맞게 설치해주시면 되겠습니다.

 

java --version
  • 잘 설치되었는지 버전 확인을 해줍니다.

 

9. appspec.yml 생성

  • 배포하고자 하는 프로젝트의 root 경로에 appspec.yml 작성
  • CodeDeploy 가 EC2 에 소스코드를 배포하는 과정을 정의
version: 0.0
os: linux
files:
  - source: /
    destination: /home/app/current
file_exists_behavior: OVERWRITE

permissions:
  - object: /home/app
    pattern: '**'
    owner: ubuntu
    group: ubuntu

hooks:
  BeforeInstall:
    - location: docker-stop.sh
      timeout: 300
      runas: ubuntu

  AfterInstall:
    - location: deploy-be.sh
      timeout: 300
      runas: ubuntu

 

10. GitHub Repository 설정

 

10-1. GitHub Repository 에 엑세스 키 저장

  • GtiHub Actions 를 통해 사용자를 인증하기 위해 'New repository secret' 을 클릭하여 키를 저장

 

  • 위와 같이 생성

 

  • 배포하고자 하는 레포지토리 클릭
  • 상단의 Actions 클릭
  • 'set up a workflow yourself' 를 통해 진행(직접 작성)
  • 'Deploy to Amazon ECS' 부분의 configure 를 클릭하여 수정(만들어진 스크립트 수정)

 

name: Deploy to AWS EC2

on:
  push:
    branches: [main]

jobs:
   build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.15.0]
    steps:
      - name: ✅ Checkout branch
        uses: actions/checkout@v3

      - name: 📀 Install Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: ⛏ Set up JDK 11
        uses: actions/setup-java@v3
        with:
          distribution: "temurin"
          java-version: "11"
        
   deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: deploy
    steps:
      - name: ✅ Checkout branch
        uses: actions/checkout@v3
    
      - name: 🗂️ Make config folder
        run: mkdir -p config
    
      - name: 📦 Zip project files
        run: zip -r ./$GITHUB_SHA.zip . 
    
      - name: 🔧 Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}
      
      - name: 🐳 Login to Amazon ECR
        run: |
          aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 654654614721.dkr.ecr.ap-northeast-2.amazonaws.com/test-vpc
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
                        
      - name: 🔨 Build and Push to ECR
        uses: docker/build-push-action@v5
        with:
          context: .
          file: .github/workflows/Dockerfile
          push: true
          tags: ${{ secrets.AWS_ECR_REPOSITORY }}:latest
    
      - name: 🚛 Upload to S3
        run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://${{ secrets.S3_BUCKET_NAME }}/${{ env.S3_BUCKET_DIR_NAME }}/$GITHUB_SHA.zip 
              
      - name: 🚀 Deploy to EC2 with CodeDeploy
        run: |
          aws deploy create-deployment \
            --application-name ${{ secrets.APPLICATION_NAME }} \
            --deployment-group-name ${{ secrets.DEPLOYMENT_GROUP_NAME }} \
            --s3-location bucket=${{ secrets.S3_BUCKET_NAME }},key=${{ env.GITHUB_SHA }}.zip,bundleType=zip

 

10-2. Dockerfile 생성

FROM gradle:7.4-jdk11-alpine as builder

ARG DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Seoul
RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/$TZ /etc/localtime && \
    echo $TZ > /etc/timezone

WORKDIR /build
COPY . /build
RUN gradle build

FROM openjdk:11.0-slim
WORKDIR /recode-be
COPY --from=builder /build/build/libs/*.jar .

EXPOSE 8080
CMD ["java", "-jar", "recode-0.0.1-SNAPSHOT.jar", "--spring.config.location=file:/test-vpc/resources/"]

 

11. GitHub Repository Environment 설정

 

11-1. 프로젝트 properties 템플릿 생성

server.port= 10004
server.servlet.encoding.charset=utf-8
server.servlet.encoding.force=true

spring.datasource.url=jdbc:mariadb://${DB_ENDPOINT}
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}

spring.jpa.open-in-view=false
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.default_batch_fetch_size=100
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
spring.output.ansi.enabled=always

# Google
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=profile,email
  • 프로젝트의 '/src/main/resources/''application.properties.template' 파일을 생성해줍니다
    • 해당 파일은 CI/CD 도중 삽입됩니다

 

11-2. GitHub 레포지토리에 Secrets 등록하기

  • 'application.properties' 혹은 'application.yml' 에 작성한 노출되면 안되는 민감한 정보는 환경변수로 등록하여 사용하여야 합니다.
  • 필요하신 내용을 모두 넣은 후 저장해주시면 되겠습니다.

 

11-3. Github Actions yaml 파일에 properties 정보 넣어주기

name: Deploy to AWS EC2

on:
  push:
    branches: [main]

jobs:
   build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.15.0]
    steps:
      - name: ✅ Checkout branch
        uses: actions/checkout@v3

      - name: 📀 Install Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: ⛏ Set up JDK 11
        uses: actions/setup-java@v3
        with:
          distribution: "temurin"
          java-version: "11"
	  
      # 추가된 부분
      - name: 🔑 Replace environment variables in application.properties
        run: |
          cp src/main/resources/application.properties.template src/main/resources/application.properties
          sed -i 's/\${DB_USERNAME}/'"${{ secrets.DB_USERNAME }}"'/g' src/main/resources/application.properties
          sed -i 's/\${DB_PASSWORD}/'"${{ secrets.DB_PASSWORD }}"'/g' src/main/resources/application.properties
          sed -i 's/\${GOOGLE_CLIENT_ID}/'"${{ secrets.GOOGLE_CLIENT_ID }}"'/g' src/main/resources/application.properties
          sed -i 's/\${GOOGLE_CLIENT_SECRET}/'"${{ secrets.GOOGLE_CLIENT_SECRET }}"'/g' src/main/resources/application.properties 
        
   deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: deploy
    steps:
      - name: ✅ Checkout branch
        uses: actions/checkout@v3
    
      - name: 🗂️ Make config folder
        run: mkdir -p config
    
      - name: 📦 Zip project files
        run: zip -r ./$GITHUB_SHA.zip . 
    
      - name: 🔧 Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}
      
      - name: 🐳 Login to Amazon ECR
        run: |
          aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 654654614721.dkr.ecr.ap-northeast-2.amazonaws.com/test-vpc
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
                        
      - name: 🔨 Build and Push to ECR
        uses: docker/build-push-action@v5
        with:
          context: .
          file: .github/workflows/Dockerfile
          push: true
          tags: ${{ secrets.AWS_ECR_REPOSITORY }}:latest
    
      - name: 🚛 Upload to S3
        run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://${{ secrets.S3_BUCKET_NAME }}/${{ env.S3_BUCKET_DIR_NAME }}/$GITHUB_SHA.zip 
              
      - name: 🚀 Deploy to EC2 with CodeDeploy
        run: |
          aws deploy create-deployment \
            --application-name ${{ secrets.APPLICATION_NAME }} \
            --deployment-group-name ${{ secrets.DEPLOYMENT_GROUP_NAME }} \
            --s3-location bucket=${{ secrets.S3_BUCKET_NAME }},key=${{ env.GITHUB_SHA }}.zip,bundleType=zip

 

결과 확인

 

1. S3

 

2. ECR

 

위와 같이 성공적으로 업로드가 완료 되었음을 확인할 수 있습니다.

 

마치며

CI/CD 작업을 진행하며 CLI 환경과 좀 더 익숙해질 수 있었네요. EIP 의 경우 프리티어라도 생성 후 사용하지 않으면 비용이 발생하니 혹여 추가로 생성된 것이 있다면 release 를 통해 삭제해주시기 바랍니다.