Skip to main content

Kamal + SSM으로 GitHub Actions에서 EC2에 배포하기

· 4 min read

이전 글에서 EC2를 public subnet에 두더라도 올바르게 구성하면 안전하다고 했다. 그 구성에서는 보안 그룹 인바운드에 CloudFront prefix list의 443만 허용하고, 관리 접속은 SSM Session Manager로 처리한다. SSH 포트는 열려 있지 않다.

하지만 Kamal은 SSH로 EC2에 접속해서 Docker 명령어를 실행한다. SSH 포트 없이 Kamal 배포를 어떻게 할 수 있을까. 이 글은 OIDC, SSM, 일회성 SSH 키를 조합해서 이 문제를 해결하는 방법을 다룬다.

해결 방법

기존 배포에 필요한 세 가지를 각각 다른 방식으로 대체한다.

Access Key/Secret Key 대신 OIDC

GitHub Actions에서 AWS에 접근하려면 자격증명이 필요하다. AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY를 secrets에 저장하는 방식이 흔하지만, 유출 위험과 교체 부담이 따른다.

OIDC(OpenID Connect)를 사용하면 워크플로우 실행마다 만료되는 임시 자격증명을 발급받는다. GitHub이 OIDC 토큰을 발행하고, AWS STS에 제출해서 임시 Access Key, Secret Key, Session Token을 받는 방식이다. 장기 자격증명이 아예 존재하지 않으므로 유출될 것도 없다. Trust Policy에서 특정 저장소와 브랜치만 Role을 assume할 수 있도록 제한하면, 권한 범위도 통제된다.

SSH 포트 대신 SSM Session Manager

SSM Agent가 Systems Manager로 연결을 시작하므로, 인바운드 22번 포트를 열 필요가 없다. 세션이 시작되면 Session Manager가 Agent에 양방향 연결을 열도록 요청하고, 이 연결을 통해 통신한다. 인증은 IAM으로 처리되고, 모든 세션은 CloudTrail에 기록된다.

키 관리 대신 일회성 SSH 키

EC2 Instance Connect를 사용하면 SSH 키를 서버에 미리 등록해 둘 필요가 없다. 워크플로우가 실행될 때 새 ed25519 키쌍을 생성하고, SendSSHPublicKey API로 공개키를 EC2 인스턴스 메타데이터에 등록한다. 이 키는 60초간만 유효하다. 비밀키는 워크플로우 안에서만 존재하며, 종료 시 shred로 완전 삭제한다.

proxy_command

Kamal의 proxy_command 설정이 위의 세 가지를 하나의 배포 흐름으로 연결한다.

ssh:
user: ubuntu
proxy_command: >-
aws ec2-instance-connect send-ssh-public-key
--instance-id %h
--instance-os-user ubuntu
--ssh-public-key file://~/.ssh/id_ed25519.pub > /dev/null &&
aws ssm start-session
--target %h
--document-name AWS-StartSSHSession
--parameters 'portNumber=22'

먼저 send-ssh-public-key가 일회성 공개키를 EC2 인스턴스 메타데이터에 등록한다. 이어서 ssm start-session이 SSM 터널을 통해 SSH 연결을 연다. Kamal은 이 터널을 일반 SSH처럼 사용해서 Docker 명령어를 실행한다.

GitHub ActionsKamalSSMSession ManagerEC2SSM Agent포트 22 닫힘아웃바운드 연결
초기 상태
SSM Agent가 Systems Manager로의 연결을 시작하므로, EC2에 인바운드 포트를 열 필요가 없다.

servers에는 IP가 아닌 instance-id를 넣는다. SSH가 연결을 시도할 때 %h에 이 값이 들어가므로, send-ssh-public-keyssm start-session 모두 같은 인스턴스를 대상으로 동작한다.

x-instance-id: &instance-id <%= ENV.fetch("INSTANCE_ID") %>

servers:
web:
- *instance-id

instance-id는 ERB로 환경변수에서 읽는다. 파일에 하드코딩하지 않으므로, 같은 deploy.yml을 로컬 배포와 CI/CD에서 공유할 수 있다.

GitHub Actions 워크플로우

  1. OIDC 인증aws-actions/configure-aws-credentials로 임시 자격증명을 발급받는다.
  2. Session Manager 플러그인 설치ubuntu-latest runner에는 기본 설치되어 있지 않다.
  3. 일회성 SSH 키쌍 생성ssh-keygen -t ed25519. 공개키 등록은 Kamal이 SSH 연결을 시도할 때 proxy_command에서 처리된다.
  4. Kamal 배포 — GHCR에 이미지를 push하고 kamal deploy를 실행한다.
  5. SSH 키 삭제if: always()로 성공/실패와 무관하게 shred로 비밀키를 삭제한다.

워크플로우는 concurrency 그룹으로 동시 배포를 방지한다. cancel-in-progress: false로 설정해서 진행 중인 배포를 취소하지 않는다.

GitHub Actions 설정값은 민감도에 따라 구분한다. AWS_ROLE_ARN은 계정 ID를 포함하므로 secret으로, AWS_REGIONINSTANCE_ID는 민감하지 않으므로 variable로 관리한다.

사전 조건

이 구성이 동작하려면 EC2에 SSM Agent가 실행 중이어야 하고, IAM Instance Profile에 AmazonSSMManagedInstanceCore 정책이 연결되어 있어야 한다. Amazon Linux 2023과 Ubuntu의 최신 AMI에는 SSM Agent가 기본 설치된다.

GitHub Actions에서 assume할 IAM Role에는 SendSSHPublicKey, ssm:StartSession, ssmmessages:* 권한이 필요하다. Trust Policy에서 저장소와 브랜치를 제한하는 것도 중요하다.

구체적인 IAM 정책과 CDK 구성은 kamal-ssm-deploy 레포지토리에서 확인할 수 있다.


SSM이 SSH 포트를, OIDC가 장기 자격증명을, 일회성 키가 키 관리를 대체한다. proxy_command가 이 세 가지를 Kamal의 배포 흐름으로 연결한다. 이렇게 하면 외부에서 EC2로 직접 접근할 수 있는 포트 없이, 완전 자동화된 배포 파이프라인이 만들어진다.