Published on

캐시가 갱신되지 않을때, SSM으로 EC2 접속해서 redis-cli 없이 디버깅한 사례

Authors

dev 환경에서 웹 카테고리 API의 응답이 어드민에서 데이터를 바꿔도 변하지 않는 이슈가 있었습니다.

응답 자체는 정상이고 에러도 없는데 내용만 예전 것이라, 캐시를 의심할 수밖에 없는 상황이었습니다.

로컬에서는 재현이 안되니 결국 서버에 들어가서 캐시를 직접 들여다봐야 했는데, 이 과정에서 소소한 함정을 몇개 밟았습니다.

같은 상황을 만날 분들을 위해 접속부터 캐시 삭제까지의 과정을 기록합니다.

참고로 캐시는 AWS ElastiCache Valkey Serverless(Redis 호환)를 사용중입니다.

1. SSM Session Manager로 EC2 접속

서버 접속은 SSH가 아니라 SSM Session Manager를 사용합니다. 포트를 열어둘 필요가 없고 IAM으로 접근을 통제할 수 있어서, 최근 인프라에서는 이쪽이 표준이 되었습니다.

AWS_ACCESS_KEY_ID=<> AWS_SECRET_ACCESS_KEY='<시크릿>' \
AWS_DEFAULT_REGION=ap-northeast-2 \
aws ssm start-session --target i-0xxxxxxxxxxxxxxxx

여기서 사소하지만 시간을 잡아먹었던 것 하나. zsh에서 백슬래시(\)로 줄바꿈할때 백슬래시 뒤에 공백이 하나라도 있으면 command not found가 납니다. 눈으로는 구분이 안되기 때문에, 명령어가 이유없이 실패하면 그냥 한 줄로 붙여서 실행하는게 정신건강에 좋습니다.

TargetNotConnected 에러가 나는 경우는 인스턴스의 SSM Agent가 연결되지 않은 상태이므로, aws ec2 describe-instance-status로 인스턴스 상태부터 확인합니다.

접속하면 ssm-user로 떨어지는데, 프로젝트 디렉토리 접근 권한이 없을 수 있어서 사용자를 전환했습니다.

sudo su - ec2-user
cd /home/ec2-user/project

2. 함정 1: venv가 아니라 .venv였다

Django shell을 띄우려고 가상환경을 활성화했는데,

source venv/bin/activate
python manage.py shell
# ModuleNotFoundError: No module named 'unfold'

멀쩡히 배포되어 돌아가는 서버에서 모듈이 없다고 합니다.

이럴때는 실제로 서비스를 돌리고 있는 프로세스가 어떤 파이썬을 쓰는지 확인하는게 가장 확실합니다.

# gunicorn PID 확인
lsof -i:8000

# 해당 프로세스의 실행 커맨드 확인
cat /proc/<gunicorn_pid>/cmdline | tr '\0' ' '
# /home/ec2-user/project/.venv/bin/python /home/ec2-user/project/.venv/bin/gunicorn ...

배포 환경은 venv가 아니라 .venv를 쓰고 있었습니다. 디렉토리에 둘 다 존재해서 생긴 함정이었는데, venv는 과거 배포 방식의 잔재였습니다.

source .venv/bin/activate
set -a && source .env && set +a && STAGE=dev python manage.py shell

/proc/<pid>/cmdline으로 실행 커맨드를 확인하는 방법은 이런 상황에서 두고두고 쓸 수 있어서 알아두면 좋습니다.

3. 함정 2: 서버에 redis-cli가 없다

캐시 서버에 접속하려고 보니 redis-cli가 설치되어 있지 않았습니다.

which redis-cli
# which: no redis-cli in (...)

설치해도 되지만, 어차피 Django가 캐시 서버와 통신하고 있으므로 Django shell을 통해서 접근하는 쪽이 더 빠릅니다. 설정에 정의된 커넥션을 그대로 쓰기 때문에 TLS나 인증 관련 설정을 다시 맞출 필요도 없습니다.

from django.core.cache import caches

cache = caches['global_cache']
client = cache.client.get_client()  # django-redis의 low-level 클라이언트
print(client)
# Redis<ConnectionPool<SSLConnection<host=...,port=6379,db=0>>>

이 client는 redis-py 클라이언트 그대로라서, redis-cli에서 하던 일을 거의 다 할 수 있습니다.

4. 함정 3: KEYS 명령이 막혀있다

카테고리 관련 키부터 찾아보려고 했는데,

client.keys('*categories*')
# ResponseError: unknown command 'keys'

Valkey Serverless에서는 KEYS 명령이 비활성화되어 있습니다.

KEYS는 전체 키스페이스를 블로킹으로 훑는 명령이라 운영 환경에서 원래도 쓰면 안되는 명령인데, 매니지드 서비스에서는 아예 막아둔 것 입니다. 덕분에(?) 강제로 올바른 방법을 쓰게 됩니다.

SCAN으로 커서를 옮겨가며 조회합니다.

cursor = 0
all_keys = []
while True:
    cursor, keys = client.scan(cursor=cursor, match='*categories*', count=100)
    all_keys.extend(keys)
    if cursor == 0:
        break

print(all_keys)
# [b'dev_global:1:categories:web:list:hash:xxxx:v3', ...]

키 목록을 보니 문제의 실마리도 같이 보였습니다. 같은 캐시에 dev_, staging_, prod_ prefix의 키가 전부 존재했고(하나의 인스턴스를 KEY_PREFIX로 논리 분리하는 구조), 버전 suffix(v3, v4, ...)가 붙은 구버전 키들이 다수 쌓여있었습니다.

5. 캐시 조회와 삭제

원인 파악 전에 일단 증상부터 해소해야 해서, 문제가 된 키를 조회하고 삭제했습니다.

캐시 매니저 클래스를 만들어둔 구조라면 shell에서 그대로 재사용할 수 있습니다.

from myapp.cache import get_category_cache_manager

manager = get_category_cache_manager()

# 지점별로 캐시가 존재하는지 훑기
for place_id in range(1, 20):
    data = manager.get_web_categories(place_id=place_id)
    if data is not None:
        print(f"place_id={place_id}: {len(data)}개 카테고리 캐시됨")

# 특정 지점 캐시만 삭제
manager.delete_web_categories(place_id=2)

# 삭제 확인
print(manager.get_web_categories(place_id=2))  # None이면 성공

전체 무효화가 필요하면 해당 네임스페이스의 버전만 올리는 방식(invalidate_all)을 사용했습니다. 버전 기반 무효화는 키를 지우는게 아니라 새 버전의 키를 바라보게 만드는 방식이라, 다른 캐시 영역에 영향을 주지 않습니다.

이런 조작을 운영 데이터가 있는 캐시에서 할때는, 삭제 전에 반드시 match 패턴으로 대상 키를 먼저 눈으로 확인하고 지우는 습관이 필요합니다.

6. 근본 원인은 설정 폴백이었다

증상을 해소하고 원인을 추적해보니, dev 서버의 Secret Manager에 캐시 호스트 환경변수(VALKEY_HOST)가 등록되어 있지 않았습니다.

설정 코드는 환경변수가 없으면 코드에 하드코딩된 기본값으로 폴백하는 구조였는데, 이 폴백 때문에 에러 한 줄 없이 의도와 다른 동작을 하고 있었던 것 입니다.

# 이런 형태의 폴백이 문제를 조용하게 만들었습니다
VALKEY_HOST = os.getenv("VALKEY_HOST", "default-host.example.com")

편의를 위한 기본값 폴백은 로컬 개발에서는 유용하지만, 서버 환경에서는 "설정이 누락되었다"는 사실 자체를 숨겨버립니다.

필수 설정은 차라리 없으면 부팅에 실패하도록 하는게 낫다는 교훈을 얻었습니다.

이번 조사에서 같이 발견된 것들(환경별 KEY_PREFIX 분리 구조, 버전 기반 무효화와 구버전 키 누적 문제 등)은 내용이 길어져서 다음 글에서 운영 노하우로 따로 정리하겠습니다.

디버깅 자체는 캐시 키 하나 지우는 일이었지만, .venv 함정, redis-cli 부재, KEYS 비활성화까지 겹치니 생각보다 험난한 길이 되었습니다.

이런 과정은 겪을 당시에 바로 문서로 남겨두는게 좋습니다. 다음에 같은 상황이 오면 이 글 하나로 접속부터 삭제까지 5분이면 끝날 것 입니다.

참고 자료

  • hongreat 블로그의 글을 봐주셔서 감사합니다!^^
  • 내용에 잘못된 부분이나 의문점이 있으시다면 댓글 부탁 & 환영 합니다~!
  • (하단의 버튼을 누르시면 댓글을 보거나 작성할 수 있습니다.)
Buy Me A Coffee