ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Github Actions Self-hosted-runner 사용 방법
    클라우드 2025. 7. 12. 11:02

    도입한 이유

    먼저 취업한 친구에게 보안에 민감한 기업 같은 경우에는 Github-hosted-runner는 Github 클라우드 환경에서 실행되므로, 민감한 코드나 비공개 환경 정보가 외부로 유출될 가능성이 있어 Self-hosted-runner를 사용한다고 이야기를 들었습니다.

     

    그래서 회사에서 사용할 기술을 미리 배워본다는 차원에서 Self-hosted-runner를 도입하여 CI/CD 파이프라인을 구축하려 했습니다.

    도입을 하기 전에 이것이 어떤 원리로 동작 되는지 파악하였습니다.

    우선 Self-hosted-runner는 사용자가 직접 관리하는 VM 또는 물리 서버에 설치하여 사용하는 실행 환경입니다.

    • Github Actions 파이프라인에 Job(작업)을 생성하고 대기시켜 놓습니다.
    • runner가 Github에 polling 방식으로 job을 가져가서 실행합니다.

    이렇게 동작되는 방식의 장점이 하나 더 있었는데, Github-hosted runner를 사용할 때는 온프레미스의 private subnet에 있는 서버에 배포하기 위해서는 Bastion을 통하거나 터널링 방식으로 외부에서 접근되는 포인트를 만들어서 배포했어야 했는데, Self-hosted-runner를 사용할 경우 polling 방식으로 Github 서버에 접근해서 Job을 가져오기 때문에 외부에서 접근되는 포인트를 만들 필요가 없어 보안적으로 안전하다는 장점이 있었습니다.

    도입 방법

    우선 레포지토리의 Admin 권한이 있어야 합니다.

    그 후 Settings - Actions - Runners에 들어가면

    Github Runner 목록 화면

    다음과 같은 화면이 나오는데 New Self-hosted-Runner를 클릭합니다.

    그리고 명령어들이 나올텐데 이 명령어를 Self-hosted-Runner를 동작하게 할 VM에서 실행시켜 러너를 실행합니다.

    # 1. 러너 설치
    mkdir actions-runner && cd actions-runner
    curl -o actions-runner-linux-x64-2.326.0.tar.gz -L <https://github.com/actions/runner/releases/download/v2.326.0/actions-runner-linux-x64-2.326.0.tar.gz>
    tar xzf ./actions-runner-linux-x64-2.326.0.tar.gz
    
    # 2. 등록
    ./config.sh \\
      --url <https://github.com/{organization}/{repo}> \\
      --token <등록용 토큰> \\
      --name shared-self-hosted-runner
      --labels self-hosted,linux,X64,shared \\
      --unattended
    
    # 3. 서비스로 등록하고 실행
    ./svc.sh install
    ./svc.sh start

     

    러너 생성시 OS와 아키텍처 선택 화면

     

    각 명령어 설명

    1. 러너 설치
      • 러너를 설치할 때 러너가 돌아갈 운영체제에 맞게 선택하고 설치해줍니다. 저는 linux_x64로 설치하였습니다.
    2. 레포지토리 등록
      • 그리고 등록해주는데 --url 옵션은 Organization과 repo 이름 입력하면 됩니다. 자동으로 나올거긴 합니다.
      • --token 옵션은 러너를 Github 레포지토리에 등록하기 위한 단계이고, runner가 Github Action과 통신을 하기 위한 값입니다. 이것도 자동으로 나올 겁니다.
      • --name 옵션은 러너의 이름을 지정합니다. 아까 Runner 목록 사진에 이름 적혀있던 거 참고하면 됩니다.
      • --label 옵션은 나중에 Github Actions workflow의 runs-on에서 어떤 러너를 선택할지 결정하는데 그 때 사용하기 위해 설정합니다.
      • 그리고 --unattended 는 대화형 입력을 생략하는 옵션인데 이건 직접 설치하기 때문에 굳이 안 붙여도 됩니다.
    3. 러너를 서비스로 등록하고 실행
      • 이는 러너를 Linux 시스템의 서비스(systemd 서비스)로 등록하고 실행하기 위한 명령어인데요. 서버 재시작 시 자동으로 러너가 다시 실행되고 백그라운드로 동작하기 위해서 사용합니다.

     

     

    이렇게 하면 러너를 등록하고 설치하였고 polling 방식으로 Github에 job이 있는지 확인합니다.

    예를 들면

    runner → Github : “작업 있어요?”
    Github → Runner : “ㄴㄴ”
    (잠시 대기)
    runner → Github : “작업 있어요?”
    Github → runner : “있어 이거 실행 ㄱㄱ”
    

    이런 방식으로 동작합니다.

    폴링하는 주기는 비공개 됐으나 보통 2~5초라고 판단하고 backoff 전략을 사용한다고 합니다.

    backoff 전략은 처음에는 빠르게 시도하다가 실패가 계속되면 점점 재시도 간격을 늘리는 방식입니다.

     

    이제 러너가 잘 돌아가는지 확인하기 위해 파이프라인을 돌려봅니다.

    저는 CD 파이프라인을 .github/workflow/be-dev-cd.yml 에 설정해두었습니다.

    현재 아래와 같은 파이프라인인데 jobs 아래의 runs-on 에서 러너 만들때 지정했던 태그를 넣어주면 해당 태그를 가진 러너에 job을 할당합니다.

    해당 태그를 가진 러너가 여러개면 그 중 하나를 선택해서 job을 실행시키는데요

    처음에 이거 모르고 태그가 겹치는 러너가 3개였는데 자꾸 파이프라인에서 SSH Connection Error가 나와서 네트워크 설정과 키 설정을 다 해주었는데 간헐적으로 오류가 자꾸 발생했고, 그 이유를 몰랐었는데 관련이 없는 러너에서 job이 실행되어 문제가 생긴 것이였습니다.

    name: Backend Dev Docker CD
    
    on:
      push:
        branches: [ dev ]
        paths-ignore:
          - '**/README.md'
          - '**/.gitignore'
    
    jobs:
      build-and-deploy:
        runs-on: [ self-hosted, linux, X64, shared ]
    
        steps:
          - uses: actions/checkout@v3
          
          - name: Set up JDK 17
            uses: actions/setup-java@v3
            with:
              java-version: '17'
              distribution: 'temurin'
              
           # CI 과정에서 테스트를 했기 때문에 테스트 제외
          - name: Build JAR
            run: |
              ./gradlew clean build -x test
              cp build/libs/newsum-0.0.1-SNAPSHOT.jar app.jar
    
          - uses: aws-actions/configure-aws-credentials@v2
            with:
              aws-access-key-id:     ${{ secrets.AWS_ACCESS_KEY_ID }}
              aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
              aws-region:            ap-northeast-2
    
          - name: Docker ECR Login
            run: |
              aws ecr get-login-password --region ap-northeast-2 \\
              | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }}
    
          # Docker Buildx 설치 및 설정
          - name: Setup Docker Buildx
            run: |
              BUILDX_VERSION="v0.24.0" # <-- Buildx 버전 명시
              BUILDX_DIR="/usr/local/lib/docker/cli-plugins"
              BUILDX_BIN="$BUILDX_DIR/docker-buildx"
    
              sudo mkdir -p "$BUILDX_DIR"
    
              # 러너 아키텍처에 맞는 Buildx 바이너리 다운로드 (X64 러너이므로 linux-amd64)
              BUILDX_URL="<https://github.com/docker/buildx/releases/download/${BUILDX_VERSION}/buildx-${BUILDX_VERSION}.linux-amd64>"
    
              echo "Downloading buildx from $BUILDX_URL to $BUILDX_BIN"
              sudo curl -L "$BUILDX_URL" -o "$BUILDX_BIN"
              sudo chmod a+x "$BUILDX_BIN"
    
              echo "Docker Buildx version after setup:"
              docker buildx version # 설치 확인을 위한 명령
    
          # arm 아키텍처로 이미지 빌드
          - name: Build & Push ARM image with tag `dev`
            run: |
              IMAGE="${{ secrets.ECR_REGISTRY }}/${{ secrets.ECR_REPO }}:dev"
              docker buildx inspect multi-arch >/dev/null 2>&1 || docker buildx create --name multi-arch --use --driver docker-container --bootstrap
              docker buildx use multi-arch
              
              docker buildx build \\
                --platform linux/arm64 \\
                --tag "$IMAGE" \\
                --push \\
                .
          # 태그로 인스턴스 IP 가져오기
          - name: Get EC2 Private IPs by Tag
            id: fetch
            run: |
              IPS=$(aws ec2 describe-instances \\
                --filters "Name=tag:Role,Values=newsum-backend" \\
                          "Name=tag:Environment,Values=dev" \\
                          "Name=instance-state-name,Values=running" \\
                --query 'Reservations[*].Instances[*].PrivateIpAddress' \\
                --output text)
              
              # IP 리스트 디버깅 출력
              echo "IPS: $IPS"
              
              # 줄바꿈 제거하고 공백 구분의 한 줄로 만들어 출력
              IPS_SINGLE_LINE=$(echo $IPS | tr '\\n' ' ')
              echo "ips=$IPS_SINGLE_LINE" >> "$GITHUB_OUTPUT"
    
          - name: Deploy to WAS EC2 via SSH
            run: |
              # GitHub Actions 시크릿을 쉘 변수로 먼저 할당
              ECR_REGISTRY=${{ secrets.ECR_REGISTRY }}
              ECR_REPO=${{ secrets.ECR_REPO }}
              SSH_KEY_PATH=${{ secrets.SSH_KEY_PATH }}
              INFISICAL_URL=${{ secrets.AWS_CLOUDFRONT_URL }}
              INFISICAL_PROJECT_ID=${{ secrets.INFISICAL_PROJECT_ID }}
              INFISICAL_TOKEN=${{ secrets.INFISICAL_TOKEN_DEV }}
              INFISICAL_ENV=${{ secrets.INFISICAL_ENV_DEV }}
              INFISICAL_API_URL=${{ secrets.INFISICAL_API_URL }}
              
              for IP in ${{ steps.fetch.outputs.ips }}; do
                echo "🚀 배포 중: $IP"
                echo "SSH키 경로 : $SSH_KEY_PATH" 
    
                # Self-hosted-runner 내부 키 경로
                ssh -o StrictHostKeyChecking=no -i $SSH_KEY_PATH ubuntu@$IP <<-EOF
                  # ECR 로그인
                  aws ecr get-login-password --region ap-northeast-2 \\
                  | docker login --username AWS --password-stdin $ECR_REGISTRY
    
                  # .env 생성
                  curl -Ls https://$INFISICAL_URL/tools/infisical-linux-arm64 -o infisical
                  chmod +x infisical && sudo mv infisical /usr/local/bin/infisical
                  infisical export \\
                    --env=$INFISICAL_ENV \\
                    --projectId=$INFISICAL_PROJECT_ID \\
                    --token=$INFISICAL_TOKEN \\
                    --format=dotenv \\
                    --domain=$INFISICAL_API_URL > /home/ubuntu/newsum-backend.env
              
                  # value에 따옴표 제거 (Infisical에서 문자열이 따옴표로 감싸져 나올 때 유용)
                  sed -i "s/=['\\"]\\([^'\\"]*\\)['\\"]$/=\\1/" /home/ubuntu/newsum-backend.env
    
                  # 기존 컨테이너 중지 및 제거
                  docker stop newsum_backend || true
                  docker rm newsum_backend || true
    
                  # 새 이미지 pull 후 실행
                  docker pull $ECR_REGISTRY/$ECR_REPO:dev
                  docker run -d \\
                    --name newsum_backend \\
                    --restart=always \\
                    --env-file /home/ubuntu/newsum-backend.env \\
                    -p 8080:8080 \\
                    $ECR_REGISTRY/$ECR_REPO:dev
              
                  # 🔐 환경변수 파일 삭제
                  rm -f /home/ubuntu/newsum-backend.env
              EOF
              done
    

     

    하지만 이 파이프라인은 Self-hosted-Runner를 사용할 때 미리 aws 및 docker 명령어를 미리 설치할 수 있다는 장점을 사용하지 않고, 중복으로 설치하는 파이프라인이고 효율적이지 못한 부분도 많이 보입니다.

     

     

    다음 포스팅은 이 파이프라인을 리팩토링 하는 과정에 대해 글을 쓰려고 합니다.

Designed by Tistory.