Skip to content

AWS EC2 Spot Fleet으로 GitLab Runner 저렴하게 구축하는 법 ​

EC2 Launch Template과 Spot Fleet으로 GitLab Runner를 운영할 때 필요한 설정 메모입니다.

실제 암호, 토큰, 계정 ID, 내부 URL, 레지스트리 인증값, Key Pair 이름, VPC/Subnet/Security Group ID는 문서에 남기지 않습니다.

필요한 값은 AWS Secrets Manager, SSM Parameter Store, GitLab Runner 등록 절차, 또는 운영 환경 변수로 주입합니다.

EC2 Launch Template ​

AMI 선정 ​

Amazon Linux 2023 최신 AMI를 사용합니다.

Instance type ​

러너 워크로드에 맞춰 선택합니다.

Docker build, test, image pull/push가 많으면 CPU와 디스크 I/O가 넉넉한 타입을 선택합니다.

Key pair ​

기존에 만들어 둔 Key Pair를 설정합니다. Key Pair 이름은 문서에 남기지 않습니다.

Network settings ​

러너는 외부에서 직접 접속될 필요가 없으므로 Public IP를 부여하지 않습니다.

  • 애플리케이션과 통신할 수 있는 VPC/Subnet을 선택합니다.
  • Bastion에서만 SSH 로그인할 수 있도록 Security Group을 구성합니다.
  • SSH 인바운드는 Bastion Security Group 또는 Bastion CIDR만 허용합니다.
  • ECR, GitLab, Private Registry 등에 접근할 수 있도록 필요한 egress 경로를 확인합니다.

Storage ​

Docker 이미지, build cache, job artifact, log가 쌓일 수 있으므로 EBS 용량을 넉넉하게 잡습니다. 러너 특성상 디스크 부족이 job 실패로 이어지기 쉽습니다.

IAM instance profile ​

EC2에 IAM Role을 연결합니다.

  • ECR pull/push가 필요하면 ECR 권한을 포함합니다.
  • SSM Parameter Store 또는 Secrets Manager에서 runner token이나 registry auth를 읽는다면 해당 read 권한을 포함합니다.
  • 운영 권한은 최소 권한으로 분리합니다.

Purchasing option ​

Spot 전용으로 구성합니다.

User data ​

아래 예시는 민감정보를 제거한 템플릿입니다. <...> 값은 실제 환경에서 Secrets Manager, SSM Parameter Store, CI 등록 절차 등으로 주입하거나 교체합니다.

bash
#!/bin/bash

echo "==================== [Start Setup] ===================="

echo "1. Installing Docker & ECR Credential Helper..."
sudo yum update -y
sudo yum install -y docker amazon-ecr-credential-helper
sudo service docker start
sudo usermod -a -G docker ec2-user

echo "1.5 Configuring ECR Credential Helper for Host..."
# 1) 호스트의 root 계정 설정
sudo mkdir -p /root/.docker
cat << 'EOF' | sudo tee /root/.docker/config.json > /dev/null
{
  "credHelpers": {
    "public.ecr.aws": "ecr-login",
    "<AWS_ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com": "ecr-login"
  }
}
EOF

# 2) 호스트의 ec2-user 계정 설정
sudo mkdir -p /home/ec2-user/.docker
sudo cp /root/.docker/config.json /home/ec2-user/.docker/config.json
sudo chown -R ec2-user:ec2-user /home/ec2-user/.docker

# 3) [중요] GitLab Runner 컨테이너 내부가 참조할 독립적인 docker-config 디렉토리 생성
# Runner 컨테이너 내부의 /root/.docker/config.json 으로 마운트될 예정입니다.
sudo mkdir -p /home/ec2-user/gitlab-runner-docker-config
sudo cp /root/.docker/config.json /home/ec2-user/gitlab-runner-docker-config/config.json
sudo chown -R ec2-user:ec2-user /home/ec2-user/gitlab-runner-docker-config

DOCKER_CONFIG="/usr/local/lib/docker"
echo "DOCKER_CONFIG $DOCKER_CONFIG"
sudo mkdir -p $DOCKER_CONFIG/cli-plugins
sudo curl -SL https://github.com/docker/compose/releases/download/v5.1.2/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose

echo "2. Creating gitlab-runner directories..."
TARGET_DIR="/home/ec2-user/gitlab-runner"
mkdir -p "$TARGET_DIR"

# 3. compose.yml 파일 생성
echo "3. Generating compose.yml..."
cat << 'EOF' > "$TARGET_DIR/compose.yml"
version: '3.8'
services:
  gitlab-runner-compose:
    image: gitlab/gitlab-runner:v18.9.0
    container_name: gitlab-runner-compose
    restart: always
    privileged: true
    stop_signal: SIGQUIT
    security_opt:
      - label=disable
    ulimits:
      nofile:
        soft: 32768
        hard: 65536
    volumes:
      - /home/ec2-user/gitlab-runner:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock
      - gitlab-runner-home:/home/gitlab-runner
      - /usr/bin/docker-credential-ecr-login:/usr/bin/docker-credential-ecr-login
      - /home/ec2-user/gitlab-runner-docker-config:/root/.docker
    logging:
      driver: json-file
      options:
        max-size: "10g"
        max-file: "5"

volumes:
  gitlab-runner-home:
EOF

# 4. config.toml 파일 생성
echo "4. Generating config.toml..."
cat << 'EOF' > "$TARGET_DIR/config.toml"
concurrent = 5
check_interval = 0
connection_max_age = "15m0s"
shutdown_timeout = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "gitlab-runner-__HOSTNAME__"
  url = "<GITLAB_URL>"
  id = <GITLAB_RUNNER_ID>
  token = "<GITLAB_RUNNER_TOKEN>"
  token_obtained_at = <GITLAB_RUNNER_TOKEN_OBTAINED_AT>
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "docker"
  request_concurrency = 4
  environment = ["DOCKER_AUTH_CONFIG={\"auths\":{\"<PRIVATE_REGISTRY_HOST>\":{\"auth\":\"<PRIVATE_REGISTRY_AUTH_BASE64>\"}},\"credHelpers\":{\"<AWS_ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com\":\"ecr-login\",\"public.ecr.aws\":\"ecr-login\"}}"]

  [runners.cache]
    MaxUploadedArchiveSize = 0
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]

  [runners.docker]
    tls_verify = false
    image = "<PRIVATE_REGISTRY_HOST>/<NAMESPACE>/docker-dind:29.4.0"
    pull_policy = "if-not-present"
    privileged = true
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache", "/certs/client", "/var/run/docker.sock:/var/run/docker.sock", "/usr/bin/docker-credential-ecr-login:/usr/bin/docker-credential-ecr-login"]
    shm_size = 0
    network_mtu = 0
EOF

# 4.1. 플레이스홀더 치환
sed -i "s/__HOSTNAME__/${HOSTNAME}/g" "$TARGET_DIR/config.toml"

# 5. 생성한 파일들의 소유권을 ec2-user로 변경
sudo chown -R ec2-user:ec2-user "$TARGET_DIR"

# 6. Docker Compose 실행
echo "5. Launching GitLab Runner container..."
cd "$TARGET_DIR"
sudo docker compose up -d

echo "==================== [Setup Finished] ===================="

Spot Fleet ​

Launch Template에서 Create Fleet from template로 생성합니다.

Launch parameters ​

User data와 Launch Template 설정을 그대로 사용해야 하므로 Use a launch template을 선택합니다.

  • Launch Template 버전을 명확히 지정합니다.
  • 실수로 $Latest가 바뀌어 다른 설정이 배포되지 않도록 운영 버전을 고정합니다.

Additional request detail ​

기본값을 적용합니다.

Target capacity ​

러너 여유분을 고려해 넉넉하게 잡습니다. 예시는 3대입니다.

Persistence ​

매우 중요합니다.

  • Maintain target capacity를 켜서 목표 인스턴스 수가 유지되도록 합니다.
  • Interrupt behavior는 terminate로 설정해 중단된 Spot 인스턴스가 자동 정리되도록 합니다.
  • Capacity rebalance는 Launch only로 켜서 중단 가능성이 있는 인스턴스가 생기면 대체 인스턴스를 먼저 띄우도록 합니다.

이 설정을 빼면 Spot 중단 이후 러너 인스턴스가 모두 사라질 수 있습니다.

Network ​

애플리케이션이 사용하는 VPC를 선택합니다. Private Subnet을 사용하고, 필요한 내부 서비스와 Registry 접근 경로를 확인합니다.

Availability Zone ​

Fleet이 사용할 수 있는 AZ를 넉넉하게 선택합니다. 선택 가능한 AZ가 많을수록 Spot 수급 실패 가능성이 줄어듭니다.

Instance type requirements ​

운영 단순성을 우선하면 Manually select instance types를 사용합니다.

예시:

  • c5.4xlarge

비용과 수급 안정성을 더 중요하게 보면 여러 유사 타입을 함께 넣는 방식도 고려합니다.

Allocation strategy ​

기본 권장값은 Price capacity optimized입니다. 가격 최우선 운영이 필요하면 Lowest price를 선택할 수 있지만, 수급 안정성은 떨어질 수 있습니다.

생성 후 ​

Auto Scaling ​

Spot Fleet Request 생성 후 Auto Scaling 정책을 추가로 구성할 수 있습니다.

  • Target tracking scaling policy: CPU 사용률, 네트워크 처리량 등 지표를 기준으로 자동 조정합니다.
  • Step scaling policy: CloudWatch 알람을 기반으로 단계별 확장/축소를 수행합니다.
  • Min/Max capacity: Fleet이 유지할 최소 및 최대 인스턴스 수를 설정합니다.
  • Default role: Auto Scaling을 위한 AWSServiceRoleForApplicationAutoScaling Service-Linked Role이 자동으로 사용됩니다.

러너 job 수와 대기 시간에 맞춰 적절한 scaling 정책을 선택합니다.

Scheduled Scaling ​

특정 시간대에 러너 용량을 미리 조정할 수 있습니다.

  • Use case: 업무 시간 전후로 러너 수를 증가/감소시켜 비용을 절감하고 빌드 대기 시간을 줄입니다.
  • Cron expression: UTC 기준으로 cron 표현식을 사용해 스케줄을 정의합니다.
    • 예시: cron(0 9 * * MON-FRI) - 평일 오전 9시(UTC)에 실행
  • Scheduled action: 특정 시간에 min/max/desired capacity를 변경하는 작업을 예약합니다.

Auto Scaling 콘솔의 Scheduled Actions에서 시간대별 capacity 조정 규칙을 추가합니다.

IAM Role ​

Trusted policy ​

아래 예시는 계정 ID와 Role 이름을 제거한 템플릿입니다.

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    },
    {
      "Sid": "AllowDeployRoleWithAnyInstance",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<ACCOUNT_ID>:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::<ACCOUNT_ID>:role/<DEPLOY_ROLE_NAME>"
        }
      }
    }
  ]
}

Permission policy ​

러너가 수행하는 job에 맞춰 최소 권한을 부여합니다.

  • ECR image pull만 필요하면 read 계열 권한만 부여합니다.
  • ECR image push가 필요하면 upload, put image 권한을 추가합니다.
  • Secrets Manager 또는 SSM Parameter Store에서 값을 읽는다면 필요한 secret/parameter ARN만 허용합니다.
  • CloudWatch Logs, S3 cache, KMS 등을 사용한다면 리소스 단위로 권한을 제한합니다.

Email: echo.youn@kakao.com