본문 바로가기
dev/프로젝트

[프로젝트] CICD (Github Actions, EC2, Docker, Putty, SSH) 적용

by dev-everyday 2025. 1. 23.
반응형

1. EC2 인스턴스 생성하기

먼저 AWS EC2 콘솔에 접속해서 EC2 인스턴스를 생성하려고 한다.

 

왼쪽 인스턴스를 클릭하고 인스턴스 시작을 누른다.

Ubuntu와 t3a.small로 설정하였다. 인스턴스는 본인이 맞는 성능과 요금의 인스턴스 유형을 고르기 바란다.

나같은 경우는 t3a.small을 선택하였는데 나중에 실제 서비스 운영까지 목표로 하고 있기 때문에 약간의 과금이 발생할 수 있다.(호달달)

나는 우선 SSH 트래픽을 내 IP에서만 허용하였다.

Key Pair 설정은 EC2 인스턴스에 접속하기 위해 사용되는 암호화된 파일이다.

AWS는 보안 문제로 인해 인스턴스 접속 시 ID, PW 방식을 권장하지 않는다.

그래서 나도 하나 만들어주기로 했다. 이 값은 잃어버리면 인스턴스에 접속할 수 없어서 잘 저장해주자.

pem으로 설정하였는데 나는 윈도우라서 PuTTY를 사용해서 .pem을 .ppk로 변환하려고 한다.

PuTTY를 다운받았는데 PuTTY(일명 쁘띠)는 SSH(Secure Shell), Telnet, Rlogin, Serial 연결을 지원하는 터미널 에뮬레이터이다. Windows 환경에서 원격 서버(AWS EC2 등)로 접속할 때 자주 사용하고 SSH 프로토콜을 사용해서 원격 리눅스 서버에 접속할 수 있다.사실 윈도우면 그냥 .ppk로 생성하는 게 좋을 거 같다.

 

PuttyGen을 실행하고 Load를 눌러주자.

로드하고 가끔 Files of type 설정으로 .pem 파일이 안 보일 수 있는데 All Files로 변경해주면 잘 보인다.

선택 후 "Successfully imported foreign key" 메시지가 나오면 성공적으로 로드된 것이다.

이후에 Save private key 버튼을 클릭해서 .ppk 파일로 저장하자.

그러면 이제 PuTTY에서 .ppk 키를 사용해 SSH 접속이 가능하다.

나는 Key passphrase도 입력하였다. 개발 시에는 입력하지 않아도 좋다. 입력하면 접근마다 패스프레이즈를 입력해야하서 귀찮다.

 

정리하자면 EC2 콘솔 접속하는 방식은 PuTTY를 이용한 GUI 방식과 SSH 접속 두 가지이고 아래와 같다.

a) PuTTY

단점은 PuTTY Configuration 화면이 너무 작다.

Category에서 Connection - SSH - Auth - Credential을 클릭하고 Private key file for authentication을 Browse해서 .ppk 파일을 올려주고 아까 입력한 패스프레이즈를 입력하면 접속 가능하다.

b) SSH

어떻게 보면 더 간단하다. .pem이 있는 폴더로 가서 cmd 창을 켜고 ubuntu@아까 입력 받은 public ip 입력 시 바로 접속이 가능하다.

ssh -i <your-key.pem> ubuntu@<your-ec2-public-ip>

 

접속한 ubuntu 환경에 Docker를 설치 및 설정을 진행하자.

# 패키지 업데이트
sudo apt update && sudo apt upgrade -y

# Docker 설치
sudo apt install -y docker.io

# Docker 서비스 실행 및 자동 실행 설정
sudo systemctl start docker
sudo systemctl enable docker

# 현재 사용자(ubuntu)를 Docker 그룹에 추가
sudo usermod -aG docker $USER

그룹 변경 사항 적용을 하기 위해 로그아웃 하고 다시 SSH 로 접속하자.

2. health check API 만들기

기존에 작성하던 프로젝트를 EC2에 올리려고 하는데 Health Check API를 간단히 구현해보자.

healthcheck 패키지를 생성하고 HealthCheckController를 생성하였다. @RequestMapping을 /health로 설정하고 간단하게 @GetMapping으로 나의 IP를 띄워주게 하였다.

로컬에서 우선적으로 코드가 잘 돌아가나 확인하였다.

package com.fortune.app.healthcheck;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/health")
public class HealthCheckController {

    @GetMapping
    public String healthCheck(HttpServletRequest request) {
        String clientIp = request.getRemoteAddr();
        return "당신의 IP는: " + clientIp;
    }
}

IPv6 형식의 내 localhost 주소가 뜨는걸 확인할 수 있다.

3. Github Actions 작성하기

- Secrets에 추가

Github Actions를 작성하기 전에 GitHub repository settings에 Secrets and variable > Actions에서 아래 값을 추가해주자.

DOCKER_HUB_USERNAME: 도커 허브 프로필 이름

DOCKER_HUB_PASSWORD: Docker Hub에서 access token을 따로 생성하여 입력하였다(아래 참고)

EC2_SSH_KEY: .pem 키를 base64로 인코딩해야한다. 아래 명령어를 입력해서 .txt를 만들고 입력하면 된다. 그냥 .pem 내용을 넣어주자.

MYSQL_ROOT_PASSWORD: MYSQL root user 비밀번호 입력

[Convert]::ToBase64String((Get-Content "C:\Users\사용자이름\Documents\파일.pem" -Encoding Byte)) | Set-Content "C:\Users\사용자이름\Documents\파일_base64.txt"

 

- Variables에 추가

EC2_HOST: EC2 퍼블릭 IP

EC2_USERNAME: ubuntu

 

이 과정은 EC2 배포 자동화를 위해 AWS 서버 정보와 Docker Hub 정보와 SSH 키 등을 등록하는 과정으로 보통 Secrets에는 민감한 정보(비밀번호, SSH 등)를 저장하고 Variables에는 코드에서 사용될 환경 변수(EC2 서버 IP, 유저명)를 등록한다.

 

이제 설정은 끝났고 github actions에 직접 .yml 파일 작성을 통해서 빌드부터 배포까지 해보려고 한다.

위치와 파일명은 .github/deploy.yml로 생성하였다.

name: CI/CD Pipeline for Fortune App

on:
  push:
    tags:
      - "v*"  # tag rule (v1.0.0)
  workflow_dispatch:
  
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
      
      - name: Grant execute permission to Gradle Wrapper
        run: chmod +x ./gradlew 
        
      - name: Extract Version from Tag
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV

      - name: Build with Gradle (Skip Tests)
        run: ./gradlew build -x test
        
      - name: Check if JAR file exists
        run: ls -l build/libs/
        
      - name: Build Docker Image with Version
        run: docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/fortune-app:${{ env.VERSION }} .

      - name: Log in to Docker Hub
        run: echo ${{ secrets.DOCKER_HUB_PASSWORD }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin

      - name: Push Docker Image
        run: docker push ${{ secrets.DOCKER_HUB_USERNAME }}/fortune-app:${{ env.VERSION }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Copy SSH Key
        run: |
          echo "${{ secrets.EC2_SSH_KEY }}" > fortune-app-key.pem
          chmod 600 fortune-app-key.pem
          
      - name: Verify SSH Key
        run: ls -l fortune-app-key.pem
      
      - name: Debug SSH Connection
        run: ssh -v -o StrictHostKeyChecking=no -i fortune-app-key.pem ${{ vars.EC2_USERNAME }}@${{ vars.EC2_HOST }} "echo Connected"

      - name: Extract Version from Tag
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV

      - name: Debug Docker Run Command
        run: |
          echo "Running: docker run -d \
          -p 8080:8080 \
          --name fortune-container \
          --link mysql-container:mysql \
          --link redis-container:redis \
          -e DB_HOST=mysql-container \
          -e DB_PORT=3306 \
          -e REDIS_HOST=redis-container \
          -e REDIS_PORT=6379 \
          -v /home/ubuntu/config/application.properties:/app/config/application.properties \
          ${{ secrets.DOCKER_HUB_USERNAME }}/fortune-app:${{ env.VERSION }}"

      - name: Deploy to EC2
        run: |
          ssh -o StrictHostKeyChecking=no -i fortune-app-key.pem ${{ vars.EC2_USERNAME }}@${{ vars.EC2_HOST }} << 'EOF'
          docker pull mysql:8.0
          docker stop mysql-container || true
          docker rm mysql-container || true
          docker run -d \
            --name mysql-container \
            -e MYSQL_ROOT_PASSWORD=${{ secrets.MYSQL_ROOT_PASSWORD }} \
            -e MYSQL_DATABASE=fortune_db \
            -p 3306:3306 \
            mysql:8.0

          docker pull redis:latest
          docker stop redis-container || true
          docker rm redis-container || true
          docker run -d \
            --name redis-container \
            -p 6379:6379 \
            redis:latest

          docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/fortune-app:${{ env.VERSION }}
          docker stop fortune-container || true
          docker rm fortune-container || true
          docker stop fortune-container || true
          docker rm fortune-container || true
          docker run -d \
            -p 8080:8080 \
            --name fortune-container \
            --link mysql-container:mysql \
            --link redis-container:redis \
            -e SPRING_DATASOURCE_URL=jdbc:mysql://mysql-container:3306/fortune_db \
            -e SPRING_DATASOURCE_USERNAME=root \
            -e SPRING_DATASOURCE_PASSWORD=${{ secrets.MYSQL_ROOT_PASSWORD }} \
            -e SPRING_REDIS_HOST=redis-container \
            -e SPRING_REDIS_PORT=6379 \
            -v /home/ubuntu/config/application.properties:/app/config/application.properties \
            ${{ secrets.DOCKER_HUB_USERNAME }}/fortune-app:${{ env.VERSION }}
          EOF

Tag가 생성될 때 GitHub Actions가 실행되도록 설정 > Docker 이미지에 태그를 버전 값으로 설정하도록 하였다.

main에 코드 push할 일이 많기 때문에 tag로 따로 따서 ec2에 반영하도록 하려고 한다.

 

실행했는데 계속 에러가 발생하였는데 문제는 connected가 되지 않았고 .pem에는 문제가 없는 것을 확인하였다.

github actions runner의 IP가 EC2에 막혀서 connected가 허용되지 않은 것이었는데 0.0.0.0을 배포할 때만 잠깐 열고 나머지는 local에서 접속 가능하게 닫았다.

그러고 빌드에서 배포까지 무수히 많은 커밋들을 했는데 docker-compose 잘 작성해놓고 나는 왜 헤맨걸까..

삽질의 결과물 .. 눙물..

 

CICD 흐름을 다시 정리하면 아래와 같다.

1. Build (GitHub에서 실행)

- 코드 체크아웃

- Java 환경 설정

- Gradle 빌드 실행

- Docker 이미지 빌드

- Docker Hub 로그인 후 이미지 푸시

2. Deploy (EC2에서 실행)

- SSH 키를 설정하여 EC2에 접속

- Docker Hub에서 최신 이미지 다운로드

- 기존 컨테이너 중지 및 삭제

- 새로운 컨테이너 실행

 

그리고 EC2 비용을 아껴야하기 때문에 EC2 인스턴스를 평소엔 중지해두자..^^

물론 EBS 요금은 청구되지만..

4. TODO

- OAuth2 + JWT + Redis 로 수정하기

반응형

'dev > 프로젝트' 카테고리의 다른 글

[프로젝트] README 적용  (4) 2025.01.28
[프로젝트] Spring Security  (2) 2025.01.28
[프로젝트] CICD  (6) 2025.01.23
[프로젝트] Docker-Compose & Docker 적용  (7) 2025.01.21
[프로젝트] On-Premise & Cloud  (16) 2025.01.21