CI도구로 Github Actions와 CD도구로 AWS CodeDeploy를 사용해서 CI/CD 환경을 구축할 것이다.
프론트엔드는 React이고 백엔드는 Spring Boot로 구현한 애플리케이션이다.

스테이징서버와 운영서버가 두개의 EC2 인스턴스로 구축되어 있다.
스테이징서버 : develop / 운영서버 : master 브랜치를 각각 따르고 있다.

스테이징서버는 프론트엔드와 백엔드 모두 EC2 인스턴스에 배포할 것이며
운영서버는 프론트엔드는 S3버킷에 백엔드는 EC2 인스턴스에 각각 배포할 것이다.

배포 과정

image

AWS IAM

  • USER : 외부 서비스에 할당 가능 (key값 제공)
  • ROLE : AWS 서비스만 할당 가능

1. AWS CLI용 USER

  • 배포시 EC2 인스턴스의 AWS CLI에서 사용할 권한 image

2. EC2 배포용 ROLE

  • 배포시 EC2가 사용하는 권한
  • AWS서비스-EC2 -> S3FullAccess, CodeDeployFullAccess -> 태그설정 image image

  • 스테이징서버, 운영서버 EC2 인스턴스 2개 모두에 생성한 권한 부여
    • EC2 인스턴스 -> 보안 -> IAM 역할 수정 image

3. CodeDeploy용 ROLE

  • 배포시 CodeDeploy가 사용하는 권한
  • AWS서비스-CodeDeploy -> CodeDeployFullAccess -> 태그설정 image image

백엔드 (스프링부트)

  • 스테이징서버 : EC2
  • 운영서버 : EC2

1. 테스트 application.yml 분리

  • application.yml에 DB접속정보가 포함되어 있을 것이다. 하지만 노출되어서는 안되는 정보이기 때문에 .gitignore에 관리되어 깃허브에 업로드 되지 않는다.
  • 접속정보가 필요하지 않은(노출되어도 무방한) 내장 인메모리 디비를 사용하여 테스트를 진행해야 한다.
  • 내장 인메모리 디비는 테스트 진행시 올라왔다가 테스트가 종료되면 내려가 흔적이 남지 않게 된다.
  • test/resources/application.yml을 사용해 내장 인메모리 디비로 테스트가 진행되도록 할 수 있다.
    • 혹은, 접속정보가 아예 존재하지 않는다면 스프링은 자동으로 내장 인메모리 디비에 연결한다.
    • 간혹 스프링이 테스트 config를 찾지 못한다면 test/resources/config/application.yml로 이동시켜보자.
  • 디비 접속정보 이외의 비밀키같이 정보를 스프링이 빈 생성에 참조할 때 존재하지 않으면 에러가 발생한다.
    • 임시값을 대체해 넣어주면 된다.
spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test
    username: sa
    password:
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    defer-datasource-initialization: true

app:
  auth:
    token:
      secret-key: tmp
      refresh-cookie-key: tmp
  oauth2:
    authorizedRedirectUris: tmp

defer-datasource-initialization : hibernate 구동시 data.sql이 실행되도록 설정

2. 외부 yml 적용

  • application.yml이 깃허브에 업로드 되지 않기 때문에 미리 서버에 만들어 놓고 스프링이 구동할 때 참조하도록 설정해야 한다

  • Application.class

    • classpath:로 시작하는 파일 경로는 프로젝트 내부의 src/main/resources/이하 경로이다
    • 프로젝트 외부의 application.yml을 추가로 설정한다
    • 개발서버에서는 프로젝트 내부의 설정파일을 운영서버에서는 외부의 설정파일을 참조하도록 했기 때문에 optional을 사용했다
@SpringBootApplication
public class Application {

	public static final String APPLICATION_LOCATIONS = "spring.config.location="
			+ "optional:classpath:application.yml,"
			+ "optional:/usr/local/myapp/application.yml";

	public static void main(String[] args) {
		new SpringApplicationBuilder(Application.class)
				.properties(APPLICATION_LOCATIONS)
				.run(args);
	}
}

3. CodeDeploy 생성

  • CD 과정을 맡을 CodeDeploy 애플리케이션, 배포그룹을 생성한다
  • 여기서 생성된 배포그룹은 생성시 태그로 선택된 EC2에서 설치된 CodeDeploy 에이전트에 의해 실행된다

(1) 애플리케이션

image

(2) 배포그룹

  • 스테이징서버에 적용할 그룹myapp-staging과 운영서버에 적용할 그룹myapp-prodution 2개 생성
  • 배포그룹이름 -> 서비스역할 : CodeDeploy용 ROLE 선택 -> 배포유형 : 현재위치 -> EC2인스턴스 -> EC2 태그추가(Name) -> AllAtOnce -> 로드밸런스 활성화 해제

image image

4. 서버에 AWS CLI 및 CodeDeploy 에이전트 설치

$ sudo yum -y update
$ sudo yum install -y aws-cli
$ sudo aws configure
AWS Access Key ID [None]:
AWS Secret Access Key [None]:
Default region name [None]: ap-northeast-2
Default output format [None]: json
$ aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2
$ chmod +x ./install
$ sudo yum -y install ruby
$ sudo ./install auto
$ sudo service codedeploy-agent status
The AWS CodeDeploy agent is running as PID xxx

5. Github Actions (CI)

(0) Secrets 적용

  • 노출되어서는 안되는 비밀키값은 Github에 Secrets로 등록하면 yml실행시 참조할 수 있다
  • Github -> Repository내의 Settings -> Secrets -> Actions -> New Repository Secrets image image

  • 그리고 Github Actions가 사용할 yml 파일을 작성한다
    • name : 해당 Action(Workflow)의 이름
    • on : 아래 jobs를 실행시킬 조건(이벤트 or 트리거)
    • jobs : 커맨드가 모인 집합
    • steps : 각 각의 실핼될 커맨드단계

(1) deploy-staging.yml

  • develop 브랜치에 push/merge가 이루어지면 스테이징서버에서 CI가 동작한다
name: deploy-staging

on:
  push:
    branches: [ develop ]

jobs:
  deploy:      # job 이름
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout Github-Actions     # Github Actions 사용을 위한 체크아웃
        uses: actions/checkout@v2

      - name: Install Java 11
        uses: actions/setup-java@v1
        with:
          java-version: '11'
      
      - name: Permission for Gradle 
        run: chmod +x ./gradlew

      - name: Start Build with Gradle
        run: ./gradlew clean build

      - name: Setting for AWS          # AWS 권한셋팅
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: $
          aws-secret-access-key: $
          aws-region: ap-northeast-2
      
      - name: Upload to S3             # 빌드파일 S3 업로드
        run: aws deploy push --application-name myapp --description "myapp Test" --s3-location s3://myapp/deploy/myapp-staging.zip --source .

      - name: Start Deploy with CodeDeploy      # CodeDeploy 실행
        run: aws deploy create-deployment --application-name myapp --deployment-config-name CodeDeployDefault.OneAtATime --deployment-group-name myapp-staging --s3-location bucket=myapp,bundleType=zip,key=deploy/myapp-staging.zip
  • Upload to S3
    • --application-name : CodeDeploy 애플리케이션 이름
    • --s3-lication : 빌드파일을 업로드할 S3 위치
    • --source . : 현재 폴더를 S3에 업로드
  • Start Deploy with CodeDeploy
    • --application-name : CodeDeploy 애플리케이션 이름
    • --application-group-name : 애플리케이션 내의 배포그룹 이름
    • --s3-location : 가져올 배포파일 위치 및 정보

(2) deploy-production.yml

  • master 브랜치에 push/merge가 이루어지면 운영서버에서 CI가 동작한다
name: deploy-production

on:
  push:
    branches: [ master ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout Github-Actions     # Github Actions 사용을 위한 체크아웃
        uses: actions/checkout@v2

      - name: Install Java 11
        uses: actions/setup-java@v1
        with:
          java-version: '11'
      
      - name: Permission for Gradle 
        run: chmod +x ./gradlew

      - name: Start Build with Gradle
        run: ./gradlew clean build

      - name: Setting for AWS         # AWS 권한셋팅
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: $
          aws-secret-access-key: $
          aws-region: ap-northeast-2
      
      - name: Upload to S3             # 빌드파일 S3 업로드
        run: aws deploy push --application-name myapp --description "myapp" --s3-location s3://myapp/deploy/myapp-production.zip --source .

      - name: Start Deploy with CodeDeploy      # CodeDeploy 실행
        run: aws deploy create-deployment --application-name myapp --deployment-config-name CodeDeployDefault.OneAtATime --deployment-group-name myapp-production --s3-location bucket=myapp,bundleType=zip,key=deploy/myapp-production.zip

6. CodeDeploy (CD)

(1) appspec.yml

  • CodeDeploy는 S3에 업로드된 빌드파일을 EC2 인스턴스로 가져온 후 appspec.yml을 실행한다
  • appspec.ymldeploy.sh를 훅으로 실행한다
version: 0.0
os: linux

files:
  - source: /
    destination: /usr/local/myapp
    overwrite: yes
permissions:
  - object: /usr/local/myapp
    owner: ec2-user
    group: ec2-user
    mode: 755
hooks:
  AfterInstall:
    - location: deploy.sh
      timeout: 60
      runas: ec2-user

(2) deploy.sh

  • appspec.yml에 의해 실행되는 배포 스크립트
  • 실행중이던 이전 스프링 프로세스를 중단시키고 새로운 프로세스를 실행시킨다
#!/usr/bin/env bash

REPOSITORY=/usr/local/myapp
cd $REPOSITORY

APP_NAME=myapp
JAR_NAME=$(ls $REPOSITORY/build/libs/ | grep '.jar' | tail -n 1)
JAR_PATH=$REPOSITORY/build/libs/$JAR_NAME

CURRENT_PID=$(pgrep -f $APP_NAME)

echo "> Kill Previous Process" >> /usr/local/myapp/deploy.log

if [ -z "$CURRENT_PID" ]
then
  echo "> No Process to Kill" >> /usr/local/myapp/deploy.log
else
  echo "> Kill $CURRENT_PID" >> /usr/local/myapp/deploy.log
  kill -15 "$CURRENT_PID" >> /usr/local/myapp/deploy.log 2>&1
  sleep 5
fi

echo "> $JAR_PATH Deploy"
nohup java -jar $JAR_PATH >> /usr/local/myapp/nohup.log 2>&1 &

프론트엔드 (리액트)

  • 스테이징서버 : EC2 (백엔드와 동일 서버)
  • 운영서버 : S3

1. CodeDeploy 생성

(1) 애플리케이션

image

(2) 배포그룹

  • 프론트엔드의 경우 운영서버는 S3 정적호스팅을 사용하기 때문에 EC2를 사용하는 스테이징서버를 위한 배포그룹만 생성하면 된다.
  • 배포그룹이름 -> 서비스역할 : CodeDeploy용 ROLE 선택 -> 배포유형 : 현재위치 -> EC2인스턴스 -> EC2 태그추가(Name) -> AllAtOnce -> 로드밸런스 활성화 해제 image image

2. Github Actions (CI)

(1) deploy-staging.yml

  • develop브랜치에 push/merge가 이루어지면 스테이징서버에서 CI가 동작한다
name: deploy-staging

on:
  push:
    branches: [ develop ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash

    strategy:
      matrix:
        node-version: [16.14.x]

    steps:
      - name: Checkout Github-Actions     # Github Actions 사용을 위한 체크아웃
        uses: actions/checkout@v2

      - name: Install node.js 16
        uses: actions/setup-node@v1
        with:
          node-version: $

      - name: Install Dependencies
        run: npm install

      - name: Start Build with npm
        run: npm run bhild
        env:
          CI: false

      - name: Setting for AWS         # AWS 권한셋팅
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: $
          aws-secret-access-key: $
          aws-region: ap-northeast-2

      - name: Upload to S3             # 빌드파일 S3 업로드
        run: aws deploy push --application-name myappF --description "myappF Test" --s3-location s3://myapp/deploy/myappF-staging.zip --source .

      - name: Start Deploy with CodeDeploy      # CodeDeploy 실행
        run: aws deploy create-deployment --application-name myappF --deployment-config-name CodeDeployDefault.OneAtATime --deployment-group-name myappF-staging --s3-location bucket=myapp,bundleType=zip,key=deploy/myappF-staging.zip

(2) deploy-production.yml

  • master 브랜치에 push/merge가 이루어지면 운영서버에서 CI가 동작한다
  • 리액트 프로젝트의 build폴더를 S3 버킷에 업로드시켜 배포한다
name: deploy-production

on:
  push:
    branches: [ master ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
  
    strategy:
      matrix:
        node-version: [16.14.x]

    steps:
      - name: Checkout Github-Actions     # Github Actions 사용을 위한 체크아웃
        uses: actions/checkout@v2

      - name: Install node.js 16
        uses: actions/setup-node@v1
        with:
          node-version: $
      
      - name: Install Dependencies
        run: npm install

      - name: Start Build with npm
        run: npm run build
        env:
          CI: false

      - name: Setting for AWS         # AWS 권한셋팅
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: $
          aws-secret-access-key: $
          aws-region: ap-northeast-2
      
      - name: Upload to S3 for Deploy             # 빌드파일 S3 업로드
        run: aws s3 cp --recursive --region ap-northeast-2 build s3://myapp.co.kr
  • Upload to S3 for Deploy
    • build s3://myapp.co.kr : build 폴더를 s3버킷으로 복사
    • --recursive : 선택폴더 이하 모든 폴더/파일

3. CodeDeploy (CD)

(1) appspec.yml

version: 0.0
os: linux

files:
  - source: /
    destination: /usr/local/myappF
    overwrite: yes
permissions:
  - object: /usr/local/myappF
    owner: ec2-user
    group: ec2-user
    mode: 755
hooks:
  AfterInstall:
    - location: deploy.sh
      timeout: 60
      runas: ec2-user

(2) deploy.sh

  • build폴더 복사 후 별도의 커맨드가 필요 없어 배포 날짜만 기록했다
#!/usr/bin/env bash

echo "> [$(date +%y-%m-%d/%H:%M)] Deploy React" >> /usr/local/debrains/deploy.log