Cloud Run 작업자 풀로 GitHub 실행기 호스팅

이 튜토리얼에서는 작업자 풀에서 자체 호스팅 GitHub 러너를 사용하여 GitHub 저장소에 정의된 워크플로를 실행하는 방법을 안내합니다.

이 워크로드를 처리하기 위해 Cloud Run 작업자 풀을 배포하고, 선택적으로 작업자 풀의 확장을 지원하기 위해 Cloud Run 함수를 배포합니다.

자체 호스팅 GitHub 실행기 정보

GitHub Actions 워크플로에서 실행기는 작업을 실행하는 머신입니다. 예를 들어 러너는 저장소를 로컬로 클론하고 테스트 소프트웨어를 설치한 다음 코드를 평가하는 명령어를 실행할 수 있습니다.

자체 호스팅 실행기를 사용하여 Cloud Run 작업자 풀 인스턴스에서 GitHub Actions를 실행할 수 있습니다. 이 튜토리얼에서는 실행 중이거나 예약되지 않은 작업 수를 기반으로 러너 풀을 자동으로 확장하는 방법을 보여줍니다. 작업이 없는 경우 풀을 0으로 확장하는 방법도 보여줍니다.

코드 샘플 검색

사용할 코드 샘플을 검색하려면 다음 안내를 따르세요.

  1. 샘플 저장소를 로컬 머신에 클론합니다.

    git clone https://github.com/GoogleCloudPlatform/cloud-run-samples
    
  2. Cloud Run 샘플 코드가 포함된 디렉터리로 변경합니다.

    cd cloud-run-samples/github-runner
    

핵심 코드 이해

이 샘플은 다음에 설명된 것처럼 작업자 풀 및 자동 확장 처리기로 구현됩니다.

작업자 풀

작업자 풀은 GitHub에서 만든 actions/runner 이미지를 기반으로 하는 Dockerfile로 구성됩니다.

작은 도우미 스크립트를 제외하고 모든 로직이 이 이미지에 포함되어 있습니다.

FROM ghcr.io/actions/actions-runner:2.329.0

# Add scripts with right permissions.
USER root
# hadolint ignore=DL3045
COPY start.sh start.sh
RUN chmod +x start.sh

# Add start entrypoint with right permissions.
USER runner
ENTRYPOINT ["./start.sh"]

이 도우미 스크립트는 컨테이너가 시작될 때 실행되며, 생성할 토큰을 사용하여 구성된 저장소에 임시 인스턴스로 등록됩니다. 스크립트는 컨테이너가 축소될 때 수행할 작업도 정의합니다.

# Configure the current runner instance with URL, token and name.
mkdir /home/docker/actions-runner && cd /home/docker/actions-runner
echo "GitHub Repo: ${GITHUB_REPO_URL} for ${RUNNER_PREFIX}-${RUNNER_SUFFIX}"
./config.sh --unattended --url ${GITHUB_REPO_URL} --pat ${GH_TOKEN} --name ${RUNNER_NAME}

# Function to cleanup and remove runner from Github.
cleanup() {
   echo "Removing runner..."
   ./config.sh remove --unattended --pat ${GH_TOKEN}
}

# Trap signals.
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM

# Run the runner.
./run.sh & wait $!

자동 확장 처리

자동 확장 처리기는 대기열에 새 작업이 있으면 작업자 풀을 확장하고 작업이 완료되면 축소하는 함수입니다. Cloud Run API를 사용하여 풀의 현재 작업자 수를 확인하고 필요에 따라 값을 조정합니다.

try:
    current_instance_count = get_current_worker_pool_instance_count()
except ValueError as e:
    return f"Could not retrieve instance count: {e}", 500

# Scale Up: If a job is queued and we have available capacity
if action == "queued" and job_status == "queued":
    print(f"Job '{job_name}' is queued.")

    if current_instance_count < MAX_RUNNERS:
        new_instance_count = current_instance_count + 1
        try:
            update_runner_instance_count(new_instance_count)
            print(f"Successfully scaled up to {new_instance_count} instances.")
        except ValueError as e:
            return f"Error scaling up instances: {e}", 500
    else:
        print(f"Max runners ({MAX_RUNNERS}) reached.")

# Scale Down: If a job is completed, check to see if there are any more pending
# or in progress jobs and scale accordingly.
elif action == "completed" and job_status == "completed":
    print(f"Job '{job_name}' completed.")

    current_queued_actions, current_running_actions = get_current_actions()
    current_actions = current_queued_actions + current_running_actions

    if current_queued_actions >= 1:
        print(
            f"GitHub says {current_queued_actions} are still pending."
            f"Won't change scaling ({current_instance_count})."
        )
    elif current_queued_actions == 0 and current_running_actions >= 1:
        print(
            f"GitHub says no queued actions, but {current_running_actions} running actions."
            f"Won't change scaling ({current_instance_count})."
        )
    elif current_actions == 0:
        print(f"GitHub says no pending actions. Scaling to zero.")
        update_runner_instance_count(0)
        print(f"Successfully scaled down to zero.")
    else:
        print(
            f"Detected an unhandled state: {current_queued_actions=}, {current_running_actions=}"
        )
else:
    print(
        f"Workflow job event for '{job_name}' with action '{action}' and "
        f"status '{job_status}' did not trigger a scaling action."
    )

IAM 구성

이 튜토리얼에서는 프로비저닝된 리소스를 사용하는 데 필요한 최소 권한이 있는 커스텀 서비스 계정을 사용합니다. 서비스 계정을 설정하려면 다음 단계를 따르세요.

  1. gcloud에서 프로젝트 ID를 설정합니다.

    gcloud config set project PROJECT_ID
    

    PROJECT_ID를 프로젝트 ID로 바꿉니다.

  2. 새 Identity and Access Management 서비스 계정을 만듭니다.

    gcloud iam service-accounts create gh-runners
    

  3. 서비스 계정에 프로젝트에서 서비스 계정으로 작동할 수 있는 권한을 부여합니다.

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role=roles/iam.serviceAccountUser
    

    PROJECT_ID를 프로젝트 ID로 바꿉니다.

GitHub 정보 가져오기

자체 호스팅 러너 추가에 대한 GitHub 문서에서는 GitHub 웹사이트를 통해 러너를 추가한 다음 인증에 사용할 특정 토큰을 제공할 것을 제안합니다.

이 튜토리얼에서는 러너를 동적으로 추가하고 삭제하며, 이를 위해 정적 GitHub 토큰이 필요합니다.

이 튜토리얼을 완료하려면 선택한 저장소와 상호작용할 수 있는 액세스 권한이 있는 GitHub 토큰을 만들어야 합니다.

GitHub 저장소 식별

이 튜토리얼에서 GITHUB_REPO 변수는 저장소 이름을 나타냅니다. 개인 사용자 저장소와 조직 저장소 모두에서 도메인 이름 뒤에 오는 GitHub 저장소 이름 부분입니다.

사용자 소유 저장소와 조직 소유 저장소 모두 도메인 이름 뒤에 오는 저장소 이름을 참조하게 됩니다.

이 가이드에서 수행할 단계:

  • https://github.com/myuser/myrepo의 경우 GITHUB_REPOmyuser/myrepo입니다.
  • https://github.com/mycompany/ourrepo의 경우 GITHUB_REPOmycompany/ourrepo입니다.

액세스 토큰 만들기

GitHub에서 액세스 토큰을 만들고 Secret Manager에 안전하게 저장해야 합니다.

  1. GitHub 계정에 로그인되어 있는지 확인합니다.
  2. GitHub의 Settings(설정) > Developer Settings(개발자 설정) > Personal Access Tokens(개인 액세스 토큰) 페이지로 이동합니다.
  3. 새 토큰 생성을 클릭하고 새 토큰 생성 (클래식)을 선택합니다.
  4. 'repo' 범위로 새 토큰을 만듭니다.
  5. 토큰 생성을 클릭합니다.
  6. 생성된 토큰을 복사합니다.

보안 비밀 값 만들기

방금 만든 보안 비밀 토큰을 가져와 Secret Manager에 저장하고 액세스 권한을 설정합니다.

  1. Secret Manager에서 보안 비밀을 만듭니다.

    echo -n "GITHUB_TOKEN" | gcloud secrets create github_runner_token --data-file=-
    

    GITHUB_TOKEN를 GitHub에서 복사한 값으로 바꿉니다.

  2. 새로 만든 보안 비밀에 대한 액세스 권한을 부여합니다.

    gcloud secrets add-iam-policy-binding github_runner_token \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role "roles/secretmanager.secretAccessor"
    

작업자 풀 배포

GitHub 작업을 처리할 Cloud Run 작업자 풀을 만듭니다. 이 풀은 GitHub에서 만든 actions/runner 이미지를 기반으로 하는 이미지를 사용합니다.

Cloud Run 작업자 풀 설정

  1. 작업자 풀의 샘플 코드로 이동합니다.

    cd worker-pool-container
    
  2. 작업자 풀을 배포합니다.

    gcloud beta run worker-pools deploy WORKER_POOL_NAME \
      --region WORKER_POOL_LOCATION \
      --source . \
      --scaling 1 \
      --set-env-vars GITHUB_REPO=GITHUB_REPO \
      --set-secrets GITHUB_TOKEN=github_runner_token:latest \
      --service-account gh-runners@PROJECT_ID.iam.gserviceaccount.com \
      --memory 2Gi \
      --cpu 4
    

    다음을 바꿉니다.

    • WORKER_POOL_NAME 작업자 풀의 이름
    • WORKER_POOL_LOCATION 작업자 풀의 리전
    • GITHUB_REPO 식별된 GitHub 저장소 이름
    • PROJECT_ID를 Google Cloud 프로젝트 ID로 바꿉니다.

    이 프로젝트에서 Cloud Run 소스 배포를 처음 사용하는 경우 기본 Artifact Registry 저장소를 만들라는 메시지가 표시됩니다.

작업자 풀 사용

이제 작업자 풀에 단일 인스턴스가 있으며 GitHub Actions의 작업을 수락할 준비가 되었습니다.

자체 호스팅 러너의 설정을 완료했는지 확인하려면 저장소에서 GitHub 작업을 호출하세요.

작업에서 자체 호스팅 실행기를 사용하려면 GitHub 작업의 작업을 변경해야 합니다. 작업에서 runs-on 값을 self-hosted로 변경합니다.

저장소에 아직 작업이 없는 경우 GitHub Actions 빠른 시작을 따르세요.

자체 호스팅 러너를 사용하도록 작업을 구성한 후 작업을 실행합니다.

GitHub 인터페이스에서 작업이 성공적으로 완료되었는지 확인합니다.

GitHub Runner Autoscaler 배포

원래 풀에 작업자 하나를 배포하여 한 번에 하나의 작업을 처리할 수 있습니다. CI 사용량에 따라 처리해야 할 작업이 급증하는 경우 풀을 확장해야 할 수 있습니다.

활성 GitHub 러너를 사용하여 작업자 풀을 배포한 후 작업 대기열의 작업 상태에 따라 작업자 인스턴스를 프로비저닝하도록 자동 스케일러를 구성합니다.

이 구현은 workflow_job 이벤트를 수신 대기합니다. 워크플로 작업이 생성되면 작업자 풀이 스케일 업되고 작업이 완료되면 다시 스케일 다운됩니다. 구성된 최대 인스턴스 수를 초과하여 풀을 확장하지 않으며 실행 중인 모든 작업이 완료되면 0으로 확장됩니다.

워크로드에 따라 이 자동 확장 처리를 조정할 수 있습니다.

웹훅 보안 비밀 값 만들기

웹훅의 보안 비밀 값을 만들려면 다음을 수행합니다.

  1. 임의의 문자열 값이 포함된 Secret Manager 보안 비밀을 만듭니다.

    echo -n "WEBHOOK_SECRET" | gcloud secrets create github_webhook_secret --data-file=-
    

    WEBHOOK_SECRET를 임의의 문자열 값으로 바꿉니다.

  2. 자동 스케일러 서비스 계정에 보안 비밀에 대한 액세스 권한을 부여합니다.

    gcloud secrets add-iam-policy-binding github_webhook_secret \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role "roles/secretmanager.secretAccessor"
    

웹훅 요청을 수신하도록 함수 배포

웹훅 요청을 수신하는 함수를 배포하려면 다음 단계를 따르세요.

  1. 웹훅의 샘플 코드로 이동합니다.

    cd ../autoscaler
    
  2. Cloud Run 함수를 배포합니다.

    gcloud run deploy github-runner-autoscaler \
      --function github_webhook_handler \
      --region WORKER_POOL_LOCATION \
      --source . \
      --set-env-vars GITHUB_REPO=GITHUB_REPO \
      --set-env-vars WORKER_POOL_NAME=WORKER_POOL_NAME \
      --set-env-vars WORKER_POOL_LOCATION=WORKER_POOL_LOCATION \
      --set-env-vars MAX_RUNNERS=5 \
      --set-secrets GITHUB_TOKEN=github_runner_token:latest \
      --set-secrets WEBHOOK_SECRET=github_webhook_secret:latest \
      --service-account gh-runners@PROJECT_ID.iam.gserviceaccount.com \
      --allow-unauthenticated
    

    다음을 바꿉니다.

    • GITHUB_REPO 도메인 이름 뒤에 오는 GitHub 저장소 이름 부분
    • WORKER_POOL_NAME 작업자 풀의 이름
    • WORKER_POOL_LOCATION 작업자 풀의 리전
    • REPOSITORY_NAME GitHub 저장소 이름
  3. 서비스가 배포된 URL을 기록해 둡니다. 이 값은 이후 단계에서 사용됩니다.

  4. 서비스 계정에 작업자 풀을 업데이트할 수 있는 권한을 부여합니다.

    gcloud alpha run worker-pools add-iam-policy-binding WORKER_POOL_NAME \
      --member "serviceAccount:gh-runners@PROJECT_ID.iam.gserviceaccount.com" \
      --role=roles/run.developer
    

    PROJECT_ID를 프로젝트 ID로 바꿉니다.

GitHub 웹훅 만들기

GitHub 웹훅을 만들려면 다음 단계를 따르세요.

  1. GitHub 계정에 로그인되어 있는지 확인합니다.
  2. GitHub 저장소로 이동합니다.
  3. 설정을 클릭합니다.
  4. '코드 및 자동화'에서 웹훅을 클릭합니다.
  5. 웹훅 추가를 클릭합니다.
  6. 다음을 입력합니다.

    1. 페이로드 URL이전에 배포한 Cloud Run 함수의 URL을 입력합니다.

      URL은 https://github-runner-autoscaler-PROJECTNUM.REGION.run.app와 같은 형식입니다. 여기서 PROJECTNUM는 프로젝트의 고유 숫자 식별자이고 REGION는 서비스를 배포한 리전입니다.

    2. 콘텐츠 유형으로 application/json을 선택합니다.

    3. Secret(비밀번호)에 이전에 만든 WEBHOOK_SECRET 값을 입력합니다.

    4. SSL 확인에서 SSL 확인 사용 설정을 선택합니다.

    5. '어떤 이벤트에서 이 웹훅을 트리거하시겠어요?'에서 개별 이벤트 직접 선택을 선택합니다.

    6. 이벤트 선택에서 워크플로 작업을 선택합니다. 다른 옵션을 선택 해제합니다.

    7. 웹훅 추가를 클릭합니다.

작업자 풀 축소

이제 웹훅이 있으므로 풀에 영구 작업자가 없어도 됩니다. 이렇게 하면 처리할 작업이 없을 때 실행 중인 작업자가 없으므로 비용이 절감됩니다.

  • 0으로 조정되도록 풀을 조정합니다.

    gcloud beta run worker-pools update WORKER_POOL_NAME \
      --region WORKER_POOL_LOCATION \
      --scaling 0
    

자동 확장 러너 사용

자동 확장 러너가 올바르게 작동하는지 확인하려면 이전에 runs-on: self-hosted로 구성한 작업을 실행하세요.

저장소의 '작업' 탭에서 GitHub 작업의 진행 상황을 추적할 수 있습니다.

Cloud Run 함수 및 Cloud Run 작업자 풀의 로그 탭을 각각 확인하여 웹훅 함수와 작업자 풀의 실행을 확인할 수 있습니다.