개발하는 자몽

CI/CD 구축하기: GitHub Actions (OIDC) + Amazon S3 + Amazon CodeDeploy 본문

Architecture & Tool/AWS

CI/CD 구축하기: GitHub Actions (OIDC) + Amazon S3 + Amazon CodeDeploy

jaamong 2025. 7. 28. 20:01

AWS EC2 인스턴스(Ubuntu 24.04) 한 대에 배포되고 있는 Gradle/SpringBoot 서버에 대해서 OIDC를 이용해서 CI/CD를 구축해 보는 글입니다. EC2 인스턴스 생성은 다루지 않습니다. 

 

⏭️다음 글: CI/CD에 블루-그린 무중단 배포 적용하기: AWS ELB, AWS Auto Scaling Groups(ASG)

 

 

⭐항상 AWS 리소스를 다룰 땐 참고하고 있는 글이 최신 버전인지 확인하기

 

 

작성 계기

GitHub Actions를 CI 툴로 사용하는 대부분의 소개글은 GitHub Actions에서 AWS로 접근하기 위해 IAM 액세스 키를 많이 사용한다. 이때 여러 가지를 고려했을 때 IAM 액세스 키보다는 OpenID Connect(OIDC) 사용을 더 권장한다고 한다. 그래서 도입하기 위해 찾아보면, OIDC를 구성하는 글과 이를 CI/CD에 적용하는 글은 다 별개로 작성되어 있어 답답했다. 그래서 그냥 내가 해보면서 성공하면 작성하자!라는 생각으로 시작했고, 다행히 성공해서 이렇게 쓰게 되었다. 

 

사전 준비

  • AWS 루트 계정 말고, 필요한 권한을 다 갖고 있는 IAM 사용자; 개인 AWS 계정이더라도, IAM 사용자를 생성해서 진행하자. 
    • 이 과정을 위해 IAM 사용자에게 어떤 권한을 부여해야 할지 모르겠고, 찾기도 힘들다면 `AdministratorAccess` 정책(policy)을 갖게 하자. 거의 대부분의 권한을 갖고 있다. 
  • CI/CD 배포 대상인 EC2 인스턴스와 프로젝트 실행에 필요한 언어 설치
    • EC2 인스턴스의 OS(AMI)는 Ubuntu 24.04
    • VPC나 서브넷(subnet)은 AWS에서 기본적으로 제공해 주는 것을 사용해도 문제없다. 직접 생성하고 사용하려면 그것 자체로 포스트 하나가 나오므로.. 그냥 디폴트 VPC와 서브넷을 사용하자.
    • 여기서는 EC2 인스턴스에 Java 17 / SpringBoot / Gradle 프로젝트를 운영한다. 하지만 개발 언어나 프레임워크가 무엇인지는 크게 중요하지 않다. 
    • 언어 설치는 사실 미리 해두지 않아도 괜찮은 것 같다. 편한대로 하면 된다. 
  • 배포할 프로젝트가 올라가있는 GitHub 레포지토리
    • 레포지토리가 속한 조직(organization)과 어떤 브랜치를 대상으로 배포할지 미리 알아두기!
      • 개인 계정인 경우, 별도로 생성해둔 조직이 없다면 보통 계정명이 조직명이 된다.
      • 브랜치도 특별히 정해진 브랜치 배포 규칙이 없다면 보통 `main` 브랜치가 된다.
  • 프로젝트에 미리 헬스체크 API 만들어두기 (당장 필요하진 않지만, 다음 글에서 필요)
    • HTTP 메소드는 `GET`, 반환은 `200 OK`만 해줘도 충분하다. 

 

본론

전체 흐름

CI/CD 파이프라인에서 GitHub Actions는 CI를 담당한다. 개발자가 GitHub에 push 등의 지정한 동작을 수행하면 GitHub Actions이 트리거 되어 `.github/workflows/{수행할_파일}.yml`에 적힌 내용을 수행한다. 이 파일은 다음 워크플로우를 수행한다. 

  • [Build] 애플리케이션 빌드 (이 결과물을 아티팩트라고 함)
  • [Build] 아티팩트를 `@actions/upload-artifact`를 이용하여 GitHub에 업로드
  • [Deploy] GitHub에 업로드한 아티팩트를 `@actions/download-artifact` 다운로드
  • [Deploy] GitHub Actions OIDC를 이용하여 AWS credentials 설정
  • [Deploy] 아티팩트 패키징(압축) 및 S3에 업로드
  • [Deploy] CodeDeploy를 이용하여 S3에 업로드된 패키징을 EC2 인스턴스에 배포
  • [Deploy] 배포 완료 대기

 

EC2 보안그룹(Security Group) 설정

EC2 인스턴스가 준비되었다면, EC2 보안그룹을 설정해보자. 이미 해두었을 것 같은데, 혹시 모르니 추가.

AWS는 보안그룹에 대해 기본적으로 아웃바운드는 다 허용하고 인바운드는 다 막아둔다. 즉, 서버로 들어오는 요청은 다 막고, 나가는 것은 다 허용한다는 뜻이다. 따라서 필요한 요청은 받을 수 있게 적절하게 인바운드 규칙을 설정해야 한다. 

 

EC2 메뉴 하위에 위치한 보안그룹을 찾아서 이동한다.

 

EC2 인스턴스를 생성할 때 보안그룹을 새로 생성하는 옵션을 선택했다면, `launch-wizard-1` 이름을 가진 보안그룹이 있을 것이다. 이것을 선택하고, 인바운드 규칙 편집을 클릭한다. 

 

다음과 같이 설정하고 마치면 된다. 

  • 인스턴스 생성 시 발급한 `.pem` 파일로 EC2에 접근하려면 SSH를 열어야 한다. 알고 있겠지만, 소스를 저렇게 `0.0.0.0/0`으로 다 열어두는 것보단 접속하는 곳의 IP만 지정하는 것이 보안상 좋다. 
  • 여기서는 스프링 부트 애플리케이션을 실행하므로, `사용자 지정 TCP`로 유형을 선택하고 포트를 `8080`으로 입력한다. 8080번 포트가 아닌 다른 포트로 애플리케이션을 실행한다면 그 포트를 입력하면 된다. 
  • Nginx 같은 걸로 HTTP 80 요청을 받아서 애플리케이션에게 전달하는 상황이 아니라서, HTTP 80은 추가하지 않았다.

이제 보안그룹 설정은 완료됐다.

 

EC2 인스턴스에 CodeDeploy의 접근을 위한 IAM 역할(role) 생성 및 부여하기

AWS 리소스와 외부에서 서로 접근하듯이, AWS 리소스끼리도 서로 접근할 수 있다. 이를 위해 적절한 권한을 가진 IAM 역할이 리소스에게 부여되어야 한다. 

 

여기서는 다음을 고려해야 한다. 

  • EC2 인스턴스 to S3 버킷에 대한 접근 권한
  • EC2나 온프레미스 환경에서 CodeDeploy를 사용하려면 CodeDeploy 에이전트가 필요하다. 그리고 이 에이전트가 필요로 하는 권한이 있다. 

이제 IAM 역할을 생성하고 부여해보자.

 

IAM 역할 생성

IAM 메뉴 하위에 있는 역할을 클릭하여 이동한다. 이동한 대시보드에서 역할 생성을 클릭한다. 

 

 

1단계: 신뢰할 수 있는 엔터티 선택에서 AWS 서비스를 선택하고, 사용 사례로 EC2를 선택한다. 선택하면 다양한 사용 사례가 아래 나열되는데, EC2 만 적힌 것을 선택하면 된다. 그리고 넘어간다. 

 

2단계: 권한 추가의 검색창에서 다음 두 개를 검색해서 추가한다.

 

  • AmazonEC2RoleforAWSCodeDeploy: 이 권한은 EC2가 S3 버킷에 제한적으로 접근할 수 있도록 하며, EC2에 설치된 CodeDeploy 에이전트에게 필요하다. 
  • AmazonS3ReadOnlyAccess: 이 권한은 모든 S3 버킷에 접근할 수 있도록 한다. 위 권한만을 가지고 S3 버킷에 접근할 수 있지만, CodeDeploy가 사용할 S3 생성 시 태그 설정(UseWithCodeDeploy=true)이 필요하다. 일단 이 권한을 추가해서 넓게 접근할 수 있도록 한다. 
    • 사실 이 부분을 CI/CD 구축한 후에 알아서 일단 이 버전으로 작성한다. 

권한 경계 설정은 손대지 않고 다음으로 넘어간다. 

 

3단계: 이름 지정, 검토 및 생성에서 이 역할의 이름과 설명을 작성한다. 이름은 어떤 역할을 수행하는지 나타낼 수 있도록 명확하게 작성한다. 나머지는 그대로 두고, 역할 생성을 클릭하여 완료한다. 

 

IAM 역할 부여

이제 EC2 인스턴스 대시보드로 돌아오자.

CI/CD를 적용할 인스턴스를 선택하고, 작업 > 보안 > IAM 역할 수정을 클릭한다. 

 

방금 만든 IAM 역할을 선택하고 IAM 역할 업데이트를 클릭하여 완료한다. 

 

EC2 인스턴스 태그(tag) 설정

CodeDeploy로 배포를 할 때는 이 리소스에게 정보를 제공해줘야 한다. 여기서 

태그는 CodeDeploy 배포 그룹에서 배포할 EC2 인스턴스를 식별하고 타겟팅하는 용도로 사용된다. 또한 태그를 이용해서 `prod`나 `dev` 등의 배포 환경을 식별할 수도 있다. 

 

다시 인스턴스 대시보드로 와서, 배포 대상인 인스턴스를 선택하자. 그리고 아래 태그 탭에서 태그 관리를 클릭한다. 

 

`Environment` 키에는 배포 환경을 입력하고, `Application` 키는 원하는 대로 입력해도 된다. 입력하고 잘 기억해 두자. (두 키 값의 이름도 잘 기억만 해두면 상관없으니, 편한 대로 입력해도 상관없다)

CodeDeploy 에이전트 설치 (Ubuntu 서버 버전)

CodeDeploy를 사용하여 EC2에 배포하려면 위 에이전트를 설치해야 한다. 터미널에서 아래 명령어로 EC2에 접속하자.

ssh -i {ec2 생성 시 발급받은 .pem이 위치한 경로 + 이름}.pem ubuntu@{ec2 퍼블릭 DNS 주소}

# 예시 1) pem 키가 위치한 곳에서 진행중일때
ssh -i example_key.pem ubuntu@ec2-dns-address  

# 예시 2) pem 키가 위치한 곳이 아닐때 (Windows cmd 이용 시)
ssh -i C:\Users\user\Desktop\...\example_key.pem ubuntu@ec2-dns-address

 

배포 대상인 EC2 인스턴스의 AMI가 Ubuntu 이므로, ` ubuntu`가 기본 사용자 이름이 된다. AMI에 따라 기본 사용자 이름이 다르므로 여기를 참고. 

 

접속을 성공했다면 아래 명령어를 순서대로 입력하여 최신 버전의 CodeDeploy 에이전트를 설치한다. 혹시 모르니 공식 문서를 참고하면서 진행하자.

sudo apt update
sudo apt install ruby-full
sudo apt install wget
cd /home/ubuntu
wget https://bucket-name.s3.region-identifier.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto

 

잘 진행되었다면 아래 명령어를 입력하여 실행 상태를 확인한다. 

systemctl status codedeploy-agent

 

정상적으로 실행되고 있다면 아래와 같이 `active (running)`를 확인할 수 있다. 

 

OIDC 설정 및 GitHub Actions OIDC를 위한 IAM 역할 생성

이제 OIDC 공급 업체를 등록하고 이 OIDC를 통해 GitHub Actions이 AWS 리소스에 접근할 수 있도록 역할을 생성 및 부여한다. 

 

IAM > 제공업체로 이동하고 공급자 추가를 클릭한다. 

 

 

ID 제공업체 추가 화면에서 아래와 같이 입력하고, 하단에 공급자 추가를 클릭한다. 

  • 공급자 유형: `OpenID Connect`
  • 공급자 URL: `https://token.actions.githubusercontent.com`
  • 대상: `sts.amazonaws.com`

 

IAM > 역할로 이동하고 역할 생성을 클릭한다. 1단계: 신뢰할 수 있는 엔터티 선택에서 웹 자격 증명을 선택한다. 

 

선택하면 아래와 같은 화면이 나온다. 모두 입력하고 다음을 클릭한다. 

  • ID 제공업체: 아까 추가한 OIDC 제공업체가 목록에 있을 것이다. 이것을 선택한다.
  • Audience: OIDC 제공업체 추가 시 입력한 `대상`을 선택한다. 
  • GitHub organization: 배포할 레포지토리가 위치한 조직명을 입력한다. 조직을 만든 적이 없다면, 보통 GitHub `username`이 조직명이 된다. 
  • GitHub Repository: 기본적으로 모두 허용인데(`*`), 보안상 타겟이 되는 레포지토리명을 입력하는 것이 좋다. 
  • GitHub branch: 이것도 기본적으로 모두 허용이지만, 배포할 브랜치를 따로 입력하자. 

2단계: 권한 추가에서 `AmazonS3FullAccess`, `AWSCodeDeployDeployerAccess`를 검색하여 추가한다. 

3단계: 이름 지정, 검토 및 생성에서 역할의 이름과 설명을 입력한다.

 

1단계에서 지정했던 신뢰할 수 있는 엔터티 선택의 JSON이 아래와 같은지 확인하자. 

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::{aws_account_id}:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:{organization}/{repository}:ref:refs/heads/{branch}"
                }
            }
        }
    ]
}

 

그다음 추가한 정책들이 모두 포함되어 있는지 확인하고, 모두 문제가 없다면 완료하자. 

 

CodeDeploy을 위한 IAM 역할 생성 및 CodeDeploy 생성

CodeDeploy IAM 역할 생성

이번에는 CodeDeploy를 위한 IAM 역할을 생성한다. 이전과 같이 역할을 생성하는 곳으로 이동하여 진행한다. 

 

1단계: 신뢰할 수 있는 엔터티 선택에서 AWS 서비스를 선택한다. 사용 사례CodeDeploy를 선택하고 다음으로 넘어간다.

2단계: 권한 추가를 보면 자동으로 `AWSCodeDeployRole` 권한이 추가되어 있다. 다음으로 넘어가자.

3단계: 이름 지정, 검토 및 생성에서 역할의 이름과 설명을 입력하고 마친다. 

 

CodeDeploy 생성

이제 CodeDeploy > 배포 > 애플리케이션으로 이동하자. 이동하면 대시보드에 있는 애플리케이션 생성 버튼을 클릭한다. 

 

본인의 애플리케이션의 이름을 입력하고, 우리는 EC2 인스턴스에 배포할 계획이므로 컴퓨팅 플랫폼을 `EC2/온프레미스`로 선택한다. 그리고 생성 버튼을 클릭한다. 참고로 애플리케이션 이름은 GitHub Actions CI 단계에서 필요하므로 잘 기억해 두자. 

 

이후 생성한 애플리케이션을 클릭하면 나오는 대시보드에서 배포 그룹 탭을 클릭한다. 여기에서 배포 그룹 생성을 클릭한다. 

 

배포 그룹 이름을 원하는 대로 입력한다. 입력해 놓고 잘 기억해 두자. GitHub Actions CI 단계에서 필요하다.

서비스 역할은 방금 CodeDeploy 용으로 생성한 IAM 역할을 선택한다. 

 

배포 유형환경 구성을 아래와 같이 선택한다. 이때 태그 그룹의 키와 값은 본인이 EC2 인스턴스에 추가한 태그의 키와 값과 동일해야 한다. 

  • 배포 유형: 현재 위치(In-place)
  • 환경 구성: Amazon EC2 인스턴스
    • 태그 그룹: EC2 인스턴스에 등록한 태그와 동일하게 설정 

CodeDeploy 에이전트는 이미 설치했으므로 안 함을 선택한다. 배포 설정배포 구성은 `AllAtOnce`로 선택한다. 로드 밸런싱 활성화는 체크하지 않는다. 이제 배포 그룹 생성을 클릭하여 완료한다. 

 

S3 버킷 생성 및 정책 설정

S3 버킷 생성

S3 서비스로 이동해서 버킷 만들기를 클릭한다. 그리고 다음과 같이 진행한다. 

  • 버킷 이름: 누군가가 쓰고 있는 이름은 사용할 수 없으므로, 전역적으로 식별되는 이름을 입력해야 한다. 
    • GitHub Actions에서 필요하니 이름을 기억해 둘 것
  • 객체 소유권: ACL 비활성화됨(권장)
  • 이 버킷의 퍼블릭 액세스 차단 설정: 모든 퍼블릭 액세스 차단
  • 버킷 버전 관리: 활성화
  • 기본 암호화 - 암호화 유형: Amazon S3 관리형 키(SSE-S3)를 사용한 서버 측 암호화
  • 기본 암호화 - 버킷 키: 활성화 

위에서 언급되지 않은 나머지 요소들은 초기 상태 그대로 놔둔다. 이제 버킷 만들기를 클릭하여 완료한다. 

 

S3 버킷 정책 설정

우리는 방금 생성한 S3 버킷에 대해 크게 네 가지 Sid를 갖도록 정책을 생성할 것이다. 

 

  1. GitHub Actions OIDC를 통해 S3 버킷에 객체를 업로드할 수 있게 함. `Condition` 문을 추가하여 역할을 맡으려는 보안 주체에 대한 추가 요구 사항을 설정할 것.
  2. GitHub Actions OIDC를 통해 업로드 중 객체의 액세스 제어 목록 (ACL)을 변경할 수 있으며, S3 버킷에서 객체를 조회할 수 있게 함. 
  3. CodeDeploy가 S3 버킷에서 객체를 조회할 수 있고, 버킷 리스트를 확인할 수 있게 함
  4. EC2 인스턴스가 S3 버킷에서 객체를 조회할 수 있고, 버킷 리스트를 확인할 수 있게 함

따라서 `GitHub Actions ODIC 공급자의 ARN`, `CodeDeploy ARN`, `EC2 인스턴스 ARN`, `S3 버킷 ARN`가 모두 필요하다. 미리 찾아놓자. 

 

범용 버킷 탭에서 생성한 S3 버킷을 클릭하고 권한 탭으로 이동한다. 아래로 이동하면 나오는 버킷 정책에서 편집을 클릭한다. 

 

이동 후 나오는 창에서 정책 생성기를 클릭한다.

 

Sid 마다 항목별로 입력하고 Add Statement를 클릭하면 된다. 

Sid 1.

  • Step 1: Select policy type: S3 Bucket Policy
  • Step 2: Add statement(s)
    • Effect: Allow
    • Principal: IAM > OIDC 공급자 > 등록했던 GitHub Actions 공급자 클릭 > ARN 복사 및 붙여 넣기
    • Actions: PutObject
    • Amazon Resource Name (ARN): "{생성한 S3 버킷 ARN}"," {생성한 S3 버킷 ARN}/*"
    • Add conditions. 아래처럼 입력 후 Add Condition 클릭.
      • Condition: StirngEquals
      • Key: s3:x-amz-server-side-encryption
      • Value: AES256

Sid 2. 

  • Step 1: Select policy type: S3 Bucket Policy
  • Step 2: Add statement(s)
    • Effect: Allow
    • Principal: IAM > OIDC 공급자 > 등록했던 GitHub Actions 공급자 클릭 > ARN 복사 및 붙여 넣기
    • Actions: PutObjectAcl, GetObject
    • Amazon Resource Name (ARN): "{생성한 S3 버킷 ARN}"," {생성한 S3 버킷 ARN}/*"

Sid 3. 

  • Step 1: Select policy type: S3 Bucket Policy
  • Step 2: Add statement(s)
    • Effect: Allow
    • Principal: 생성한 CodeDeploy ARN 입력
    • Actions: GetObject, ListBucket
    • Amazon Resource Name (ARN): "{생성한 S3 버킷 ARN}"," {생성한 S3 버킷 ARN}/*"

Sid 4. 

  • Step 1: Select policy type: S3 Bucket Policy
  • Step 2: Add statement(s)
    • Effect: Allow
    • Principal: 생성한 EC2 인스턴스 ARN 입력
    • Actions: GetObject, ListBucket
    • Amazon Resource Name (ARN): "{생성한 S3 버킷 ARN}"," {생성한 S3 버킷 ARN}/*"

모두 입력했다면 아래에 Generate Policy를 클릭하고, 나오는 팝업에서 Copy Policy를 클릭한다. 다시 버킷 정책 편집창으로 돌아가서 복사한 정책을 JSON 입력란에 붙여 넣기 한다. 완료하면 아래와 같은 JSON 형식의 정책을 확인할 수 있다. 

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "{자유롭게_입력_중복안됨}",
			"Effect": "Allow",
			"Principal": {
				"AWS": {GitHub_Actions_OIDC_ARN}
			},
			"Action": "s3:PutObject",
			"Resource": [
				"arn:aws:s3:::{S3_BUCKET_NAME}",
				"arn:aws:s3:::{S3_BUCKET_NAME}/*"
			],
			"Condition": {
				"StringEquals": {
					"s3:x-amz-server-side-encryption": "AES256"
				}
			}
		},
		{
			"Sid": "{자유롭게_입력_중복안됨}",
			"Effect": "Allow",
			"Principal": {
				"AWS": {GitHub_Actions_OIDC_ARN}
			},
			"Action": [
				"s3:PutObjectAcl",
				"s3:GetObject"
			],
			"Resource": [
				"arn:aws:s3:::{S3_BUCKET_NAME}",
				"arn:aws:s3:::{S3_BUCKET_NAME}/*"
			]
		},
		{
			"Sid": "{자유롭게_입력_중복안됨}",
			"Effect": "Allow",
			"Principal": {
				"AWS": {CodeDeploy_ARN}
			},
			"Action": [
				"s3:GetObject",
				"s3:ListBucket"
			],
			"Resource": [
				"arn:aws:s3:::{S3_BUCKET_NAME}",
				"arn:aws:s3:::{S3_BUCKET_NAME}/*"
			]
		},
		{
			"Sid": "{자유롭게_입력_중복안됨}",
			"Effect": "Allow",
			"Principal": {
				"AWS": {EC2_INSTANCE_ARN}
			},
			"Action": [
				"s3:GetObject",
				"s3:ListBucket"
			],
			"Resource": [
				"arn:aws:s3:::{S3_BUCKET_NAME}",
				"arn:aws:s3:::{S3_BUCKET_NAME}/*"
			]
		}
	]
}

모두 확인했다면 변경 사항 저장을 클릭하여 완료한다. 

 

appspec.yml 작성

이제 AWS에서의 작업은 완료되었고, 프로젝트 루트 디렉토리에 `appspec.yml`을 생성한다. 이 파일은 CodeDeploy가 각 배포 단계에서 무엇을 수행해야 하는지 명시한다. 이 글에서는 각 요소가 무엇을 의미하는지는 설명하지 않는다. 사실 보면 어떤 역할을 하는지 자세히는 몰라도 어림잡아 알 수 있다. `AppSpec` 파일의 `hooks`에 대한 자세한 설명은 여기를 참고.

version: 0.0
os: linux

files:
  - source: /
    destination: /home/ubuntu/hello
    overwrite: yes

permissions:
  - object: /
    owner: ubuntu
    group: ubuntu
    mode: 755

hooks:
  BeforeInstall:
    - location: scripts/install_dependencies.sh
      timeout: 300
      runas: root
  ApplicationStart:
    - location: scripts/start_server.sh
      timeout: 300
      runas: ubuntu
  ApplicationStop:
    - location: scripts/stop_server.sh
      timeout: 300
      runas: ubuntu

 

scripts 작성

이제 루트 디렉토리에 `scripts` 디렉토리를 생성하고 하위에 `install_dependencies.sh`, `start_server.sh`, `stop_server.sh`를 생성한다. 

install_dependencies.sh

이 파일은 배포 파일(bundle)이 설치되기 전 실행되며, 애플리케이션 실행에 필요한 라이브러리 등을 설치한다. 이 글에서는 간단한 Gradle, Java 애플리케이션을 실행하는 경우를 다룬다.

각 애플리케이션마다 요구되는 의존성이 다르므로 확인 후 여기에 추가 작성하면 된다. 또한 EC2 인스턴스의 AMI(OS)에 따라 명령어가 다르니 그 부분도 잘 확인하자. 

#!/bin/bash

sudo apt update -y

start_server.sh

이 단계는 의존성이나 배포 파일이 설치된 이후에 실행된다. 

#!/bin/bash

cd /home/ubuntu/hello
sudo pkill -f java
nohup java -jar build/libs/*.jar > springboot-app.log 2>&1 &
  1. `.jar` 파일이 있는 위치로 이동
  2. 실행되고 있는 java 프로그램 종료: 새로운 버전을 실행하기 전에 기존 버전 애플리케이션 종료
  3. `nohup`을 이용한 `.jar` 파일 실행 및 로그 기록
    • `nohup`으로 프로그램을 데몬으로 실행
    • 프로그램의 표준 출력(1)과 표준 에러(2)를 `springboot-app.log` 파일에 기록 
      • `2>&1` : 표준 에러를 표준 출력이 기록되는 같은 파일에 리다이렉트함
    • 백그라운드로 실행(&)

stop_server.sh

이 단계는 애플리케이션의 새 버전을 설치하기 전에 실행되므로, 기존 버전을 종료하는 명령어를 작성한다. 지금까지의 순서상으로는 가장 먼저 실행된다. 

#!/bin/bash

sudo pkill -f java

 

deploy.yml 작성

루트 디렉토리에 `.github/workflows/deploy.yml`를 생성한다. 위치만 정확하면 파일 이름은 상관없다. 

 

잠시 확인! 지금까지의 구조!

 

배포할 작업물이 있는 GitHub 레포지토리로 이동하자. 그리고 Settings > Security > Secrets and variables > Actions로 이동하자. `Repository secrets`에 AWS 계정 ID를 환경변수로 등록하자. 

 

이 코드는 Gradle 프로젝트에 맞춰져 있다. 따라서 아래 코드를 복붙하고 상황에 맞춰 잘 수정하자.

각 단계마다 간단하게 주석을 적어놨으니 꼭! 제대로! 확인!

name: Java CI with Gradle

on:
  push:
    branches: [ "main" ]  # main 브랜치에 push 하면 워크플로우 동작
  pull_request:
    branches: [ "main" ]  # main 브랜치에 PR이 발생하면 워크플로우 동작

env:
  AWS_REGION: {AWS_리소스를_생성한_REGION}
  S3_BUCKET: {S3_BUCKET_이름}
  CODEDEPLOY_APPLICATION: {CodeDeploy_Application_이름}
  CODEDEPLOY_DEPLOYMENT_GROUP: {CodeDeploy_배포그룹_이름}
  
jobs:
  # Job 1. 빌드
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
    # Step 1. 소스코드 가져오기
    - name: Checkout code
      uses: actions/checkout@v4

    # Step 2. Java 환경 설정
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'

    - name: Caching gradle dependencies
      uses: actions/cache@v4
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    # Step 3. Gradle 설정
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0

    # Step 4. gradlew 실행 권한 얻기
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    # Step 5. 애플리케이션 빌드
    - name: Build with Gradle Wrapper
      run: ./gradlew clean bootJar

    # Step 6. 빌드한 JAR 파일 저장
    - name: Upload build artifacts
      uses: actions/upload-artifact@v4
      with:
        name: jar-artifact
        path: build/libs/*.jar

  # Job 2. 배포
  deploy:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
    # Step 1. 소스 코드 가져오기
    - uses: actions/checkout@v4

    # Step 2. 이전 단계에서 빌드한 JAR 파일 다운로드
    - name: Download build artifacts
      uses: actions/download-artifact@v4
      with:
        name: jar-artifact
        path: build/libs/

    - name: show all files downloaded  # 어디에 저장되었는지 확인용
      run: |
        ls -al build/libs/
        
    # Step 3. OIDC로 AWS credentials 설정
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::${{secrets.AWS_ACCOUNT_ID}}:role/GitHubActionsRole
        role-session-name: GitHub_to_AWS_via_FederatedOIDC
        aws-region: ${{ env.AWS_REGION }}

    # Step 4. 배포 패키징
    - name: Create deployment package
      run: |
        # Create a temporary directory for deployment package
        mkdir -p deployment-package

        # Create a ZIP file: .jar 파일, AppSpec 파일, scripts 디렉토리 전체
        zip -r deployment-package.zip build/libs/*.jar appspec.yml scripts  

    # Step 5. S3에 패키징 파일 업로드
    - name: Upload to S3
      run: | 
        # Generate a unique filename 
        COMMIT_HASH=$(echo ${{ github.sha }} | cut -c1-7)
        DEPLOYMENT_KEY="deployments/springboot-${COMMIT_HASH}.zip"  # S3 버킷에 deploymenets 객체(디렉토리)가 생기고 그 하위에 springboot-{github_commit_id}.zip 파일이 생긴다.

        # Upload to S3
        aws s3 cp deployment-package.zip s3://${{ env.S3_BUCKET }}/${DEPLOYMENT_KEY}

        # Save the S3 key 
        echo "DEPLOYMENT_KEY=${DEPLOYMENT_KEY}" >> $GITHUB_ENV

    # Step 6. CodeDeploy 트리거
    - name: Deploy with CodeDeploy
      run: | 
        # Create a deployment with CodeDeploy
        DEPLOYMENT_ID=$(aws deploy create-deployment \
          --application-name ${{ env.CODEDEPLOY_APPLICATION }} \
          --deployment-group-name ${{ env.CODEDEPLOY_DEPLOYMENT_GROUP }} \
          --s3-location bucket=${{ env.S3_BUCKET }},key=${{ env.DEPLOYMENT_KEY }},bundleType=zip \
          --deployment-config-name CodeDeployDefault.AllAtOnce \
          --description "Deployment from GitHub Actions - commit ${{ github.sha }}" \
          --query 'deploymentId' \
          --output text)

        echo "Deployment ID: $DEPLOYMENT_ID"
        echo "DEPLOYMENT_ID=${DEPLOYMENT_ID}" >> $GITHUB_ENV

    # Step 7. 배포 완료 대기
    - name: Wait for deployment completion
      run: |
        echo "Waiting for deployment ${{ env.DEPLOYMENT_ID }} to complete..."    

        # Wait for the deployment to finish
        aws deploy wait deployment-successful --deployment-id ${{ env.DEPLOYMENT_ID }}

        # Get deployment status
        STATUS=$(aws deploy get-deployment \
          --deployment-id ${{ env.DEPLOYMENT_ID }} \
          --query 'deploymentInfo.status' \
          --output text)
        
        echo "Deployment completed with status: $STATUS"
        
        if [ "$STATUS" != "Succeeded" ]; then
          echo "Deployment failed!"
          exit 1
        fi

GitHub Actions를 CI로 사용하여 AWS에 배포하는 스크립트는 구글링 하면 많이 나오니, 여기에서는 OIDC 부분만 참고해도 좋다. 

 

확인

여기까지 진행했다면 이번에는 `main` 브랜치에 `push` 해보자! 방금 적은 스크립트에 의해 워크플로우가 트리거 되고, 레포지토리 `Actions` 탭에서 확인할 수 있다. 잘 진행되었다면 아래와 같이 파란 불을 볼 수 있다!

 

위에서 생성한 S3 버킷도 확인해 보면 `deploymenets/springboot-{github_commit_id}.zip`이 있는 것을 확인할 수 있다.

 

CodeDeploy 상태도 확인해 보자. CodeDeploy 애플리케이션에서 배포배포그룹 탭으로 이동하고, 성공이 있는 것을 확인할 수 있다.

 

마지막으로 EC2 인스턴스에 배포된 헬스체크 API를 확인해 보자. Postman이든 브라우저든 창에 아래처럼 입력하자. 

http://{EC2_퍼블릭_DNS_주소}/{health_check_api_endpoint}

 

설정한 대로 잘 나왔다면 진짜 끝!

 

배포 실패의 경우 확인하는 방법

배포가 언제나 성공하면 좋겠지만, 그렇지 않아서... 

 

CodeDeploy > 애플리케이션 > 배포 > 실패한 배포 ID를 클릭하고, 나오는 창에서 하단으로 내리면 배포 수명 주기 이벤트 > `View events` 버튼이 있다(없는 경우도 있는데, 그런 경우에는 `실패한 배포 ID`를 클릭해서 나오는 메시지를 확인하자). 이걸 클릭하면 (그나마) 자세한 실패 원인을 확인할 수 있다.

 

다음으로

여기까지 잘 성공했다면 이제 CI/CD 구축을 완료한 것이다! 물론 한 대의 인스턴스만을 사용하기 때문에 무중단 배포는 아니지만, 일단 CI/CD 구축 성공을 축하하자🎉

 

다음 글에서는 현재 조합에서 AWS Elastic Load Balancing(ELB), AWS AutoScaling Group(ASG)를 추가하여 블루/그린 무중단 배포 하는 방법을 다룰 예정이다. 

 

 

 

 

 

🔖

Comments