AWS에 처음부터 배포하기 5) Jenkins 배포 환경 설정

Yeshin Lee
25 min readOct 8, 2024

--

토이 프로젝트와 과제 테스트 등 우선 순위에 밀려 한 달이 지난 지금에서야 Jenkins 배포 환경 설정에 대해 작성한다. 오랜만에 작성하다보니 복습하는 데 많은 도움이 되었다.

Jenkins 배포 환경을 설정한다는 것은 CI/CD 파이프라인을 구축하기 위한 준비 과정을 의미한다. 파이프라인(Pipeline)은 일련의 과정들이 자동으로 순차적으로 실행되는 워크 플로우인데 즉, 소스코드 다운로드하여 Jenkins로 빌드 & 패키징, 파일 전송 및 실행하는 것을 파이프라인 스크립트 파일로 실행하는 것이다. 개발자가 코드를 푸시하면, 이 코드가 빌드 및 테스트, 배포되는 모든 자동화 과정이 하나의 파이프라인 안에서 처리되는 것이다.

파이프라인을 구성하는데 Jenkins가 설치된 서버 외에 하나의 서버가 더 필요하다. 이 때 Jenkins가 설치된 서버를 Jenkins 마스터 서버, 추가 서버를 Jenkins 에이전트 서버라고 한다.

  • Jenkins 마스터 서버: Jenkins 마스터 서버는 주로 파이프라인을 관리하고, 작업(빌드, 테스트, 배포 등)을 실행하는 노드(에이전트)로 작업을 할당한다.
  • Jenkins 에이전트 (슬레이브) 서버: Jenkins 마스터 서버로부터 명령을 받아 실제 작업을 수행하는 서버로 대규모 프로젝트나 여러 프로젝트를 동시에 처리할 때 유용하다.

서버가 하나 더 필요하기 때문에 인스턴스를 하나 더 만들어야 하는데, 왜 추가적인 서버가 필요할까?

  • 다양한 환경에서의 빌드 및 테스트: 다른 운영 체제 등 특정 환경에서 빌드나 테스트를 실행할 때 추가 서버가 필요할 수 있다.
  • 보안 분리: 배포를 위한 실제 서버는 Jenkins 서버와 분리된 네트워크 또는 환경에 있을 수 있다.
  • 리소스 확장: Jenkins는 자원 소모가 많은 빌드, 테스트, 배포 작업을 수행할 수 있으므로 추가 서버로 작업을 분산시킨다.

새로운 EC2 인스턴스를 Jenkins 에이전트(Agent) 서버로 사용하려는 경우, Jenkins 마스터 서버와의 통합을 설정해야 한다. 즉, Jenkins 에이전트를 설치하고, 마스터 서버와 연결한다.

에이전트 서버역할을 할 ec2는 마스터 서버 역할을 할 ec2와 동일한 설정(보안그룹 포함)에 동일한 키로 설정한다. 기존 인스턴스의 이름을 master_server, 새로 생성한 인스턴스의 이름은 agent_server라고 지었다. 참고로 이름은 인스턴스의 동작에 영향을 주지 않는 메타 데이터이므로 서버가 돌아가는 중에 변경해도 상관없다. 아래 명령어로 에이전트 서버에서 필요환 환경을 설정한다.

# 리눅스 업데이트
sudo dnf update -y

# Amazon Corretto: AWS가 제공하는 오픈JDK 배포판으로, Java 17 설치
sudo dnf install -y java-17-amazon-corretto-devel

# 자바 버전 확인
java -version

# node 설치
curl -fsSL <https://rpm.nodesource.com/setup_current.x> | sudo bash -
sudo dnf install -y nodejs

이제 Jenkins 마스터 서버에서 에이전트 서버를 설정한다. New Node Permanent Agent 옵션 선택 — create를 클릭한다. Host에는 agent-server의 Public IPv4 Address를 입력한다.

Credentials에서는 SSH key로 인증한다.

ID 필드를 비워두면 Jenkins가 자동으로 ID 를 생성한다. 참고로 private key를 넣을 때는 위 아래 설명도 같이 넣어야 한다.

-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----

Host Key Verification Strategy에는 ‘Known hosts file Verification Strategy’를, Availability에는 ‘Keep this agent online as much as possible’를 선택한다. Node Properties는 기본값을 유지하되 필요에 따라 추가적으로 설정한다. 그러면 아래와 보이는데..

왼쪽 바에서 agent-server가 offline으로 나타난다. 이는 SSH Key 문제로 agent server가 아닌 master server에서 SSH Key를 생성해야 한다.

SSH key를 생성하고 Credentials ec2-user의 private key를 master server에서 생성한 private key로 변경한다. 그리고 master-server에서 생성한 SSH 공개 키를 agent server의 ~/.ssh/authorized_keys 파일에 넣는다.

# Jenkins 마스터 서버가 SSH로 접속할 수 있도록
# 에이전트 서버의 ~/.ssh/authorized_keys 파일에 공개 키 추가
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

SSH Key 변경 후, master-server에서 agent-server로 성공적으로 들어가진다.

여기서 알 수 있는 부분은 Credentials에 사용되는 SSH Key는 마스터 서버가 에이전트 서버에 접속할 때 사용할 키라는 것이다. 마스터 서버가 에이전트 서버에 접속하여 빌드나 배포 작업을 진행하기 때문에 마스터 서버에서 생성해야하며, 해당 공개 키는 에이전트 서버에 저장되어 있어야 한다.

Jenkins 홈에 들어가니 agent-server의 상태가 N/A로 이상하게 보인다. N/A(Not Available)는 해당 항목에 대해 정보가 제공되지 않는다는 것이다.

Log를 확인했더니 키 교환(Key exchange)이 끝나지 않아 연결이 끊겼다.

SSHLauncher{host='3.38.212.93', port=22, credentialsId='', jvmOptions='', javaPath='', prefixStartSlaveCmd='', suffixStartSlaveCmd='', launchTimeoutSeconds=60, maxNumRetries=10, retryWaitTime=15, sshHostKeyVerificationStrategy=hudson.plugins.sshslaves.verifiers.KnownHostsFileKeyVerificationStrategy, tcpNoDelay=true, trackCredentials=true}
[08/28/24 09:48:02] [SSH] Opening SSH connection to 3.38.212.93:22.
/var/jenkins_home/.ssh/known_hosts [SSH] No Known Hosts file was found at /var/jenkins_home/.ssh/known_hosts. Please ensure one is created at this path and that Jenkins can read it.
Key exchange was not finished, connection is closed.
SSH Connection failed with IOException: "Key exchange was not finished, connection is closed.", retrying in 15 seconds. There are 10 more retries left.
/var/jenkins_home/.ssh/known_hosts [SSH] No Known Hosts file was found at /var/jenkins_home/.ssh/known_hosts. Please ensure one is created at this path and that Jenkins can read it.
Key exchange was not finished, connection is closed.
...
ERROR: Connection is not established!
java.lang.IllegalStateException: Connection is not established!
at PluginClassLoader for trilead-api//com.trilead.ssh2.Connection.getRemainingAuthMethods(Connection.java:989)
at PluginClassLoader for ssh-credentials//com.cloudbees.jenkins.plugins.sshcredentials.impl.TrileadSSHPublicKeyAuthenticator.getRemainingAuthMethods(TrileadSSHPublicKeyAuthenticator.java:89)
at PluginClassLoader for ssh-credentials//com.cloudbees.jenkins.plugins.sshcredentials.impl.TrileadSSHPublicKeyAuthenticator.canAuthenticate(TrileadSSHPublicKeyAuthenticator.java:81)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(Unknown Source)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(Unknown Source)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(Unknown Source)
at java.base/java.util.Spliterators$IteratorSpliterator.tryAdvance(Unknown Source)
at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.base/java.util.stream.ReferencePipeline.findFirst(Unknown Source)
at PluginClassLoader for ssh-credentials//com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator.newInstance(SSHAuthenticator.java:222)
at PluginClassLoader for ssh-credentials//com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator.newInstance(SSHAuthenticator.java:173)
at PluginClassLoader for ssh-slaves//hudson.plugins.sshslaves.SSHLauncher.openConnection(SSHLauncher.java:882)
at PluginClassLoader for ssh-slaves//hudson.plugins.sshslaves.SSHLauncher.lambda$launch$0(SSHLauncher.java:441)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
[08/28/24 09:50:47] Launch failed - cleaning up connection
[08/28/24 09:50:47] [SSH] Connection closed.

바로 다음 줄에서 No Known Hosts file was found at /var/jenkins_home/.ssh/known_hosts를 보고, Jenkins 마스터 서버가 에이전트 서버에 SSH로 연결하려고 시도할 때 known_hosts 파일이 없어서 호스트 키 검증에 실패하는 것을 알아챘다. Jenkins는 기본적으로 호스트 키 검증을 통해 SSH 연결의 보안을 유지한다고 한다. 마스터 서버에서 known_hosts 파일을 만들고 에이전트 서버의 호스트 키를 추가했다.

# master-server에서 컨테이너에 접근하는 명령어 실행
docker exec -it <jenkins-container-id> /bin/bash

# Jenkins 홈 디렉토리로 이동
cd /var/jenkins_home/.ssh

# 만약 .ssh 디렉토리가 없으면 새로 만들고 권한 부여
mkdir .ssh
chmod 700 .ssh

# ssh-keyscan 명령어로 에이전트 서버의 호스트 키를 known_hosts 파일에 추가
ssh-keyscan -H 3.38.212.93 >> known_hosts

# known_hosts 파일 권한 설정
chmod 600 .ssh/known_hosts

# 컨테이너 종료
exit

해당 설정을 완료하면 Jenkins 홈페이지로 돌아가 Relaunch agent를 클릭, 에이전트 연결을 다시 시도했더니 인증 실패 에러가 발생했다.

Warning: no key algorithms provided; JENKINS-42959 disabled
SSHLauncher{host='3.38.212.93', port=22, credentialsId='', jvmOptions='', javaPath='', prefixStartSlaveCmd='', suffixStartSlaveCmd='', launchTimeoutSeconds=60, maxNumRetries=10, retryWaitTime=15, sshHostKeyVerificationStrategy=hudson.plugins.sshslaves.verifiers.KnownHostsFileKeyVerificationStrategy, tcpNoDelay=true, trackCredentials=true}
[08/28/24 09:56:33] [SSH] Opening SSH connection to 3.38.212.93:22.
Searching for 3.38.212.93 in /var/jenkins_home/.ssh/known_hosts
Searching for 3.38.212.93:22 in /var/jenkins_home/.ssh/known_hosts
[08/28/24 09:56:33] [SSH] SSH host key matches key in Known Hosts file. Connection will be allowed.
ERROR: Server rejected the 1 private key(s) for ec2-user (credentialId:/method:publickey)
ERROR: Failed to authenticate as ec2-user with credential=
java.io.IOException: Publickey authentication failed.
at PluginClassLoader for trilead-api//com.trilead.ssh2.auth.AuthenticationManager.authenticatePublicKey(AuthenticationManager.java:349)
at PluginClassLoader for trilead-api//com.trilead.ssh2.Connection.authenticateWithPublicKey(Connection.java:472)
at PluginClassLoader for ssh-credentials//com.cloudbees.jenkins.plugins.sshcredentials.impl.TrileadSSHPublicKeyAuthenticator.doAuthenticate(TrileadSSHPublicKeyAuthenticator.java:110)
at PluginClassLoader for ssh-credentials//com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator.authenticate(SSHAuthenticator.java:431)
at PluginClassLoader for ssh-credentials//com.cloudbees.jenkins.plugins.sshcredentials.SSHAuthenticator.authenticate(SSHAuthenticator.java:468)
at PluginClassLoader for ssh-slaves//hudson.plugins.sshslaves.SSHLauncher.openConnection(SSHLauncher.java:882)
at PluginClassLoader for ssh-slaves//hudson.plugins.sshslaves.SSHLauncher.lambda$launch$0(SSHLauncher.java:441)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.io.IOException: Could not generate signature
at PluginClassLoader for trilead-api//com.trilead.ssh2.signature.KeyAlgorithm.generateSignature(KeyAlgorithm.java:43)
at PluginClassLoader for trilead-api//com.trilead.ssh2.auth.AuthenticationManager.authenticatePublicKey(AuthenticationManager.java:316)
... 10 more
Caused by: java.security.SignatureException: Could not sign data
at java.base/sun.security.rsa.RSASignature.engineSign(Unknown Source)
at java.base/java.security.Signature$Delegate.engineSign(Unknown Source)
at java.base/java.security.Signature.sign(Unknown Source)
at PluginClassLoader for trilead-api//com.trilead.ssh2.signature.KeyAlgorithm.generateSignature(KeyAlgorithm.java:41)
... 11 more
Caused by: javax.crypto.BadPaddingException: RSA private key operation failed
at java.base/sun.security.rsa.RSACore.crtCrypt(Unknown Source)
at java.base/sun.security.rsa.RSACore.rsa(Unknown Source)
... 15 more
[08/28/24 09:56:33] [SSH] Authentication failed.
Authentication failed.
[08/28/24 09:56:33] Launch failed - cleaning up connection
[08/28/24 09:56:33] [SSH] Connection closed.

ERROR:Server rejected the 1 private key(s) ~, ERROR:Failed to authenticate as ec2-user with credential ~ 메시지를 보니 인증 실패였다. 그 밑에 RSA private key operation failed를 보고 SSH private key 문제로 생각했다. SSH Key 파일 문제인가 싶었는데 SSH Key 형식이 문제였다. 나는 OpenSSH 형식으로 키를 생성했는데 Jenkins에서는 PEM 형식의 RSA 키를 사용한다고 한다. 아래 명령어로 PEM 형식으로 변환해주었다.

ssh-keygen -p -m PEM -f [private key 위치]

Credentials에서 private key를 업데이트한 후, 다시 launch를 돌려보았더니 Could not copy ~ 란다.

...
java.io.IOException: Could not copy remoting.jar into '/var/jenkins' on agent
at PluginClassLoader for ssh-slaves//hudson.plugins.sshslaves.SSHLauncher.copyAgentJar(SSHLauncher.java:734)
at PluginClassLoader for ssh-slaves//hudson.plugins.sshslaves.SSHLauncher.lambda$launch$0(SSHLauncher.java:463)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.io.IOException: Could not copy remoting.jar to '/var/jenkins/remoting.jar' on agent
at PluginClassLoader for ssh-slaves//hudson.plugins.sshslaves.SSHLauncher.copyAgentJar(SSHLauncher.java:726)
... 5 more
Caused by: com.trilead.ssh2.SFTPException: Permission denied (SSH_FX_PERMISSION_DENIED: The user does not have sufficient permissions to perform the operation.)
at PluginClassLoader for trilead-api//com.trilead.ssh2.SFTPv3Client.openFile(SFTPv3Client.java:1201)
at PluginClassLoader for trilead-api//com.trilead.ssh2.SFTPv3Client.createFile(SFTPv3Client.java:1074)
at PluginClassLoader for trilead-api//com.trilead.ssh2.SFTPv3Client.createFile(SFTPv3Client.java:1055)
at PluginClassLoader for trilead-api//com.trilead.ssh2.jenkins.SFTPClient.writeToFile(SFTPClient.java:102)
at PluginClassLoader for ssh-slaves//hudson.plugins.sshslaves.SSHLauncher.copyAgentJar(SSHLauncher.java:719)
... 5 more
[08/28/24 10:07:05] Launch failed - cleaning up connection
[08/28/24 10:07:05] [SSH] Connection closed.

이는 Jenkins가 에이전트 서버에 remoting.jar 파일을 복사하려고 할 때, 에이전트 서버의 /var/jenkins 디렉토리에 대한 쓰기 권한이 없다고 한다. 아래 명령어로 /var/jenkins 디렉토리의 권한을 변경해주었다.

# master server에서 agent server에 접속
ssh -i ~/.ssh/id_rsa ec2-user@3.38.212.93

# /var/jenkins 폴더가 존재하는지 확인하고, 없으면 디렉토리를 생성한다.
ls -ld /var/jenkins

# /var/jenkins 디렉토리와 그 하위 파일의 소유자, 그룹을 Jenkins가 사용할 계정(ec2-user)으로 변경
sudo chown -R ec2-user:ec2-user /var/jenkins

# ec2-user 계정은 디렉토리와 그 하위 파일에 대해 읽기/쓰기/실행 권한을
# 그룹과 다른 사용자(others)는 읽기/쓰기 권한 부여
sudo chmod -R 755 /var/jenkins

다시 launch를 해보니 successfully connected라고 뜬다. 이는 Jenkins 에이전트가 마스터 서버로부터 작업을 받아서 수행할 준비가 되었다는 것이다.

로그 밑에 3개의 점은 현재 agent-server가 foreground에서 돌고 있다는 것을 의미한다. nohup 명령어로 백그라운드로 돌도록 수정하기 전에, 노드 프로젝트를 agent-server에 올리는 작업과 아래 사진에서 보이는 것과 같이 메모리 부족 현상을 해결할 것이다.

--

--