2026. 5. 28. 23:02ㆍ프로젝트
Flicker 사이드 프로젝트를 GitHub Actions로 자동 배포하면서, SSH 키 관리하기가 불편했다.
EC2에서 22번 포트 열어두는 것도 찜찜하고, GitHub Secrets에 private key 통째로 박는 것도 그렇고.
그래서 이번에 다 갈아엎으면서 두 가지를 시도했다.
- AWS 엑세스 키 대신 GitHub OIDC로 임시 자격증명 받기
- EC2 SSH 대신 SSM(Session Manager) 으로 명령 전송
결과적으로 키를 따로 관리하지 않아도 됐고, EC2 보안 그룹에서도 22번 포트를 닫을 수 있게 되었다.
왜 OIDC인가?
*OIDC(OpenIDConnect)는 토큰 기반 인증이다 : ID Token(JWT)
JWT 안에는 개념적으로 repo, 브랜치, workflow 같은 정보가 들어있다.
[AS-IS]
- IAM 사용자 만들고 Key를 생성해 GitHub Secrets에 ACCESS_KEY, SECRET_KEY 두 개를 갖고 있음.
동작은 하는데 마음에 안 드는 점이 있었다.
이 키들은 영구라서, 유출되면 내가 무효화하기 전까지 누군가 계속 쓸 수 있게 된다. Rotation도 수동이라 한 번 만들어두고 까먹기 쉽다.
실제로 이전 프로젝트에서, 키를 yaml에 평문으로 박아놓고 쓴 적도 있었다. (다행히 private 리포라 노출은 안 됐지만)
[TO-BE]
OIDC는 이걸 임시 토큰 방식으로 바꾼다.
GitHub Actions가 실행될 때마다 GitHub가 JWT를 발급하고, AWS STS가 검증한 다음 1시간짜리 임시 자격증명을 준다.
워크플로우가 끝나면 만료. GitHub Secrets에는 Role ARN만 남는데, 이건 비밀이 아닌 식별자라서 노출돼도 괜찮다.
OIDC 셋업 - 핵심은 Trust Policy
1. Identity Provider 등록 IAM → Identity providers → Add provider
- Type: OpenID Connect
- URL: https://token.actions.githubusercontent.com
- Audience: sts.amazonaws.com
2. IAM Role 생성 후 Trust Policy 추가 (JSON 형식)
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:sns-service/flicker-server:*"
}
}
sub의 형식이 repo:<org>/<repo>:<context>라는 걸 알면 이 조건의 보안 의미가 명확해진다.
다른 사람의 리포에서 토큰을 위조해도 "repo"가 내 리포 이름이랑 일치하지 않으면 거부된다. 그래서 와일드카드를 써도 안전하다.
워크플로우에서 OIDC 사용
permissions:
id-token: write # 이거 없으면 OIDC 토큰 발급 안 됨
contents: read
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-2
permissions.id-token: write를 까먹으면 "Could not assume role" 에러가 뜨는데, 에러 메시지만 봐서는 권한 문제인지 trust policy 문제인지 헷갈린다. (30분 날렸다)
워크플로우 최상단에 이 두 줄을 박아두는 게 안전하다.
SSH 대신 SSM
여기까지 OIDC로 ECR 푸시까지는 잘 됐다.
즉 기존에 GitHub Actions 가 AWS 에 접근할 때 키 2개를 저장/관리해야 했던 문제를 해결했다.
다음은 EC2에 들어가서 Docker 컨테이너 교체하는 단계인데, 보통은 SSH로 한다.
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ...
그런데 SSH를 사용하게 되면 SSH Private Key를 또 우리가 들고 있어야 해서 반쪽짜리 개선이다. (OIDC로 키 줄였더니 SSH 키로 다시 키를 만드는 셈이다.)
게다가 가장 큰 문제는 GitHub Actions 러너는 임시 VM에서 실행되니까, IP 가 항상 바뀐다.
SSH 접속을 허용하려면 EC2 보안 그룹에서 Runner IP를 열어야 하는데, IP가 계속 바뀌다 보니 현실적으로 0.0.0.0/0 으로 열어두는 경우가 많다. 그렇다면 사실상 0.0.0.0/0 (전 세계 어디서든 22번 포트 접근 가능) -> 보안 상 좋지 않다!
반면 SSM은 반대다.
EC2 내부의 SSM Agent 가 먼저 AWS SSM 서버로 아웃바운드 연결을 유지한다.
GitHub Actions 에서는 EC2에 직접 SSH 접속하는 대신, AWS API(SSM Send Command)를 호출해 "이 인스턴스에서 이 명령을 실행해줘" 라고 요청만 보낸다.
실제 명령 실행은 이미 AWS와 연결돼 있던 SSM Agent가 수행한다.
즉,
- EC2 인바운드 22 포트 불필요
- SSH Key 관리 불필요
- Security Group 에서 SSH 완전 제거 가능
SSH, SSM 방식 차이점
1. SSH 방식
GitHub Runner ---> EC2
(직접 접속)
2. SSM 방식
EC2 ---> AWS SSM
(먼저 outbound 연결 유지)
GitHub Actions ---> AWS API
그래서 보안 관점에서, 인터넷에 SSH 포트를 노출하지 않아도 된다는 장점이 있음.
워크플로우 (dpeloy.yml)
- name: Send deployment command via SSM
id: ssm-deploy
env:
ECR_REGISTRY: ${{ needs.build-and-push.outputs.ecr-registry }}
IMAGE_TAG: ${{ needs.build-and-push.outputs.image-tag }}
run: |
COMMAND_ID=$(aws ssm send-command \
--instance-ids "${{ secrets.EC2_INSTANCE_ID }}" \
--document-name "AWS-RunShellScript" \
--comment "Deploy flicker ${IMAGE_TAG}" \
--region ${{ env.AWS_REGION }} \
--parameters '{
"commands": [
"set -e",
"export REGISTRY='"${ECR_REGISTRY}"'",
"export REPO='"${{ env.ECR_REPOSITORY }}"'",
"export TAG='"${IMAGE_TAG}"'",
결론
- GitHub Secrets에 남은 비밀값: DB 비밀번호, JWT 키만 (Access Key, SSH key 사라짐)
- EC2 보안 그룹: 22번 포트 닫음, 8080만 인바운드 허용
- 배포 흐름: main에 push -> 약 1m 40초 후 자동 배포 완료
- 자격증명 형태: 영구 키 -> 1시간짜리 임시 자격증명
수동 배포 시절 (로컬 빌드 → 도커 허브 푸시 → SSH 접속 → docker pull → run) 대비 사람 손이 들어갈 일이 없어졌다.
이전엔 SSH로 들어가서 docker ps 보고 안 되면 직접 손보는 식이었다면, 지금은 SSM 로그를 GitHub Actions UI에서 본다.
-> 또한 이력이 CloudTrail에 자동 기록되서 더 편하게 쓸 수 있을 듯?
남은 숙제
지금 구조의 한계도 명확하다.
- 컨테이너 교체 시 잠깐 다운타임이 있다 (docker rm -f → docker run). 블루/그린이나 롤링 배포로 가야 0초가 된다.
- Redis가 같은 EC2 안 도커 컨테이너로 돈다. ElastiCache 비용 절감 목적인데, 그래서 EC2 stop/start 한 번 했다가 RefreshToken 다 날아간 적 있다. 컨테이너에 -v 볼륨 마운트랑 AOF를 켜서 EBS 영속화로 우회했다.
- 인프라가 다 콘솔 클릭으로 만들어졌다. Terraform으로 코드화하는 게 다음 차례다.
블루/그린이랑 Terraform 작업하면서 또 정리할 거 생기면 적어두려 한다.
'프로젝트' 카테고리의 다른 글
| [IaC] Terraform으로 인프라 코드화하기 (0) | 2026.06.13 |
|---|---|
| 사이드 프로젝트에 블루/그린 배포 붙여보기 — 9초의 벽 (0) | 2026.06.03 |
| [프로젝트] 캐시를 통한 좋아요, 팔로우 로직 최적화 (1) | 2024.08.10 |
| [프로젝트] Pagination 으로 피드 랜덤조회 개선 (0) | 2024.08.08 |
| [프로젝트] 레디스 Pub/Sub, RabbitMQ 도입으로 구조 개선 (0) | 2024.06.18 |