Published on

EKS IRSA는 되는데 STS 429 Throttling 해결

Authors

서론

EKS에서 IRSA(IAM Roles for Service Accounts) 설정은 멀쩡합니다. 서비스어카운트에 어노테이션도 붙었고, 파드 안에서 AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE도 보이며, 권한도 맞습니다. 그런데 운영 트래픽이 올라가거나 파드가 많이 뜨는 타이밍에 갑자기 애플리케이션 로그에 아래 같은 오류가 쏟아집니다.

  • ThrottlingException: Rate exceeded
  • HTTP 429 from STS
  • AssumeRoleWithWebIdentity 호출 실패

이 상황은 “IRSA가 안 된다”가 아니라, IRSA가 너무 잘(?) 동작해서 STS 토큰 교환 요청이 폭주한 결과인 경우가 대부분입니다. 이 글에서는 (1) 왜 STS 429가 나는지, (2) 어떤 지표/로그로 확진하는지, (3) 즉효성 있는 완화책과 (4) 구조적으로 재발을 막는 방법을 실전 관점에서 정리합니다.

관련해서 IRSA 자체 점검이 필요하다면 먼저 아래 글의 10분 진단 체크리스트가 도움이 됩니다.

증상: “IRSA는 되는데” 왜 STS만 429로 터지나

IRSA 흐름을 아주 단순화하면 다음과 같습니다.

  1. 파드가 서비스어카운트 토큰(JWT)을 파일로 마운트받음 (AWS_WEB_IDENTITY_TOKEN_FILE)
  2. AWS SDK/클라이언트가 STS AssumeRoleWithWebIdentity 호출
  3. STS가 임시 자격 증명(AccessKey/SecretKey/SessionToken)을 반환
  4. SDK는 이 자격 증명을 캐시하고, 만료 전에 갱신

문제는 2번입니다. 다음 조건이 겹치면 STS 호출 수가 기하급수적으로 늘어납니다.

  • 파드 수가 많음(수십~수백)
  • 각 파드 안에 워커/스레드/프로세스가 많음(예: gunicorn worker 8개, sidecar 포함)
  • 애플리케이션이 여러 AWS 클라이언트를 짧은 주기로 생성(클라이언트 생성 시마다 크리덴셜 리졸브)
  • 크리덴셜 캐시가 프로세스/컨테이너 단위로 분리되어 공유되지 않음
  • 트래픽 스파이크/오토스케일로 파드가 동시에 기동(콜드 스타트)

즉, IRSA는 정상이라도 “토큰 교환 API(STS) 호출량”이 한도를 넘으면 429가 납니다.

확진: STS Throttling인지 빠르게 확인하는 방법

1) 애플리케이션 로그에서 API 이름 확인

대부분 SDK는 어떤 API가 실패했는지 로그/예외 메시지에 남깁니다.

  • AssumeRoleWithWebIdentity 가 보이면 거의 확정
  • GetCallerIdentity만 실패한다면(헬스체크에서 자주 호출) 다른 패턴일 수 있음

2) CloudTrail에서 STS 이벤트 빈도 확인

CloudTrail의 EventName이 AssumeRoleWithWebIdentity로 급증하는지 확인합니다.

  • 이벤트 소스: sts.amazonaws.com
  • 이벤트 이름: AssumeRoleWithWebIdentity
  • UserAgent: aws-sdk-*

3) CloudWatch 지표/로그(가능하면)로 429 집계

STS는 서비스별 지표가 제한적일 수 있으나, 애플리케이션 측에서 다음을 반드시 남기면 좋습니다.

  • 예외 타입(ThrottlingException)
  • HTTP status(429)
  • 재시도 횟수
  • 호출한 SDK/라이브러리 버전

4) 파드 내부에서 환경변수/토큰 파일 확인(보조)

IRSA 자체가 틀린 건 아닌지 확인하는 최소 체크입니다.

kubectl exec -it deploy/myapp -- sh -c 'env | egrep "AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION"'

kubectl exec -it deploy/myapp -- sh -c 'ls -l $AWS_WEB_IDENTITY_TOKEN_FILE && head -c 50 $AWS_WEB_IDENTITY_TOKEN_FILE'

여기까지 정상인데도 429라면, 거의 항상 “호출량/캐시/재시도” 문제입니다.

원인 패턴 5가지 (현장에서 가장 흔함)

1) 멀티프로세스 서버에서 크리덴셜 캐시가 프로세스마다 따로 돈다

예: gunicorn --workers 8이면, 8개 프로세스가 각자 STS를 치는 구조가 됩니다. 파드 50개면 동시에 400개의 프로세스가 토큰 교환을 시도할 수 있습니다.

2) AWS 클라이언트를 요청마다 새로 만든다

요청 핸들러에서 매번 boto3.client('s3') 같은 코드를 호출하면, 크리덴셜 로딩/갱신이 과도해질 수 있습니다(특히 여러 서비스 클라이언트를 매번 만들 때).

3) 짧은 시간에 파드가 대량 기동(롤링 배포/오토스케일)

모든 파드가 거의 동시에 “첫 STS 호출”을 하면서 콜드 스타트 폭주가 발생합니다.

4) 과도한 GetCallerIdentity/STS 호출을 헬스체크에 넣었다

liveness/readiness에서 STS를 직접 호출하면, 클러스터 전체가 지속적으로 STS를 두드립니다.

5) 재시도 정책이 공격적이거나(혹은 없음) 동시성 제어가 없다

429는 재시도를 해야 하지만, 지수 백오프 없이 즉시 재시도하면 스로틀링을 더 악화시킵니다. 반대로 재시도가 전혀 없으면 순간 스파이크에 바로 장애가 납니다.

해결 전략 개요: “STS 호출을 줄이고, 실패 시 우아하게 버틴다”

해결은 두 축으로 나뉩니다.

  1. 호출량 감소: 캐싱/클라이언트 재사용/프로세스 수 조절/배포 전략
  2. 스로틀링 내성: 적절한 재시도(지수 백오프+지터), 타임아웃, 서킷브레이커

아래부터는 우선순위대로 적용하기 쉬운 것부터 정리합니다.

1) 애플리케이션에서 AWS 클라이언트 “요청당 생성”을 없애기

Python(boto3) 예시: 전역/싱글톤으로 재사용

# bad: 요청마다 생성
import boto3

def handler(event, context):
    s3 = boto3.client("s3")
    return s3.list_buckets()

# good: 프로세스당 1회 생성(재사용)
import boto3

_s3 = boto3.client("s3")

def handler(event, context):
    return _s3.list_buckets()

주의: 멀티프로세스(gunicorn workers)에서는 “프로세스당 1개”이므로, 워커 수가 많으면 여전히 STS 호출이 많을 수 있습니다. 그래도 요청당 생성보다는 압도적으로 낫습니다.

Node.js(AWS SDK v3) 예시

// bad
import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";
export async function handler() {
  const s3 = new S3Client({});
  return await s3.send(new ListBucketsCommand({}));
}

// good
import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({});
export async function handler() {
  return await s3.send(new ListBucketsCommand({}));
}

2) 멀티프로세스/멀티컨테이너 동시성 줄이기 (가장 즉효)

  • gunicorn/uwsgi worker 수를 “CPU 기준 최대치”로 무작정 올리면 STS가 먼저 죽습니다.
  • sidecar(예: metrics/logging)에서 AWS SDK를 쓰는지 확인하세요. 사이드카도 IRSA를 사용하면 STS 호출 주체가 됩니다.

권장 접근:

  • 워커 수를 줄이고(예: 8 → 2~4) HPA로 파드를 늘리는 방식이 STS 관점에서 더 안정적일 때가 많습니다.
  • 파드 기동 시점에만 STS가 몰리면, maxSurge, maxUnavailable 조정으로 롤링 배포 동시성을 낮춥니다.

예: Deployment 롤링 업데이트 완화

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 10%
    maxUnavailable: 0

3) STS 재시도 정책을 “지수 백오프 + 지터”로 강제

429는 일시적일 때가 많아 재시도로 회복됩니다. 다만 재시도는 반드시 **지수 백오프(Exponential Backoff) + 지터(Jitter)**가 있어야 합니다.

Python(botocore)에서 adaptive retry 활성화

botocore는 retry mode를 지원합니다. 컨테이너 환경변수로 강제하는 게 가장 간단합니다.

env:
  - name: AWS_RETRY_MODE
    value: adaptive
  - name: AWS_MAX_ATTEMPTS
    value: "10"
  • adaptive는 클라이언트 측에서 스로틀링 신호를 반영해 속도를 조절합니다.
  • AWS_MAX_ATTEMPTS는 무작정 크게 하기보다, 서비스 특성/타임아웃과 함께 조정하세요.

Java SDK v2 예시(재시도 정책 명시)

import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.retry.RetryMode;
import software.amazon.awssdk.services.s3.S3Client;

S3Client s3 = S3Client.builder()
    .overrideConfiguration(
        ClientOverrideConfiguration.builder()
            .retryStrategy(RetryMode.ADAPTIVE)
            .build()
    )
    .build();

재시도 설계 전반(타임아웃, 폴백, 서킷브레이커) 관점은 아래 글의 패턴이 그대로 적용됩니다.

4) STS 호출이 “언제” 폭주하는지 분리: 콜드 스타트 vs 상시 폭주

A. 배포/스케일 시점에만 터진다

  • 원인: 동시에 많은 파드가 기동하며 첫 STS 교환을 수행
  • 대응:
    • 롤링 업데이트 동시성 낮추기
    • HPA scale-up 속도 제한(가능하면)
    • 애플리케이션 시작 시 AWS 호출(예: SSM 파라미터 로드)을 지연/캐시

B. 평소에도 지속적으로 터진다

  • 원인: 요청당 클라이언트 생성, STS를 헬스체크에서 호출, 워커 과다, 외부 라이브러리의 잦은 credential refresh 등
  • 대응:
    • 클라이언트 재사용
    • 헬스체크에서 AWS 호출 제거(단순 HTTP/DB ping 등으로 대체)
    • 워커/스레드 모델 재검토

5) IRSA 토큰/자격증명 갱신 주기 이해하기 (불필요한 갱신 방지)

IRSA에서 사용하는 웹 아이덴티티 토큰은 쿠버네티스가 회전(rotating)합니다. SDK는 임시 자격증명의 만료를 감지해 갱신합니다.

여기서 중요한 점:

  • 자격증명 캐시가 살아있어야 갱신이 최소화됩니다.
  • 컨테이너/프로세스가 자주 재시작되면 매번 “첫 AssumeRoleWithWebIdentity”가 발생합니다.

따라서:

  • CrashLoopBackOff가 있다면 먼저 안정화(재시작이 STS 폭주를 유발)
  • readiness 실패로 재기동이 반복되는지 확인

노드/파드 상태 점검이 필요하면 아래 글의 체크리스트가 도움이 됩니다.

6) (가능하면) STS 호출 자체를 줄이는 아키텍처 선택지

서비스 특성에 따라 아래 옵션이 “근본 해결”이 될 수 있습니다.

1) 역할(Role) 분리/통합 재검토

마이크로서비스마다 Role을 잘게 쪼개면 보안은 좋아지지만, 파드 수가 많을수록 STS 호출 주체가 늘어납니다.

  • 정말로 서비스마다 별도 Role이 필요한지
  • 동일 권한을 쓰는 워커/잡이 여러 개라면 Role을 통합할 수 있는지

2) AWS 서비스 호출을 중앙화(예: 내부 API로 프록시)

모든 파드가 S3/SSM/Secrets Manager를 직접 호출하는 대신, 내부에서 제한된 수의 “credential-holding service”로 집약하면 STS 호출 주체가 줄어듭니다.

  • 단점: 중앙 컴포넌트 장애 시 영향이 커짐
  • 장점: STS/QPS 제어가 쉬움, 캐시를 공유하기 쉬움

3) STS를 헬스체크/부트스트랩에 쓰지 않기

부트 시점에 SSM/Secrets Manager를 읽어 환경을 구성하는 패턴은 흔하지만, 대규모 롤아웃에서 STS/다운스트림 모두에 스파이크를 줍니다.

  • 가능한 값은 ConfigMap/Secret로 미리 주입
  • 꼭 필요하면 지연 로딩 + 캐시 + 백오프

실전 체크리스트: 30분 내 안정화 플랜

  1. 로그에서 429가 AssumeRoleWithWebIdentity인지 확인
  2. 요청당 AWS 클라이언트 생성 제거(가장 먼저)
  3. AWS_RETRY_MODE=adaptive, AWS_MAX_ATTEMPTS 적용
  4. gunicorn/worker 수 조정, 사이드카 포함 AWS SDK 사용 여부 점검
  5. 롤링 업데이트 동시성(maxSurge) 낮춰 콜드 스타트 폭주 완화
  6. 헬스체크에서 STS/AWS 호출 제거
  7. CloudTrail로 STS 이벤트 급증이 줄었는지 확인

트러블슈팅 예시: “IRSA는 되는데 ExternalSecret이 가끔 죽는다”

External Secrets Operator/ExternalSecret도 내부적으로 AWS SDK를 사용하며, 클러스터 규모가 크면 STS 스로틀링의 영향을 받습니다. IRSA/KMS/권한 문제와 스로틀링 문제는 증상이 섞여 보일 수 있으니, 아래 글의 진단 순서를 따라가면 분리하는 데 도움이 됩니다.

결론

EKS에서 “IRSA는 되는데 STS 429 Throttling”은 설정 오류라기보다 STS 토큰 교환 호출이 과도하게 발생하는 운영/구현 문제인 경우가 대부분입니다. 가장 효과적인 처방은 (1) AWS 클라이언트 재사용으로 STS 호출을 줄이고, (2) 적절한 재시도(지수 백오프+지터, adaptive retry)를 적용하며, (3) 배포/스케일 동시성을 낮춰 콜드 스타트 폭주를 막는 것입니다.

위 조치만으로도 대개 “간헐적 인증 장애”가 사라지고, STS 호출량이 안정 구간으로 내려옵니다. 그래도 지속된다면 Role 설계/중앙화 같은 아키텍처 레벨의 감소 전략을 검토하는 단계로 넘어가면 됩니다.