EKS 기반 CI 파이프라인: ECR 빌드 및 푸시
지원 버전: EKS 1.28+, GitLab Runner 17.0+, ARC 0.9+ 마지막 업데이트: 2026년 2월 23일
< 이전: NLB 블루/그린 | 목차 | 다음: ArgoCD 멀티클러스터 >
이 문서에서는 Amazon EKS 클러스터에서 CI(Continuous Integration) 파이프라인을 구축하고 Amazon ECR(Elastic Container Registry)에 컨테이너 이미지를 빌드하여 푸시하는 방법을 설명합니다.
목차
ECR 설정
Amazon ECR은 완전관리형 컨테이너 레지스트리로, EKS와 긴밀하게 통합됩니다. Terraform을 사용하여 ECR 리포지토리를 설정하고 이미지 스캐닝, 수명 주기 정책, 교차 계정 액세스를 구성합니다.
Terraform ECR 리포지토리 생성
# ecr.tf - ECR 리포지토리 및 정책 설정
# ECR 리포지토리 생성
resource "aws_ecr_repository" "app" {
name = "mycompany/myapp"
image_tag_mutability = "IMMUTABLE" # 이미지 태그 불변성 설정
# 이미지 스캐닝 설정
image_scanning_configuration {
scan_on_push = true # 푸시 시 자동 스캔
}
# 암호화 설정
encryption_configuration {
encryption_type = "KMS"
kms_key = aws_kms_key.ecr.arn
}
tags = {
Environment = "production"
Team = "platform"
}
}
# KMS 키 생성 (ECR 암호화용)
resource "aws_kms_key" "ecr" {
description = "ECR repository encryption key"
deletion_window_in_days = 7
enable_key_rotation = true
tags = {
Name = "ecr-encryption-key"
}
}
# 이미지 수명 주기 정책
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "최근 30개의 프로덕션 이미지 유지"
selection = {
tagStatus = "tagged"
tagPrefixList = ["prod-", "release-"]
countType = "imageCountMoreThan"
countNumber = 30
}
action = {
type = "expire"
}
},
{
rulePriority = 2
description = "최근 10개의 개발 이미지 유지"
selection = {
tagStatus = "tagged"
tagPrefixList = ["dev-", "feature-"]
countType = "imageCountMoreThan"
countNumber = 10
}
action = {
type = "expire"
}
},
{
rulePriority = 3
description = "태그 없는 이미지 7일 후 삭제"
selection = {
tagStatus = "untagged"
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 7
}
action = {
type = "expire"
}
},
{
rulePriority = 4
description = "90일 이상된 이미지 삭제"
selection = {
tagStatus = "any"
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 90
}
action = {
type = "expire"
}
}
]
})
}이미지 스캐닝 구성
# ecr-scanning.tf - 향상된 스캐닝 설정
# 레지스트리 스캐닝 구성 (향상된 스캐닝)
resource "aws_ecr_registry_scanning_configuration" "this" {
scan_type = "ENHANCED" # Inspector 기반 향상된 스캐닝
rule {
scan_frequency = "CONTINUOUS_SCAN" # 지속적 스캐닝
repository_filter {
filter = "*"
filter_type = "WILDCARD"
}
}
rule {
scan_frequency = "SCAN_ON_PUSH"
repository_filter {
filter = "mycompany/*"
filter_type = "WILDCARD"
}
}
}
# 스캔 결과 알림을 위한 EventBridge 규칙
resource "aws_cloudwatch_event_rule" "ecr_scan" {
name = "ecr-scan-findings"
description = "ECR 이미지 스캔 결과 알림"
event_pattern = jsonencode({
source = ["aws.ecr"]
detail-type = ["ECR Image Scan"]
detail = {
scan-status = ["COMPLETE"]
finding-severity-counts = {
CRITICAL = [{ "numeric" : [">", 0] }]
HIGH = [{ "numeric" : [">", 0] }]
}
}
})
}
resource "aws_cloudwatch_event_target" "sns" {
rule = aws_cloudwatch_event_rule.ecr_scan.name
target_id = "SendToSNS"
arn = aws_sns_topic.security_alerts.arn
}교차 계정 액세스 정책
# ecr-cross-account.tf - 교차 계정 액세스 설정
variable "allowed_accounts" {
description = "ECR 액세스를 허용할 AWS 계정 ID 목록"
type = list(string)
default = ["111111111111", "222222222222"]
}
# ECR 리포지토리 정책 (교차 계정 풀 허용)
resource "aws_ecr_repository_policy" "cross_account" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "CrossAccountPull"
Effect = "Allow"
Principal = {
AWS = [for account in var.allowed_accounts : "arn:aws:iam::${account}:root"]
}
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
},
{
Sid = "LambdaECRImageRetrievalPolicy"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
]
Condition = {
StringLike = {
"aws:sourceArn" = "arn:aws:lambda:ap-northeast-2:${data.aws_caller_identity.current.account_id}:function:*"
}
}
}
]
})
}
data "aws_caller_identity" "current" {}멀티 리전 복제 구성
# ecr-replication.tf - 멀티 리전 복제 설정
resource "aws_ecr_replication_configuration" "this" {
replication_configuration {
rule {
destination {
region = "us-west-2"
registry_id = data.aws_caller_identity.current.account_id
}
destination {
region = "eu-west-1"
registry_id = data.aws_caller_identity.current.account_id
}
repository_filter {
filter = "mycompany/myapp"
filter_type = "PREFIX_MATCH"
}
}
# 교차 계정 복제 (DR 계정으로)
rule {
destination {
region = "ap-northeast-2"
registry_id = "333333333333" # DR 계정 ID
}
repository_filter {
filter = "mycompany/myapp"
filter_type = "PREFIX_MATCH"
}
}
}
}GitLab Runner on EKS
GitLab Runner를 EKS 클러스터에 배포하여 CI 파이프라인을 실행합니다. Kubernetes executor를 사용하면 각 빌드 작업이 독립된 Pod에서 실행됩니다.
Helm values.yaml 설정
# gitlab-runner-values.yaml
gitlabUrl: https://gitlab.com/
runnerRegistrationToken: "" # 더 이상 사용하지 않음 (deprecated)
# Runner 인증 토큰 (GitLab 16.0+ 권장 방식)
runnerToken: "" # GitLab UI에서 생성한 러너 토큰
# 동시 실행 작업 수
concurrent: 10
# 체크 간격 (초)
checkInterval: 3
# 로그 수준
logLevel: info
# RBAC 설정
rbac:
create: true
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "secrets", "configmaps"]
verbs: ["get", "list", "watch", "create", "delete", "update"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
# ServiceAccount 설정
serviceAccount:
create: true
name: gitlab-runner
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/GitLabRunnerRole
# Runner Pod 리소스
resources:
limits:
memory: 512Mi
cpu: 500m
requests:
memory: 256Mi
cpu: 200m
# Node 선택
nodeSelector:
workload-type: ci-runner
# Tolerations
tolerations:
- key: "ci-runner"
operator: "Equal"
value: "true"
effect: "NoSchedule"
# Runner 설정 (config.toml)
runners:
config: |
[[runners]]
name = "eks-kubernetes-runner"
executor = "kubernetes"
[runners.kubernetes]
namespace = "gitlab-runner"
image = "alpine:latest"
privileged = false
# Pod 리소스 제한
cpu_limit = "2"
cpu_limit_overwrite_max_allowed = "4"
cpu_request = "500m"
cpu_request_overwrite_max_allowed = "2"
memory_limit = "4Gi"
memory_limit_overwrite_max_allowed = "8Gi"
memory_request = "1Gi"
memory_request_overwrite_max_allowed = "4Gi"
# 서비스 리소스 제한
service_cpu_limit = "1"
service_cpu_request = "100m"
service_memory_limit = "1Gi"
service_memory_request = "256Mi"
# Helper 컨테이너 설정
helper_cpu_limit = "500m"
helper_cpu_request = "100m"
helper_memory_limit = "256Mi"
helper_memory_request = "128Mi"
# Pod 어노테이션
[runners.kubernetes.pod_annotations]
"prometheus.io/scrape" = "true"
"prometheus.io/port" = "9252"
# Pod 라벨
[runners.kubernetes.pod_labels]
"app" = "gitlab-ci"
"environment" = "ci"
# Node 선택자
[runners.kubernetes.node_selector]
"workload-type" = "ci-build"
# Tolerations
[[runners.kubernetes.node_tolerations]]
key = "ci-build"
operator = "Equal"
value = "true"
effect = "NoSchedule"
# 볼륨 마운트 (캐시용)
[[runners.kubernetes.volumes.empty_dir]]
name = "docker-cache"
mount_path = "/var/lib/docker"
medium = "Memory"
[[runners.kubernetes.volumes.empty_dir]]
name = "build-cache"
mount_path = "/cache"
# 환경 변수
[runners.kubernetes.pod_spec_patch]
terminationGracePeriodSeconds = 30
# Docker-in-Docker 서비스 설정
[runners.kubernetes.services]
[[runners.kubernetes.services]]
name = "docker-dind"
alias = "docker"
# 캐시 설정 (S3)
[runners.cache]
Type = "s3"
Shared = true
[runners.cache.s3]
ServerAddress = "s3.amazonaws.com"
BucketName = "gitlab-runner-cache-bucket"
BucketLocation = "ap-northeast-2"IAM 역할 및 신뢰 정책 (Pod Identity)
# iam-gitlab-runner.tf - GitLab Runner용 IAM 설정
# IAM 역할 생성
resource "aws_iam_role" "gitlab_runner" {
name = "GitLabRunnerRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "pods.eks.amazonaws.com"
}
Action = [
"sts:AssumeRole",
"sts:TagSession"
]
}
]
})
tags = {
Purpose = "GitLab Runner on EKS"
}
}
# ECR 푸시 정책
resource "aws_iam_role_policy" "ecr_push" {
name = "ecr-push-policy"
role = aws_iam_role.gitlab_runner.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ECRGetAuthToken"
Effect = "Allow"
Action = [
"ecr:GetAuthorizationToken"
]
Resource = "*"
},
{
Sid = "ECRPushPull"
Effect = "Allow"
Action = [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
Resource = "arn:aws:ecr:ap-northeast-2:123456789012:repository/mycompany/*"
}
]
})
}
# S3 캐시 버킷 액세스 정책
resource "aws_iam_role_policy" "s3_cache" {
name = "s3-cache-policy"
role = aws_iam_role.gitlab_runner.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "S3CacheAccess"
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
]
Resource = [
"arn:aws:s3:::gitlab-runner-cache-bucket",
"arn:aws:s3:::gitlab-runner-cache-bucket/*"
]
}
]
})
}
# EKS Pod Identity Association
resource "aws_eks_pod_identity_association" "gitlab_runner" {
cluster_name = var.cluster_name
namespace = "gitlab-runner"
service_account = "gitlab-runner"
role_arn = aws_iam_role.gitlab_runner.arn
}.gitlab-ci.yml 예제
# .gitlab-ci.yml - 완전한 CI 파이프라인 예제
variables:
AWS_REGION: ap-northeast-2
ECR_REGISTRY: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
IMAGE_NAME: mycompany/myapp
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
stages:
- test
- build
- scan
- push
# 기본 이미지 및 서비스
default:
image: docker:24.0
services:
- docker:24.0-dind
tags:
- kubernetes
- eks
# 캐시 설정
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/
- node_modules/
- .cache/
# 단위 테스트
unit-test:
stage: test
image: node:20-alpine
script:
- npm ci --cache .npm --prefer-offline
- npm run lint
- npm run test:unit -- --coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
paths:
- coverage/
expire_in: 1 week
# 통합 테스트
integration-test:
stage: test
image: node:20-alpine
services:
- name: postgres:15-alpine
alias: db
- name: redis:7-alpine
alias: cache
variables:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
DATABASE_URL: postgresql://test_user:test_password@db:5432/test_db
REDIS_URL: redis://cache:6379
script:
- npm ci --cache .npm --prefer-offline
- npm run test:integration
allow_failure: false
# Docker 이미지 빌드
build:
stage: build
before_script:
- until docker info; do sleep 1; done
script:
- |
docker build \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=${CI_COMMIT_SHA} \
--build-arg VERSION=${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} \
--cache-from ${ECR_REGISTRY}/${IMAGE_NAME}:cache \
--tag ${IMAGE_NAME}:${CI_COMMIT_SHA} \
--file Dockerfile \
.
- docker save ${IMAGE_NAME}:${CI_COMMIT_SHA} -o image.tar
artifacts:
paths:
- image.tar
expire_in: 1 hour
# 이미지 보안 스캔 (Trivy)
security-scan:
stage: scan
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- docker load -i image.tar
- |
trivy image \
--exit-code 1 \
--severity CRITICAL,HIGH \
--ignore-unfixed \
--format json \
--output trivy-report.json \
${IMAGE_NAME}:${CI_COMMIT_SHA}
artifacts:
reports:
container_scanning: trivy-report.json
paths:
- trivy-report.json
expire_in: 1 week
allow_failure: true
# ECR에 푸시
push:
stage: push
before_script:
- apk add --no-cache aws-cli
- aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
- docker load -i image.tar
script:
# 태그 지정 및 푸시
- docker tag ${IMAGE_NAME}:${CI_COMMIT_SHA} ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}
- docker push ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}
# 브랜치별 태그
- |
if [ "$CI_COMMIT_BRANCH" == "main" ]; then
docker tag ${IMAGE_NAME}:${CI_COMMIT_SHA} ${ECR_REGISTRY}/${IMAGE_NAME}:latest
docker push ${ECR_REGISTRY}/${IMAGE_NAME}:latest
fi
# 릴리스 태그
- |
if [ -n "$CI_COMMIT_TAG" ]; then
docker tag ${IMAGE_NAME}:${CI_COMMIT_SHA} ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_TAG}
docker push ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_TAG}
fi
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_TAGS3 캐시 버킷 구성
# s3-cache.tf - GitLab Runner 캐시용 S3 버킷
resource "aws_s3_bucket" "gitlab_cache" {
bucket = "gitlab-runner-cache-bucket"
tags = {
Purpose = "GitLab Runner Build Cache"
}
}
resource "aws_s3_bucket_lifecycle_configuration" "gitlab_cache" {
bucket = aws_s3_bucket.gitlab_cache.id
rule {
id = "expire-old-cache"
status = "Enabled"
expiration {
days = 14
}
noncurrent_version_expiration {
noncurrent_days = 7
}
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "gitlab_cache" {
bucket = aws_s3_bucket.gitlab_cache.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "gitlab_cache" {
bucket = aws_s3_bucket.gitlab_cache.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}Runner 토큰 관리
# runner-secret.yaml - GitLab Runner 토큰 시크릿
apiVersion: v1
kind: Secret
metadata:
name: gitlab-runner-token
namespace: gitlab-runner
type: Opaque
stringData:
runner-registration-token: "" # deprecated
runner-token: "glrt-xxxxxxxxxxxxxxxxxxxx" # GitLab UI에서 생성# GitLab Runner 토큰 생성 방법 (GitLab 16.0+)
# 1. GitLab UI -> Settings -> CI/CD -> Runners
# 2. "New project runner" 클릭
# 3. 러너 설정 구성 후 토큰 복사
# 4. Secret으로 저장
kubectl create secret generic gitlab-runner-token \
--namespace gitlab-runner \
--from-literal=runner-token="glrt-xxxxxxxxxxxxxxxxxxxx"GitHub Self-Hosted Runner
GitHub Actions Runner Controller(ARC)를 사용하여 EKS에서 자체 호스팅 러너를 운영합니다.
ARC 설치 (Helm)
# arc-installation.tf - Actions Runner Controller 설치
resource "helm_release" "arc_controller" {
name = "arc"
namespace = "arc-systems"
repository = "oci://ghcr.io/actions/actions-runner-controller-charts"
chart = "gha-runner-scale-set-controller"
version = "0.9.3"
create_namespace = true
values = [
yamlencode({
replicaCount = 2
resources = {
limits = {
cpu = "500m"
memory = "512Mi"
}
requests = {
cpu = "200m"
memory = "256Mi"
}
}
metrics = {
controllerManagerAddr = ":8080"
listenerAddr = ":8080"
listenerEndpoint = "/metrics"
}
})
]
}
resource "helm_release" "arc_runner_set" {
name = "arc-runner-set"
namespace = "arc-runners"
repository = "oci://ghcr.io/actions/actions-runner-controller-charts"
chart = "gha-runner-scale-set"
version = "0.9.3"
create_namespace = true
depends_on = [helm_release.arc_controller]
values = [
yamlencode({
githubConfigUrl = "https://github.com/myorg/myrepo"
# GitHub App 인증 (권장)
githubConfigSecret = {
github_app_id = var.github_app_id
github_app_installation_id = var.github_app_installation_id
github_app_private_key = var.github_app_private_key
}
minRunners = 1
maxRunners = 10
runnerGroup = "eks-runners"
template = {
spec = {
serviceAccountName = "arc-runner"
containers = [
{
name = "runner"
image = "ghcr.io/actions/actions-runner:latest"
resources = {
limits = {
cpu = "2"
memory = "4Gi"
}
requests = {
cpu = "500m"
memory = "1Gi"
}
}
env = [
{
name = "DOCKER_HOST"
value = "tcp://localhost:2376"
},
{
name = "DOCKER_TLS_VERIFY"
value = "1"
},
{
name = "DOCKER_CERT_PATH"
value = "/certs/client"
}
]
volumeMounts = [
{
name = "docker-certs"
mountPath = "/certs"
},
{
name = "work"
mountPath = "/home/runner/_work"
}
]
},
{
name = "docker"
image = "docker:24.0-dind"
securityContext = {
privileged = true
}
env = [
{
name = "DOCKER_TLS_CERTDIR"
value = "/certs"
}
]
volumeMounts = [
{
name = "docker-certs"
mountPath = "/certs"
},
{
name = "docker-graph"
mountPath = "/var/lib/docker"
}
]
}
]
volumes = [
{
name = "docker-certs"
emptyDir = {}
},
{
name = "docker-graph"
emptyDir = {}
},
{
name = "work"
emptyDir = {}
}
]
nodeSelector = {
"workload-type" = "ci-runner"
}
tolerations = [
{
key = "ci-runner"
operator = "Equal"
value = "true"
effect = "NoSchedule"
}
]
}
}
})
]
}RunnerDeployment / RunnerSet CRD 예제
# runner-deployment.yaml - 레거시 ARC v0.x 방식 (참고용)
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: eks-runner-deployment
namespace: arc-runners
spec:
replicas: 3
template:
spec:
repository: myorg/myrepo
labels:
- eks
- self-hosted
- linux
serviceAccountName: arc-runner
resources:
limits:
cpu: "2"
memory: "4Gi"
requests:
cpu: "500m"
memory: "1Gi"
dockerEnabled: true
dockerMTU: 1500
nodeSelector:
workload-type: ci-runner
tolerations:
- key: "ci-runner"
operator: "Equal"
value: "true"
effect: "NoSchedule"
---
# runner-set.yaml - ARC v0.9+ 권장 방식
apiVersion: actions.github.com/v1alpha1
kind: AutoscalingRunnerSet
metadata:
name: eks-runner-set
namespace: arc-runners
spec:
githubConfigUrl: "https://github.com/myorg"
githubConfigSecret: github-config-secret
minRunners: 1
maxRunners: 20
runnerGroup: "eks-runners"
runnerScaleSetName: "eks-k8s-runners"
template:
spec:
serviceAccountName: arc-runner
containers:
- name: runner
image: ghcr.io/actions/actions-runner:latest
command: ["/home/runner/run.sh"]
resources:
limits:
cpu: "2"
memory: "4Gi"
requests:
cpu: "500m"
memory: "1Gi"Scale-from-Zero 설정
# horizontal-runner-autoscaler.yaml - 레거시 방식 (참고용)
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
name: eks-runner-autoscaler
namespace: arc-runners
spec:
scaleTargetRef:
kind: RunnerDeployment
name: eks-runner-deployment
minReplicas: 0
maxReplicas: 20
scaleDownDelaySecondsAfterScaleOut: 300
metrics:
- type: PercentageRunnersBusy
scaleUpThreshold: "0.75"
scaleDownThreshold: "0.25"
scaleUpFactor: "2"
scaleDownFactor: "0.5"
- type: TotalNumberOfQueuedAndInProgressWorkflowRuns
repositoryNames:
- myorg/myrepo
scaleUpThreshold: "1"
scaleDownThreshold: "0"
---
# ARC v0.9+ 방식에서는 scale-from-zero가 기본 동작
# minRunners: 0으로 설정하면 자동으로 scale-from-zero 활성화GitHub Actions 워크플로우
# .github/workflows/build.yml
name: Build and Push to ECR
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
env:
AWS_REGION: ap-northeast-2
ECR_REGISTRY: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
IMAGE_NAME: mycompany/myapp
jobs:
test:
runs-on: eks-runners # self-hosted runner label
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
build:
needs: test
runs-on: eks-runners
permissions:
id-token: write
contents: read
outputs:
image: ${{ steps.build.outputs.image }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:cache
cache-to: type=registry,ref=${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:cache,mode=max
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp }}
VCS_REF=${{ github.sha }}
VERSION=${{ github.ref_name }}
- name: Output image
run: echo "image=${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}" >> $GITHUB_OUTPUT
security-scan:
needs: build
runs-on: eks-runners
permissions:
id-token: write
contents: read
security-events: write
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ needs.build.outputs.image }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'Pod Identity 설정
# iam-github-actions.tf - GitHub Actions용 IAM 설정
# OIDC Provider for GitHub Actions (워크플로우 ID 토큰용)
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["ffffffffffffffffffffffffffffffffffffffff"]
}
# GitHub Actions용 IAM 역할
resource "aws_iam_role" "github_actions" {
name = "GitHubActionsRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:myorg/myrepo:*"
}
}
}
]
})
}
# ECR 푸시 정책 연결
resource "aws_iam_role_policy_attachment" "ecr_push" {
role = aws_iam_role.github_actions.name
policy_arn = aws_iam_policy.ecr_push.arn
}
# ARC Runner용 Pod Identity
resource "aws_iam_role" "arc_runner" {
name = "ArcRunnerRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "pods.eks.amazonaws.com"
}
Action = [
"sts:AssumeRole",
"sts:TagSession"
]
}
]
})
}
resource "aws_eks_pod_identity_association" "arc_runner" {
cluster_name = var.cluster_name
namespace = "arc-runners"
service_account = "arc-runner"
role_arn = aws_iam_role.arc_runner.arn
}Multi-Platform Build
ARM(Graviton)과 x86 아키텍처 모두를 지원하는 멀티 플랫폼 이미지를 빌드합니다.
ARM Runner NodePool (Graviton)
# arm-runner-nodepool.yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: ci-runner-arm
labels:
purpose: ci-runner
arch: arm64
spec:
template:
metadata:
labels:
workload-type: ci-runner
arch: arm64
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["arm64"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["c7g", "m7g", "r7g"]
- key: karpenter.k8s.aws/instance-size
operator: In
values: ["large", "xlarge", "2xlarge"]
taints:
- key: ci-runner
value: "true"
effect: NoSchedule
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
limits:
cpu: 100
memory: 200Gi
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 5mx86 Runner NodePool
# x86-runner-nodepool.yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: ci-runner-amd64
labels:
purpose: ci-runner
arch: amd64
spec:
template:
metadata:
labels:
workload-type: ci-runner
arch: amd64
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["c7i", "m7i", "r7i"]
- key: karpenter.k8s.aws/instance-size
operator: In
values: ["large", "xlarge", "2xlarge"]
taints:
- key: ci-runner
value: "true"
effect: NoSchedule
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
limits:
cpu: 100
memory: 200Gi
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 5mGitLab CI 멀티 아키텍처 빌드
# .gitlab-ci.yml - 멀티 아키텍처 빌드
variables:
AWS_REGION: ap-northeast-2
ECR_REGISTRY: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
IMAGE_NAME: mycompany/myapp
stages:
- build
- manifest
# 아키텍처별 병렬 빌드
build:
stage: build
image: docker:24.0
services:
- docker:24.0-dind
parallel:
matrix:
- ARCH: amd64
RUNNER_TAG: ci-runner-amd64
- ARCH: arm64
RUNNER_TAG: ci-runner-arm64
tags:
- ${RUNNER_TAG}
before_script:
- apk add --no-cache aws-cli
- aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
script:
- |
docker build \
--platform linux/${ARCH} \
--tag ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}-${ARCH} \
--push \
.
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_TAG
# 멀티 아키텍처 매니페스트 생성
create-manifest:
stage: manifest
image: docker:24.0
tags:
- kubernetes
needs:
- build
before_script:
- apk add --no-cache aws-cli
- aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
script:
# 멀티 아키텍처 매니페스트 생성
- |
docker manifest create ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA} \
${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}-amd64 \
${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}-arm64
# 아키텍처 어노테이션 추가
- |
docker manifest annotate ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA} \
${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}-amd64 \
--os linux --arch amd64
- |
docker manifest annotate ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA} \
${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}-arm64 \
--os linux --arch arm64
# 매니페스트 푸시
- docker manifest push ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}
# latest 태그 (main 브랜치)
- |
if [ "$CI_COMMIT_BRANCH" == "main" ]; then
docker manifest create ${ECR_REGISTRY}/${IMAGE_NAME}:latest \
${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}-amd64 \
${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}-arm64
docker manifest push ${ECR_REGISTRY}/${IMAGE_NAME}:latest
fi
# 릴리스 태그
- |
if [ -n "$CI_COMMIT_TAG" ]; then
docker manifest create ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_TAG} \
${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}-amd64 \
${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}-arm64
docker manifest push ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_TAG}
fi
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_TAGGitHub Actions 멀티 아키텍처 빌드
# .github/workflows/multi-arch-build.yml
name: Multi-Architecture Build
on:
push:
branches: [main]
tags: ['v*']
env:
AWS_REGION: ap-northeast-2
ECR_REGISTRY: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
IMAGE_NAME: mycompany/myapp
jobs:
build:
runs-on: eks-runners
permissions:
id-token: write
contents: read
strategy:
matrix:
platform:
- linux/amd64
- linux/arm64
include:
- platform: linux/amd64
runner: eks-runners-amd64
- platform: linux/arm64
runner: eks-runners-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push by platform
uses: docker/build-push-action@v5
with:
context: .
platforms: ${{ matrix.platform }}
push: true
tags: ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
cache-from: type=registry,ref=${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
cache-to: type=registry,ref=${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }},mode=max
manifest:
needs: build
runs-on: eks-runners
permissions:
id-token: write
contents: read
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create and push manifest
run: |
# imagetools를 사용한 멀티 아키텍처 매니페스트 생성
docker buildx imagetools create \
--tag ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
--tag ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}-amd64 \
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}-arm64
# 릴리스 태그
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
TAG_NAME="${{ github.ref_name }}"
docker buildx imagetools create \
--tag ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG_NAME} \
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}-amd64 \
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}-arm64
fi
- name: Inspect manifest
run: |
docker buildx imagetools inspect ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}빌드 최적화
빌드 시간을 단축하고 리소스 효율성을 높이는 최적화 기법입니다.
BuildKit 캐시 활용
# Dockerfile - BuildKit 최적화
# syntax=docker/dockerfile:1.6
FROM node:20-alpine AS builder
WORKDIR /app
# 패키지 파일만 먼저 복사하여 캐시 활용
COPY package*.json ./
# BuildKit 캐시 마운트 활용
RUN --mount=type=cache,target=/root/.npm \
npm ci --prefer-offline
# 소스 코드 복사 및 빌드
COPY . .
RUN --mount=type=cache,target=/app/.next/cache \
npm run build
# 프로덕션 이미지
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# 비-root 사용자 생성
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# 빌드 결과물만 복사
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]# GitLab CI - BuildKit 캐시 활용
build:
stage: build
image: docker:24.0
services:
- docker:24.0-dind
variables:
DOCKER_BUILDKIT: 1
script:
- |
docker build \
--cache-from type=registry,ref=${ECR_REGISTRY}/${IMAGE_NAME}:cache \
--cache-to type=registry,ref=${ECR_REGISTRY}/${IMAGE_NAME}:cache,mode=max \
--tag ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA} \
--push \
.Kaniko 빌드 (Rootless)
# kaniko-build.yaml - Docker 데몬 없이 빌드
apiVersion: v1
kind: Pod
metadata:
name: kaniko-build
namespace: ci-builds
spec:
restartPolicy: Never
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myrepo.git#refs/heads/main"
- "--destination=123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/mycompany/myapp:latest"
- "--cache=true"
- "--cache-repo=123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/mycompany/myapp/cache"
- "--snapshot-mode=redo"
- "--use-new-run"
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker
resources:
limits:
cpu: "2"
memory: "4Gi"
requests:
cpu: "500m"
memory: "1Gi"
volumes:
- name: docker-config
secret:
secretName: ecr-docker-config
items:
- key: .dockerconfigjson
path: config.json
serviceAccountName: kaniko-builder# .gitlab-ci.yml - Kaniko 빌드 작업
build-kaniko:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
tags:
- kubernetes
script:
- |
# ECR 인증 설정
mkdir -p /kaniko/.docker
echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json
- |
/kaniko/executor \
--context ${CI_PROJECT_DIR} \
--dockerfile ${CI_PROJECT_DIR}/Dockerfile \
--destination ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA} \
--cache=true \
--cache-repo=${ECR_REGISTRY}/${IMAGE_NAME}/cache \
--snapshot-mode=redo \
--compressed-caching=false레이어 캐싱 전략
# Dockerfile - 최적화된 레이어 캐싱
# 1단계: 의존성 설치 (캐시 효율 극대화)
FROM node:20-alpine AS deps
WORKDIR /app
# 패키지 매니저 락 파일만 복사
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
# 패키지 매니저에 따라 설치
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# 2단계: 빌드
FROM node:20-alpine AS builder
WORKDIR /app
# 의존성 복사
COPY --from=deps /app/node_modules ./node_modules
# 소스 코드 복사
COPY . .
# 환경 변수 설정
ENV NEXT_TELEMETRY_DISABLED 1
# 빌드
RUN npm run build
# 3단계: 프로덕션 이미지
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# 보안: 비-root 사용자
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 필요한 파일만 복사
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]빌드 시간 최적화 팁
# .gitlab-ci.yml - 빌드 최적화 예제
variables:
# BuildKit 활성화
DOCKER_BUILDKIT: 1
BUILDKIT_PROGRESS: plain
# 병렬 빌드 설정
COMPOSE_DOCKER_CLI_BUILD: 1
COMPOSE_PARALLEL_LIMIT: 4
build-optimized:
stage: build
script:
# 1. 변경된 파일 기반 빌드 스킵
- |
if git diff --name-only HEAD~1 HEAD | grep -qvE '(\.md$|\.txt$|docs/)'; then
echo "코드 변경 감지, 빌드 실행"
else
echo "문서만 변경됨, 빌드 스킵"
exit 0
fi
# 2. 병렬 빌드 (멀티 스테이지)
- |
docker build \
--target builder \
--cache-from ${ECR_REGISTRY}/${IMAGE_NAME}:builder-cache \
--tag ${ECR_REGISTRY}/${IMAGE_NAME}:builder-cache \
--build-arg BUILDKIT_INLINE_CACHE=1 \
. &
docker build \
--target tester \
--cache-from ${ECR_REGISTRY}/${IMAGE_NAME}:tester-cache \
--tag ${ECR_REGISTRY}/${IMAGE_NAME}:tester-cache \
--build-arg BUILDKIT_INLINE_CACHE=1 \
. &
wait
# 3. 최종 이미지 빌드
- |
docker build \
--cache-from ${ECR_REGISTRY}/${IMAGE_NAME}:builder-cache \
--cache-from ${ECR_REGISTRY}/${IMAGE_NAME}:tester-cache \
--cache-from ${ECR_REGISTRY}/${IMAGE_NAME}:latest \
--tag ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA} \
--build-arg BUILDKIT_INLINE_CACHE=1 \
.
# 4. 캐시 이미지 푸시 (백그라운드)
- docker push ${ECR_REGISTRY}/${IMAGE_NAME}:builder-cache &
- docker push ${ECR_REGISTRY}/${IMAGE_NAME}:tester-cache &
- docker push ${ECR_REGISTRY}/${IMAGE_NAME}:${CI_COMMIT_SHA}
- wait요약
이 문서에서 다룬 주요 내용:
ECR 설정: Terraform을 사용한 ECR 리포지토리 생성, 수명 주기 정책, 이미지 스캐닝, 교차 계정 액세스, 멀티 리전 복제 구성
GitLab Runner on EKS: Helm을 통한 설치, Kubernetes executor 설정, Pod Identity를 통한 ECR 푸시, S3 캐시 구성
GitHub Self-Hosted Runner: ARC 설치 및 구성, RunnerDeployment/RunnerSet CRD, Scale-from-zero, GitHub Actions 워크플로우
Multi-Platform Build: ARM(Graviton)과 x86 러너 NodePool, 병렬 빌드, docker manifest 및 buildx imagetools를 통한 멀티 아키텍처 이미지 생성
빌드 최적화: BuildKit 캐시, Kaniko 빌드, 레이어 캐싱 전략, 빌드 시간 단축 기법
관련 문서
퀴즈
이 장에서 배운 내용을 테스트하려면 CI 파이프라인 퀴즈를 풀어보세요.