Post

CICD with Github Actions for project Deving

✅ CICD settings

  • nohup해서 실행했을 때 잘 실행되는 상태
  • .github안에 workflows라는 파일 만들고
  • 이 안에 cd.yml 파일 생성

Image

  • test controller도 하나 있으면 변경되는 내용 확인하기에 좋음
1
2
3
4
5
6
7
8
9
@Slf4j
@RestController
public class TestController {
    @GetMapping("/test")
    public String hello() {
        return "/CICD test 페이지입니다";
    }
}

🔴 /login redirect error

  • spring securitybuild파일에 넣고 배포를 했더니
  • /test로 보내도 로그인이 안 되어 있다면서 spring security/login으로 redirect해버림
  • 아직 로그인, 회원가입 등 구현하기 전 단계이므로
  • security config파일을 spring security를 사용하지 않도록 임시로 permitAll로 바꿔둔다.
  • ❗️ 나중에 로그인, 회원가입 등 인증 구현 후에는 원래대로 바꿔야!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .cors(cors -> cors
                        .configurationSource(CorsConfig.corsConfigurationSource()))
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers("/signup").permitAll()
                                .requestMatchers("/login").permitAll()
                                .requestMatchers("/swagger-ui/**").permitAll()
                                .requestMatchers("/**").permitAll()
                                .anyRequest().permitAll() //redirect막기 위해 임시로 permitAll
//                    .anyRequest().authenticated() // 인증 구현시, 주석 해제
                );
        return http.build();
    }
}

✅ CD 1

Image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
name: CD #actions workflow에 뜨는 이름

on:
  push: #push되었을 때
    branches: #어떤 브랜치에 push되었을 때
      - main # - develop

jobs:
  deploy:
    runs-on: ubuntu-latest # 내 github actions를 실행할 컴퓨터 OS

    steps:
      - name: connect EC2 through SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: $ #github actions secerts에 다음 3가지 저장
          username: $
          key: $ #EC2만들 때 다운받은 pem키
          script: |
            cd /home/ec2-user/Moim-BE
            git pull origin main #새로운 코드 pull해서 가져오고
            ./gradlew clean build -x test # build하기
            sudo fuser -k -n tcp 8080 || true #port 8080에 running하는 프로세스 지우고, 없다면 그냥 없는대로
            nohup java -jar build/libs/*SNAPSHOT.jar > ./output.log  2>&1 & #nohup으로 무중단 배포, log도 만들어라
  • github actions secerts에 사용되는 secrets 추가

Image

  • 💡 결론: main브랜치에 새로운 내용을 바꾸고, push하면 잘 반영된다.
  • test controllerreturn값에 간단히 내용을 추가하고 git add, commit, push하면 반영되는 것 확인

  • 👍🏻 git pull을 활용해서 변경된 부분의 프로젝트 코드에 대해서만 업데이트 하기 때문에 CI/CD 속도가 빠르다
  • 👍🏻 CI/CD 툴로 Github Actions만 사용하기 때문에 인프라 구조가 복잡하지 않고 간단
  • 👎🏻 빌드 작업을 EC2에서 직접 진행, 서버 부하가 크다
  • 👎🏻 Github 계정 정보가 해당 EC2에 저장된다

✅ CD 2

Image

  • 🆚 기존 CD 1과 차이점

    • CD 1는 바뀐 code에 대해서만 pull을 받아왔다면, CD 2는 통째로 파일을 지우고, 새로 올려서 배포하는 방식
    • checkout repository
    • install JDK: temurin이라는 JDK설치, 기업에서 많이 사용
    • change build file name
    • use SCP
  • running: 현재 실행중인 jar파일이 있는 폴더
  • willrun: 새롭게 내용이 반영되어, 앞으로 실행하고 싶은 jar파일이 있는 폴더
  • 새로운 내용 반영 후 실행하고 싶은 jar파일을 willrun에 저장
  • running은 지우고
  • 다시 폴더 running만들고
  • willrun에 있는 jar파일을 running으로 복사
  • 포트 8080에서 실행하고 있는 프로세스 죽이고
  • willrun에 있는 jar파일 nohup으로 실행
  • willrun폴더 삭제
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
name: CD

on:
  push:
    branches:
      - develop

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository #repository불러오기
        uses: actions/checkout@v2=4

      - name: Set up JDK 17  #java 17을 설치해라, 그 중에서도 temurin
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: test and build #build하고
        run: ./gradlew clean build -x test

      - name: change build file name #build file이름 바꾸기
        run: mv ./build/libs/*SNAPSHOT.jar ./project.jar #원래 *SNAPSHOT.jar 였는데 ./project.jar로 이름을 바꾼다

      - name: use SCP to send build file to EC2 #SCP라는걸 이용해서 만든 build file을 EC2로 보낸다
        uses: appleboy/scp-action@v0.1.7
        with:
          host: $
          username: $
          key: $
          source: project.jar
          target: /home/ec2-user/Moim-BE/willrun  #앞으로 실행될 파일을 build file을 willrun라는 폴더에 저장할 것

      - name: connect EC2 through SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: $
          username: $
          key: $ #EC2만들 때 다운받은 pem키
				  script: |
            rm -rf /home/ec2-user/Moim-BE/running  #rm -rf 현재 실행 중인 파일 담긴 폴더 지우기
            mkdir /home/ec2-user/Moim-BE/running #mkdir 현재 실행 중인 파일 담을 폴더 만들기 mkdir
            mv /home/ec2-user/Moim-BE/willrun/project.jar /home/ec2-user/Moim-BE/running/project.jar  #앞으로 실행될 파일에 있는 폴더에 가서 willrun/project.jar #현재 실행될 파일을 저장하는 폴더로 이동
            cd /home/ec2-user/Moim-BE/running   #cd 현재 실행되고 있는 파일 있는 폴더로 가서
            sudo fuser -k - tcp 8080 || true #현재 포트 8080에서 실행되는 파일 있으면 지우고, 없으면 없는대로 true
            nohup java -jar project.jar > ./output.log 2>&1 & #nohup 빌드 파일 실행
            rm -rf /home/ec2-user/Moim-BE/willrun #rm -rf 실행될 파일 저장하는 폴더는 삭제

  • 👍🏻 빌드 작업을 Github Actions에서 해줌, 서버에 영향을 주지 않는다
  • 👍🏻 CI/CD 툴로 Github Actions만 사용, 다른 스크립트가 필요없어 간단하다
  • 👎🏻 여러 EC2 인스턴스에 배포를 해야 하는 상황이라면, 직접 Github Actions에 스크립트를 작성해야 한다

👎🏻 다른 브랜치에서 push, pull request한 경우

  • 개발을 하다보면 develop브랜치에서 직접 작업을 하는 경우는 거의 없고
  • 특정 기능 개발을 위한 다른 브랜치에서 개발, 테스트 후 develop으로 merge한다
  • 따라서 다른 브랜치에서 push한 다음, develop으로 pull request를 하여 merge했을 때
  • CI가 자동으로 이루어지기 위해 CI 스크립트 작성

✅ CI

  • on: pull_request:라는 점이 CD와 다르다
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
name: CI

on:
  pull_request:
    branches:
      - develop

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582
        # GitHub Action that installs and caches Gradle to speed up builds.

      - name: Run tests with Gradle
        run: ./gradlew test

      - name: Build with Gradle
        run: ./gradlew build -x test
  • 👍🏻 이렇게 CI를 작성하면 다른 브랜치에서 머지했을 때 빌드가 자동화 됨

CD 2 🆚 CD 3

  • ✔️ CD 2: Renames JAR to project.jar
  • ✔️ CD 3: Uses versioned JAR (todo-0.0.1-SNAPSHOT.jar)
  • 👍🏻 JAR파일의 버전을 관리할 수 있다

  • ✔️ CD 2: Uses SCP (Secure Copy Protocol)
  • ✔️ CD 3: Uses SCP but with additional SSH key setup
  • 👍🏻 SSH 키로 접속한다는 점이 다르다

  • ✔️ CD 2: Directly moves and runs the JAR on EC2
  • ✔️ CD 3: Runs a separate deploy.sh script for better modularity
  • 👍🏻 변경사항 발생 시 modularity가 높으니 부분만 수정하면 된다

  • ✔️ CD 2: Minimal Logging (only redirects to output.log)
  • ✔️ CD 3: Maintains date-based logs for better debugging
  • 👍🏻 날짜별로 log를 유지하니 에러 발생 시 디버깅이 쉽다

  • ✔️ CD 2: Deletes /willrun directory after deployment
  • ✔️ CD 3: Keeps logs and maintains directory structure
  • 👍🏻 bug발생 시 directory확인 가능

✔️ Conclusion

  • 👍🏻 CD 2의 장점:
    • simple, fast, straightforward
    • no external script dependency, 따로 스크립트 작성할 필요 없음! only github actions
  • 👎🏻 CD 2의 단점:

    • No Environment Variable Handling
    • minimal logging
    • not modular
  • 👍🏻 CD 3의 장점:
    • more secure: SSH key based authentication
    • better loggin
    • modular, maintainable
    • handle environment variables properly
  • 👎🏻 CD 3의 단점:
    • more complex setup, requires external deploy.sh script
    • SSH setup required
    • slower, additional SSH setups required

✅ CD 3

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
name: CD

on:
  push:
    branches:
      - develop

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582

      - name: Add SSH host key #Adds SSH host key (to prevent host verification issues)
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -H $ >> ~/.ssh/known_hosts
          chmod 644 ~/.ssh/known_hosts

      - name: Set up SSH private key #Sets up SSH private key for authentication.
        run: |
          echo "$" > ~/.ssh/private_key.pem
          chmod 600 ~/.ssh/private_key.pem

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

      - name: Check JAR File
        run: ls -l build/libs/

      - name: Upload JAR to Remote Server
        run: |
          echo "$" > ~/.ssh/private_key.pem
          chmod 600 ~/.ssh/private_key.pem
          ssh -i ~/.ssh/private_key.pem $@$ "mkdir -p $"
          scp -i ~/.ssh/private_key.pem build/libs/moim-0.0.1-SNAPSHOT.jar $@$:$
        env:
          EC2_PRIVATE_KEY: $

      - name: Deploy to Remote Server # Runs the deploy.sh script remotely
        run: |
          ssh -i ~/.ssh/private_key.pem $@$ '
            export APP_DIR="$";
            export DB="$";
            export DB_PASSWORD="$";
            export DB_USERNAME="$";
            export HOST="$";
            bash -s
          ' < ./deploy.sh
        env:
          EC2_PRIVATE_KEY: $
          APP_DIR: $
          DB: $
          DB_PASSWORD: $
          DB_USERNAME: $
          HOST: $

❓ Where do I save the .pem key?

  • SSH연결을 위해 EC2서버에 .pem key를 저장해야 하는건 알겠는데,
  • .pem key는 너무 길어서 bashrc에 저장하는게 아닌 것 같았다
  • 그러다 검색하다 이걸 알게되었다
  • ⚠️ Important: If your private key is multi-line, you should store it as a file instead of an environment variable.
  • reference the key in a file

  • 그래서 추가된 내용이 다음과 같다
  • github secrets에서 EC2_PRIVATE_KEY를 가져와서
  • ~/.ssh/private_key.pem라는 파일을 만들고 복사해서 저장하기
1
2
3
4
- name: Set up SSH private key #Sets up SSH private key for authentication.
  run: |
    echo "$" > ~/.ssh/private_key.pem
    chmod 600 ~/.ssh/private_key.pem

✅ deploy.sh

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
34
35
36
37
#!/bin/bash

# 어플리케이션 세팅
APP_DIR="${APP_DIR}"
JAR_PATH="${APP_DIR}/moim-0.0.1-SNAPSHOT.jar"
LOG_DIR="${APP_DIR}/logs"
DEPLOY_DATE=$(date +'%Y-%m-%d')

# DB 세팅
DB="${DB}"
DB_PASSWORD="${DB_PASSWORD}"
DB_USERNAME="${DB_USERNAME}"
HOST="${HOST}"

mkdir -p "$LOG_DIR"

PID=$(lsof -t -i:8080)
if [ ! -z "$PID" ]; then
  echo "Killing the old application with PID: $PID"
  kill -9 $PID
fi

LOG_FILE="${LOG_DIR}/moim-${DEPLOY_DATE}.log"

echo "Starting new application..."
nohup java -jar "$JAR_PATH" > "$LOG_FILE" 2>&1 &
NEW_PID=$!

sleep 5

# 서버가 실행 중인지 확인
if ps -p $NEW_PID > /dev/null; then
  echo "Application started successfully!"
else
  echo "Server did not start. Please check the logs: $LOG_FILE"
fi

🔴 No such file or directory error

Image

Image

💊 환경변수에 추가

  • $
  • 💊 bashrc에도 추가
1
2
APP_DIR
/home/ec2-user/Moim-BE/build/libs

💊 파일을 먼저 생성하고, upload JAR하도록 코드 추가

  • ensure the directory $ exists before scp runs

  • 🔴 Before

1
2
3
4
5
- name: Upload JAR to Remote Server
  run: |
    echo "$" > ~/.ssh/private_key.pem
    chmod 600 ~/.ssh/private_key.pem
    scp -i ~/.ssh/private_key.pem build/libs/moim-0.0.1-SNAPSHOT.jar $@$:$
  • 🟢 After
1
2
3
4
5
6
- name: Upload JAR to Remote Server
  run: |
    echo "$" > ~/.ssh/private_key.pem
    chmod 600 ~/.ssh/private_key.pem
    ssh -i ~/.ssh/private_key.pem $@$ "mkdir -p $"
    scp -i ~/.ssh/private_key.pem build/libs/moim-0.0.1-SNAPSHOT.jar $@$:$
This post is licensed under CC BY 4.0 by the author.