Table of contents
Open Table of contents
1. 배경
AWS CodeDeploy를 EC2가 아닌 온프레미스 서버에서 사용할 때, 가장 먼저 부딪히는 문제가 AWS 인증(credentials) 관리입니다.
EC2는 Instance Profile이 있어서 IAM Role을 자동으로 부여받지만, 온프레미스 서버는 그런 메커니즘이 없습니다. 그래서 AWS 공식 문서에서는 아래 방식을 권장합니다.
- IAM Users를 생성하고 Access Key를 발급 또는 IAM Role 생성
- STS Assume Role로 임시 credentials를 발급
- Cron으로 주기적으로 갱신
- credentials 갱신 후 CodeDeploy Agent 재기동
이 방식은 동작은 하지만 몇 가지 문제가 있습니다.
- 배포 중 재기동 리스크: Cron이 실행되는 타이밍에 배포가 진행 중이라면 Agent가 재기동되면서 배포가 중단될 수 있습니다.
- 운영 복잡도: Cron 스크립트, IAM User, 재기동 로직을 별도로 관리해야 합니다.
- 장애 포인트 증가: Cron 미실행, 스크립트 오류 등으로 인한 credentials 만료 장애가 발생할 수 있습니다.
그래서 재기동 없이 credentials를 자동 갱신할 수 있는 방법을 찾아보았습니다.
2. SSM Hybrid Activation과 CodeDeploy Agent
AWS Systems Manager(SSM)의 Hybrid Activation을 사용하면 온프레미스 서버를 SSM Managed Node로 등록할 수 있습니다. 등록된 서버에서 amazon-ssm-agent는 임시 credentials를 발급받아 /root/.aws/credentials에 저장하고, 30분마다 자동으로 갱신합니다.
# /root/.aws/credentials
[default]
aws_access_key_id = ASIA...
aws_secret_access_key = xxxxxx
aws_session_token = xxxxxx
CodeDeploy Agent도 /etc/codedeploy-agent/conf/codedeploy.onpresmises.yml 파일에 credentials 경로를 지정하면 지정된 자격증명을 읽어 AWS API를 호출합니다.
# /etc/codedeploy-agent/conf/codedeploy.onpremises.yml
---
iam_session_arn: arn:aws:sts::{ACCOUNT}:assumed-role/{ROLE}/{NAME}
aws_credentials_file: /root/.aws/credentials
region: ap-northeast-2
그렇다면 SSM Agent가 이미 30분마다 credentials를 갱신해주고 있으니, CodeDeploy Agent도 갱신된 값을 자연스럽게 읽어야 하지 않을까요?
실제로 테스트해보면 SSM Agent가 파일을 갱신해도 CodeDeploy Agent는 여전히 예전 credentials를 물고 있어서 ExpiredTokenException이 발생합니다.
3. 문제: credentials 캐시
원인을 파악하기 위해 CodeDeploy Agent 소스를 분석했습니다.
CodeDeploy Agent는 내부적으로 Aws::SharedCredentials를 통해 credentials를 로딩하고, Aws::RefreshingCredentials 모듈을 mixin해서 자동 갱신 로직을 구현합니다.
# /opt/codedeploy-agent/lib/instance_agent/file_credentials.rb
module InstanceAgent
class FileCredentials
include Aws::CredentialProvider
include Aws::RefreshingCredentials
def initialize(path)
@path = path
super()
end
private
def refresh
@credentials = Aws::SharedCredentials.new(path: @path).credentials
@expiration = Time.now + 1800 # 30분
end
end
end
RefreshingCredentials 모듈은 credentials가 호출될 때마다 만료 여부를 체크하고, 만료 5분 전이면 refresh()를 자동 호출합니다.
def near_expiration?
if @expiration
(Time.now.to_i + 5 * 60) > @expiration.to_i # 만료 5분 전
else
true
end
end
여기까지 보면 자동 갱신이 되어야 할 것 같습니다. 하지만 문제는 refresh() 내부의 Aws::SharedCredentials 동작 방식에 있습니다.
Aws::SharedCredentials 내부는 대략 이런 조건으로 동작합니다:
if @path && @path == shared_config.credentials_path
# 기본 경로(/root/.aws/credentials)면 싱글톤 shared_config 재사용
@credentials = shared_config.credentials(profile: @profile_name)
else
# 다른 경로면 SharedConfig를 새로 생성해서 파일 재읽기
config = SharedConfig.new(
credentials_path: @path,
profile_name: @profile_name
)
@credentials = config.credentials(profile: @profile_name)
end
@path가 기본 경로(/root/.aws/credentials)와 같으면 싱글톤으로 관리되는 shared_config를 그대로 재사용합니다. refresh()가 30분마다 실행되어도 싱글톤 인스턴스가 최초 파싱된 값을 계속 반환하기 때문에, SSM Agent가 파일을 아무리 갱신해도 CodeDeploy Agent 프로세스는 최초 로딩된 값을 그대로 사용합니다.
4. 해결 방법 검토
캐시 문제를 해결하는 방법은 크게 두 가지입니다.
방법 1) CodeDeploy Agent 소스 수정
refresh() 호출 전에 Aws.shared_config.fresh()로 싱글톤 캐시를 강제로 클리어합니다.
def refresh
# refresh 함수 호출 시 shared_config.fresh() 호출하여 캐시 클리어어
Aws.shared_config.fresh(config_enabled: true, credentials_path: @path)
@credentials = Aws::SharedCredentials.new(path: @path).credentials
@expiration = Time.now + 1800
end
동작은 하지만 AWS에서 관리하는 CodeDeploy Agent 소스를 직접 수정하는 방식이라 Agent 업데이트 시 변경사항이 사라지고, AWS에서 권장하지 않습니다.
방법 2) credentials 경로 분리
앞서 본 조건처럼, @path가 기본 경로와 다르면 SharedConfig를 매번 새로 생성해서 파일을 다시 읽습니다. 즉, SSM Agent가 쓰는 경로를 기본 경로가 아닌 다른 경로로 변경하면, CodeDeploy Agent가 refresh() 시 SharedConfig를 새로 생성하면서 최신 credentials를 읽어오게 됩니다.
방법 2를 선택한 이유:
- 소스 수정 없이 설정만으로 해결 가능
- Agent 업데이트에도 영향받지 않음
- 근본적인 원인(기본 경로 싱글톤 재사용)을 구조적으로 해결
5. 해결: systemctl override로 환경변수 주입
SSM Agent 소스 분석
amazon-ssm-agent 소스의 agent/managedInstances/sharedCredentials/shared_credentials.go를 보면 credentials 저장 경로를 결정하는 GetSharedCredsFilePath() 함수가 있습니다.
func GetSharedCredsFilePath(filename string) (string, error) {
// 1순위: AWS_SHARED_CREDENTIALS_FILE 환경변수
if credPath := os.Getenv("AWS_SHARED_CREDENTIALS_FILE"); credPath != "" && filename == "" {
return credPath, nil
}
// 2순위: $HOME/.aws/credentials
homeDir := getPlatformSpecificHomeLocation()
return filepath.Join(homeDir, ".aws", "credentials"), nil
}
AWS_SHARED_CREDENTIALS_FILE 환경변수가 설정되어 있으면 그 경로를 1순위로 사용합니다. 이 환경변수를 SSM Agent 서비스에 주입하면 /root/.aws/credentials 대신 원하는 경로에 credentials를 저장하도록 바꿀 수 있습니다.
SSM Agent 서비스 override
sudo vi /etc/systemd/system/codedeploy-agent.service.d/override.conf
[Service]
Environment="AWS_SHARED_CREDENTIALS_FILE=/var/lib/amazon/ssm/credentials"
sudo systemctl daemon-reload
sudo systemctl restart amazon-ssm-agent
CodeDeploy Agent 경로 수정
CodeDeploy Agent도 동일한 환경변수로 읽는 경로를 맞춰줍니다. 기본 경로(/root/.aws/credentials)가 아닌 경로를 바라보게 되므로, refresh() 시 SharedConfig를 매번 새로 생성해서 파일을 재읽는 구조가 됩니다.
# /etc/codedeploy-agent/conf/codedeploy.onpremises.yml
---
iam_session_arn: arn:aws:sts::{ACCOUNT}:assumed-role/{ROLE}/{NAME}
aws_credentials_file: /var/lib/amazon/ssm/credentials
region: ap-northeast-2
sudo systemctl daemon-reload sudo systemctl restart codedeploy-agent
적용 확인
# 환경변수 적용 확인
sudo systemctl show amazon-ssm-agent | grep Environment
sudo systemctl show codedeploy-agent | grep Environment
# SSM Agent가 새 경로에 쓰는지 확인
watch -n 5 "cat /var/lib/amazon/ssm/credentials"
# CodeDeploy Agent 로그 확인
tail -f /var/log/aws/codedeploy-agent/codedeploy-agent.log
6. 마무리
이 방식으로 설정하면 아래 흐름으로 동작합니다.
SSM Agent (30분마다)
→ /var/lib/amazon/ssm/credentials 갱신
CodeDeploy Agent
→ credentials 호출 시 near_expiration? 체크
→ 만료 5분 전이면 /var/lib/amazon/ssm/credentials 재읽기
→ 기본 경로가 아니므로 SharedConfig 새로 생성
→ 최신 credentials 반환
Cron 제거, Agent 재기동 없이 credentials 자동 갱신이 가능해졌습니다.
이 방법이 레퍼런스가 거의 없었던 이유는, SSM Hybrid Activation과 CodeDeploy Agent를 온프레미스에서 함께 사용하는 케이스 자체가 드물기 때문입니다. 대부분의 온프레미스 환경은 IAM User 고정 credentials를 사용하거나, EC2에서는 Instance Profile을 사용해서 이 조합을 쓸 일이 없습니다.
같은 문제를 겪고 있는 분들께 도움이 되길 바랍니다.