Conversation
develop 중심 CI와 OIDC EB 배포(스모크·롤백)를 도입.
ci: BE 패턴 기반 FastAPI CI/CD 정비
There was a problem hiding this comment.
Code Review
This pull request introduces a deployment issue template, a CI/CD OIDC setup guide, a runtime specification, and an infrastructure smoke test script for PostgreSQL, Redis, and S3. The review feedback highlights several critical issues: the specified Python 3.14 version is unsupported on AWS Elastic Beanstalk; dotenv_values does not load environment variables into os.environ, which will cause database connection failures; and requiring static AWS credentials in the smoke test script will break OIDC-based authentication. Suggestions are provided to use load_dotenv, simplify the environment variable retrieval helper, and make AWS keys optional to support OIDC.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| @@ -0,0 +1 @@ | |||
| python-3.14 | |||
|
|
||
| import asyncpg | ||
| import boto3 | ||
| from dotenv import dotenv_values |
There was a problem hiding this comment.
| def get_env_value(env: dict[str, str | None], *keys: str, required: bool = False) -> str | None: | ||
| for key in keys: | ||
| value = env.get(key) or os.getenv(key) | ||
| if value: | ||
| return value | ||
| if required: | ||
| joined_keys = ", ".join(keys) | ||
| raise RuntimeError(f"Missing required env key: {joined_keys}") | ||
| return None |
There was a problem hiding this comment.
load_dotenv를 사용해 환경 변수를 프로세스에 직접 로드하면, get_env_value 함수에서 env 딕셔너리를 매개변수로 전달받을 필요 없이 os.getenv를 통해 일관되게 값을 조회할 수 있습니다. 함수 시그니처를 단순화하여 가독성을 높일 수 있습니다.
| def get_env_value(env: dict[str, str | None], *keys: str, required: bool = False) -> str | None: | |
| for key in keys: | |
| value = env.get(key) or os.getenv(key) | |
| if value: | |
| return value | |
| if required: | |
| joined_keys = ", ".join(keys) | |
| raise RuntimeError(f"Missing required env key: {joined_keys}") | |
| return None | |
| def get_env_value(*keys: str, required: bool = False) -> str | None: | |
| for key in keys: | |
| value = os.getenv(key) | |
| if value: | |
| return value | |
| if required: | |
| joined_keys = ", ".join(keys) | |
| raise RuntimeError(f"Missing required env key: {joined_keys}") | |
| return None |
| async def check_postgres() -> None: | ||
| conn = await asyncpg.connect( | ||
| user=os.getenv("LOCAL_DB_USER", "postgres"), | ||
| password=os.getenv("LOCAL_DB_PASSWORD", "postgres"), | ||
| database=os.getenv("LOCAL_DB_NAME", "app"), | ||
| host=os.getenv("LOCAL_DB_HOST", "127.0.0.1"), | ||
| port=int(os.getenv("LOCAL_DB_PORT", "5432")), | ||
| ) |
There was a problem hiding this comment.
check_postgres에서도 os.getenv 대신 get_env_value를 사용하도록 수정하여 일관성을 유지하고, 환경 변수가 비어 있거나 누락되었을 때의 기본값 처리를 안전하게 수행할 수 있도록 개선합니다.
| async def check_postgres() -> None: | |
| conn = await asyncpg.connect( | |
| user=os.getenv("LOCAL_DB_USER", "postgres"), | |
| password=os.getenv("LOCAL_DB_PASSWORD", "postgres"), | |
| database=os.getenv("LOCAL_DB_NAME", "app"), | |
| host=os.getenv("LOCAL_DB_HOST", "127.0.0.1"), | |
| port=int(os.getenv("LOCAL_DB_PORT", "5432")), | |
| ) | |
| async def check_postgres() -> None: | |
| conn = await asyncpg.connect( | |
| user=get_env_value("LOCAL_DB_USER") or "postgres", | |
| password=get_env_value("LOCAL_DB_PASSWORD") or "postgres", | |
| database=get_env_value("LOCAL_DB_NAME") or "app", | |
| host=get_env_value("LOCAL_DB_HOST") or "127.0.0.1", | |
| port=int(get_env_value("LOCAL_DB_PORT") or "5432"), | |
| ) |
| def check_redis(env: dict[str, str | None]) -> None: | ||
| redis_host = get_env_value(env, "REDIS_LOCAL_HOST", "LOCAL_REDIS_HOST") or "localhost" | ||
| redis_port = int(get_env_value(env, "REDIS_LOCAL_PORT", "LOCAL_REDIS_PORT") or "6379") | ||
| redis_client = Redis(host=redis_host, port=redis_port, db=0, decode_responses=True) | ||
| assert redis_client.ping() is True |
There was a problem hiding this comment.
check_redis 함수에서 불필요해진 env 매개변수를 제거하고, 단순화된 get_env_value를 사용하도록 수정합니다.
| def check_redis(env: dict[str, str | None]) -> None: | |
| redis_host = get_env_value(env, "REDIS_LOCAL_HOST", "LOCAL_REDIS_HOST") or "localhost" | |
| redis_port = int(get_env_value(env, "REDIS_LOCAL_PORT", "LOCAL_REDIS_PORT") or "6379") | |
| redis_client = Redis(host=redis_host, port=redis_port, db=0, decode_responses=True) | |
| assert redis_client.ping() is True | |
| def check_redis() -> None: | |
| redis_host = get_env_value("REDIS_LOCAL_HOST", "LOCAL_REDIS_HOST") or "localhost" | |
| redis_port = int(get_env_value("REDIS_LOCAL_PORT", "LOCAL_REDIS_PORT") or "6379") | |
| redis_client = Redis(host=redis_host, port=redis_port, db=0, decode_responses=True) | |
| assert redis_client.ping() is True |
| def check_s3(env: dict[str, str | None]) -> None: | ||
| aws_access_key = get_env_value(env, "AWS_ACCESS_KEY", required=True) | ||
| aws_secret_key = get_env_value(env, "AWS_SECRET_KEY", required=True) | ||
| aws_region = get_env_value(env, "AWS_REGION", required=True) | ||
| aws_bucket = get_env_value(env, "AWS_BUCKET", required=True) | ||
|
|
||
| s3 = boto3.client( | ||
| "s3", | ||
| aws_access_key_id=aws_access_key, | ||
| aws_secret_access_key=aws_secret_key, | ||
| region_name=aws_region, | ||
| ) | ||
| s3.head_bucket(Bucket=aws_bucket) | ||
| key = f"ci-smoke/{uuid.uuid4().hex}.txt" | ||
| body = b"ci-smoke-test" | ||
| s3.put_object(Bucket=aws_bucket, Key=key, Body=body, ContentType="text/plain") | ||
| obj = s3.get_object(Bucket=aws_bucket, Key=key) | ||
| assert obj["Body"].read() == body | ||
| s3.delete_object(Bucket=aws_bucket, Key=key) |
There was a problem hiding this comment.
OIDC 기반 인증을 도입하는 경우, GitHub Actions 환경에서는 정적 AWS_ACCESS_KEY 및 AWS_SECRET_KEY 대신 임시 자격 증명(OIDC/IAM Role)을 사용하게 됩니다. AWS_ACCESS_KEY와 AWS_SECRET_KEY를 필수(required=True)로 요구하면 OIDC 환경에서 스모크 테스트가 실패하게 됩니다.
따라서 해당 키들을 선택 사항으로 변경하고, 제공되지 않았을 때는 boto3가 기본 자격 증명 체인(OIDC 환경 변수 등)을 통해 자동으로 인증하도록 수정해야 합니다.
| def check_s3(env: dict[str, str | None]) -> None: | |
| aws_access_key = get_env_value(env, "AWS_ACCESS_KEY", required=True) | |
| aws_secret_key = get_env_value(env, "AWS_SECRET_KEY", required=True) | |
| aws_region = get_env_value(env, "AWS_REGION", required=True) | |
| aws_bucket = get_env_value(env, "AWS_BUCKET", required=True) | |
| s3 = boto3.client( | |
| "s3", | |
| aws_access_key_id=aws_access_key, | |
| aws_secret_access_key=aws_secret_key, | |
| region_name=aws_region, | |
| ) | |
| s3.head_bucket(Bucket=aws_bucket) | |
| key = f"ci-smoke/{uuid.uuid4().hex}.txt" | |
| body = b"ci-smoke-test" | |
| s3.put_object(Bucket=aws_bucket, Key=key, Body=body, ContentType="text/plain") | |
| obj = s3.get_object(Bucket=aws_bucket, Key=key) | |
| assert obj["Body"].read() == body | |
| s3.delete_object(Bucket=aws_bucket, Key=key) | |
| def check_s3() -> None: | |
| aws_access_key = get_env_value("AWS_ACCESS_KEY") | |
| aws_secret_key = get_env_value("AWS_SECRET_KEY") | |
| aws_region = get_env_value("AWS_REGION", required=True) | |
| aws_bucket = get_env_value("AWS_BUCKET", required=True) | |
| client_kwargs = {"region_name": aws_region} | |
| if aws_access_key and aws_secret_key: | |
| client_kwargs["aws_access_key_id"] = aws_access_key | |
| client_kwargs["aws_secret_access_key"] = aws_secret_key | |
| s3 = boto3.client("s3", **client_kwargs) | |
| s3.head_bucket(Bucket=aws_bucket) | |
| key = f"ci-smoke/{uuid.uuid4().hex}.txt" | |
| body = b"ci-smoke-test" | |
| s3.put_object(Bucket=aws_bucket, Key=key, Body=body, ContentType="text/plain") | |
| obj = s3.get_object(Bucket=aws_bucket, Key=key) | |
| assert obj["Body"].read() == body | |
| s3.delete_object(Bucket=aws_bucket, Key=key) |
| def main() -> int: | ||
| env = dotenv_values(".env") | ||
| asyncio.run(check_postgres()) | ||
| check_redis(env) | ||
| check_s3(env) | ||
| print("infra smoke OK") | ||
| return 0 |
There was a problem hiding this comment.
main 함수에서 dotenv_values 대신 load_dotenv를 호출하고, env 딕셔너리를 전달하지 않도록 단순화된 함수들을 호출하도록 수정합니다.
| def main() -> int: | |
| env = dotenv_values(".env") | |
| asyncio.run(check_postgres()) | |
| check_redis(env) | |
| check_s3(env) | |
| print("infra smoke OK") | |
| return 0 | |
| def main() -> int: | |
| load_dotenv() | |
| asyncio.run(check_postgres()) | |
| check_redis() | |
| check_s3() | |
| print("infra smoke OK") | |
| return 0 |
|
|
||
| ## EB Python 플랫폼 | ||
|
|
||
| `runtime.txt`에 `python-3.14`를 명시했습니다. dev/prod EB 환경이 Python 3.14 플랫폼을 지원하는지 확인하고, 미지원 시 플랫폼 업그레이드를 선행하세요. |
There was a problem hiding this comment.
가이드 문서에 언급된 Python 버전도 python-3.14 대신 실제 사용 중인 Python 버전(예: python-3.11 또는 python-3.12)으로 수정하여 혼선을 방지해 주세요.
| `runtime.txt`에 `python-3.14`를 명시했습니다. dev/prod EB 환경이 Python 3.14 플랫폼을 지원하는지 확인하고, 미지원 시 플랫폼 업그레이드를 선행하세요. | |
| runtime.txt에 python-3.11을 명시했습니다. dev/prod EB 환경이 Python 3.11 플랫폼을 지원하는지 확인하고, 미지원 시 플랫폼 업그레이드를 선행하세요. |
✨ 작업 개요
체크리스트
Issueissyu Prod CI/CDgreen/health200 확인