본 포스트는 Python 가상환경을 이용해 Flask서버를 Pm2 로 구동하던 중 Permission Error 가 발생한 문제에 대해서 다룹니다.
[문제 발생]
사내에서 운영하던 Data 처리용 서버에서 어느순간 특정 API에 대해 Error가 발생하는 것을 인지했습니다.
에러의 내용인 즉슨
별다른 코드 변경없이 구동 되던 Flask 서버에서
PermissionError: [Errno 13] Permission denied: 발생.
자세한 에러로그는

다음과 같이 특정 서비스에서 외부서버의 파일을 다운받아 처리하는 함수에서 발생했습니다.
코드는 다운로드 폴더를 확인하고, 폴더가 존재하면 그곳에 파일을 저장. 존재하지 않는다면 폴더를 생성하는 로직이었습니다.
(예시 구현)
current_directory = os.getcwd()
download_directory = current_directory + "/downloads"
if not (os.path.isdir(download_directory)):
os.makedirs(os.path.join(download_directory))
이 시점에서 /downloads 폴더는 이미 존재했고 chmod 는 775로 설정되어있는 상황이었습니다.
이에 chmod를 잠시 777로 변경해도 mkdir 명령은 실행되지 않았습니다.
[원인 추론]
정리할 수 있는 부분은
- mkdir 명령이 실행되었다는 것은 이미 디렉토리에 있는 폴더 역시 읽지 못한 것.
- 해당 서버의 재기동이 30일 가량 지났다는 것. (긴 시간 구동)
- pm2로 관리되고 있으며, pm2 restart 시 정상 동작 한다는 것.
이런 요소들과 검색을 통해 몇가지의 해결 방향을 확인하였습니다.
1. virtual env 가 root로 생성되어있지는 않은지 확인할 것 (근거)
많은 원인중 하나로 언급되는 것이 Python 의 sudo 여부였습니다.
실제로 저도 root 나 사용자간 권한 문제 때문에 애를 먹은 적이 많았는데요.
해당 경우 root가 아닌 사용자 계정으로 들어가 venv를 생성하고 정상적으로 구동하였습니다.
2. Python virtual environment의 Permission 변경 가능성 (근거)
이전 Python 보안 이슈중 하나로 멀티스레드 환경에서 umask 값을 임의로 변경하는 문제가 있었는데요.
https://github.com/python/cpython/issues/65281
해당 문제는 검색해보니
https://python-security.readthedocs.io/vuln/os-makedirs-not-thread-safe.html
CVE-2014-2667 로 버전 3.5 부터 해결된 사항이었습니다. (현재 버전은 3.10)
추가적으로 가상환경을 새롭게 활성화 시켰을 경우 정상적으로 동작하는 것을 보아 이 원인도 넘기는 것으로 하였습니다.
3. pm2의 오랜 구동으로 인해 os 경로에 영향 및 권한 설정에 문제 (Process Environment Drift)
재시작시 정상적으로 구동되는 서버. 그리고 오류 당시 존재하는 폴더를 읽지 못한 이슈.. 등등을 고려했을 때,
원인은 PM2로 오랜시간 구동하는 경우 내부 os 경로값이 변경되었을 가능성을 놓을 수 없었습니다.
기존 디렉토리가 아닌 pm2 가 설치된 디렉토리 혹은 전혀 다른 디렉토리를 바라보고 있었다면 downloads 폴더를 읽지 못한것도, 새로운 폴더를 생성하지 못한것도 어느정도 설명이 되기 때문입니다.
다만, 바로 확인해볼 수 없었던 것은 운영상 장애로 인해 서버 재시작을 빠르게 구동하여 현재 이슈 재현이 불가능했습니다.
따라서 해당 3번 이슈에 대해서는 예방방법을 구현하기로 했습니다.
[예방 방법 구현]
Drift 문제로 인해 환경이 변경되는 것을 가정하고 해당 시점에 변경된 환경들을 기록하고자 하였습니다.
파악할 내용은 현재 OS의 CWD, UMASK 값을 중심으로 추가적인 정보들을 수집하는 것을 목표로 잡았습니다.
수집 시점은 API가 호출되었을 경우 상태를 로깅하고, 이 로깅을 통해 3번 문제가 실제로 문제가 되는것을 파악하면 cron등을 통해 이상이 파악될 경우 자동 재시작하는 방법을 고려하고자 합니다.
먼저 로깅의 예시 코드입니다.
import os
import time
import logging
def log_environment_state():
logging.info(f"CWD: {os.getcwd()}")
logging.info(f"UID/GID: {os.getuid()}/{os.getgid()}")
logging.info(f"PID: {os.getpid()}")
# umask
current_umask = os.umask(0o022)
os.umask(current_umask)
logging.info(f"umask: {oct(current_umask)}")
@app.before_request
def check_permissions():
try:
os.listdir('.')
# 파일 생성 테스트
with open('/tmp/flask_permission_test', 'w') as f:
f.write(str(time.time()))
os.remove('/tmp/flask_permission_test')
except PermissionError as e:
logging.error(f"Permission error detected: {e}")
log_environment_state()
혹 주기적인 모니터링이 필요한 경우
import os
import time
import threading
import logging
from datetime import datetime
class EnvironmentMonitor:
def __init__(self, check_interval=3600): # 1시간마다 체크
self.check_interval = check_interval
self.initial_umask = None
self.initial_env = {}
self.monitoring = False
def start_monitoring(self):
"""환경 모니터링 시작"""
# 초기 환경 저장
self.initial_umask = os.umask(0o022)
os.umask(self.initial_umask)
self.initial_env = {
'PWD': os.getcwd(),
'USER': os.getenv('USER'),
'PATH': os.getenv('PATH')
}
self.monitoring = True
monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
monitor_thread.start()
logging.info(f"Environment monitoring started. Initial umask: {oct(self.initial_umask)}")
def _monitor_loop(self):
"""모니터링 루프"""
while self.monitoring:
try:
self._check_environment()
time.sleep(self.check_interval)
except Exception as e:
logging.error(f"Environment monitoring error: {e}")
time.sleep(60) # 오류 시 1분 후 재시도
def _check_environment(self):
"""환경 변화 체크"""
# umask 체크
current_umask = os.umask(0o022)
os.umask(current_umask)
if current_umask != self.initial_umask:
logging.warning(f"Umask changed! Initial: {oct(self.initial_umask)}, Current: {oct(current_umask)}")
# 필요시 원래 umask로 복구
os.umask(self.initial_umask)
# 작업 디렉토리 체크
current_cwd = os.getcwd()
if current_cwd != self.initial_env['PWD']:
logging.warning(f"Working directory changed! Initial: {self.initial_env['PWD']}, Current: {current_cwd}")
# 환경 변수 체크
for key, initial_value in self.initial_env.items():
current_value = os.getenv(key)
if current_value != initial_value:
logging.warning(f"Environment variable {key} changed! Initial: {initial_value}, Current: {current_value}")
# Flask 앱에서 사용
monitor = EnvironmentMonitor(check_interval=1800) # 30분마다 체크
@app.before_first_request
def setup_monitoring():
monitor.start_monitoring()
[결과]
이후 문제가 발견되었을 경우 해당 문서는 업데이트하여 진행하고자 합니다.
'개발 이슈,해결법' 카테고리의 다른 글
| [Xtrabackup] DB 백업본으로 DB 데이터 유실 확인하기 - Docker활용 (0) | 2025.11.03 |
|---|---|
| [AI code review] AI 코드리뷰 도입기 (with Gemini) (0) | 2025.09.20 |
| [Mysql 8.0] Mysql Lock 확인 (0) | 2024.08.30 |
| BE 와 FE 는 어떻게 협업해야 할까. (0) | 2023.05.01 |
| [JAXB] XSD to java Code (xsd , java object변환) (0) | 2023.02.09 |