BMONZ 란?
BMONZ는 Blog MONsterZ의 약자로 블로그몬의 회원관리 서비스이며 아래 항목을 구현할 예정이다.
서비스 기술 스택
- Springboot 3.2.2
- Java 21 (OpenJDK 21.0.2)
- Gradle 8.5
- Mybatis 3
- Postgresql
서비스 구성도
목표 설계도는 아래와 같다. 정적 리소스들에 대한 처리는 CloudFront와 S3를 이용한 CDN으로 처리하며, BMONZ와 같은 동적 리소스 처리는 Amazon API Gateway와 Lightsail instance를 이용해 처리할 예정이다.

프로젝트 생성 및 로컬 개발환경 설정
BMONZ 어플리케이션 초기 설정
springboot 프로젝트 생성
어플리케이션을 띄우기 위한 최소한의 Dependencies만 추가하여 spring initializr를 이용해 프로젝트를 생성했다.

API 문서 생성 및 관리를 위해 swagger도 Dependencies에 추가했다. springboot v3를 지원하는 springdoc-openapi v2.3.0
로 추가했다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' // swagger-ui
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
테스트 API 생성
단순 동작여부 확인을 위해 테스트용 API를 생성했다.
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/bmonz")
public class MemberController {
@Operation(summary = "BMONZ 회원가입", description = "신규 계정을 생성한다.")
@PostMapping("/signup")
public ResponseEntity<?> signup() {
log.info("loggging test");
return new ResponseEntity<>("signup BMONZ", HttpStatus.OK);
}
}
Slf4j
와 Swagger
가 정상 동작하는지 확인 한 후 다음 스텝으로 진행했다.

Github Repository 연결
Github에서 Public Repository를 생성했다. .gitignore
혹은 README
파일 등은 선택하지 않았다.

위에서 생성한 로컬 프로젝트를 Intellij Terminal을 통해 Github Repository에 push했다.
# Github에서 제공하는 스크립트
echo "# bmonz-api" >> README.md
git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/yourbran/bmonz-api.git
git push -u origin main
로컬 개발환경 구성
BMONZ는 Docker를 이용한 MSA 어플리케이션으로 개발할 것이다. 개발 도구는 intellij를 사용할 것이며 gradle bootBuildImage를 활용하여 Dockerize한다. 생성된 이미지는 Docker Plugin을 이용해 Intellij에서 실행시키며 swagger-ui를 이용해 단위테스트를 진행한다.
Intellij Run Configurations 설정
gradle bootBuildImage --imageName=bmonz:latest
명령어를 이용해 Dockerize한다.

Bind Port
Docker를 실행하기에 앞서 Docker Conatainer와 Host간의 포트를 연결해주어야 한다. intellij Run Configurations에서 설정이 가능하다. 테스트를 위한 것이므로 기본 포트인 8080을 설정했다.

Run on Docker
Docker process가 정상적으로 올라왔고, Swagger-UI 및 Slf4j의 logging 테스트까지 정상적으로 실행되는 것을 확인했다.

CI/CD 파이프라인 구축
개발IDE부터 서버까지의 프로세스 흐름도는 아래와 같다.

BMONZ CI/CD 파이프라인을 구성하기 위해 아래 다섯 가지 절차를 따라 진행했다.
- AWS ECR 생성
- Github 저장소 브랜치 구성
- CodeBuild 구성
- CodeDeploy 구성
- CodePipeline 구성
AWS ECR 생성
배포 단위는 docker image가 될 것이다. 빌드&Dockerize된 이미지를 저장하고 태그 정보를 관리하기 위하여 AWS ECR(ElasticContainerRegistry)을 이용할 것이다.

BMONZ 개발/운영을 목적으로 만드는 저장소이므로 private으로 생성했다.

이렇게 생성된 ECR에는 추후 만들 buildspec.yml을 통해 docker image가 저장될 예정이다.
Github 저장소 브랜치 구성
Build는 PR(Pull Request)을 통해 각 브랜치에 Merged가 될 때에만 작동하도록 제한할 예정이다. 우선 각 Branch는 PR을 통해서만 Merge될 수 있도록 설정한다.
Settings - Branches - Add branch protection rule
위 이미지와 동일하게 develop 브랜치에 대해서도 protection rule을 추가해준다. rule을 추가 했으면 develop 브랜치와 feature 브랜치도 생성한다.

로컬(feature*)에서 개발하고 개발환경(develop)을 거쳐 운영(main)으로 반영될 것이기 때문이다.
CodeBuild 구성
CodeBuild를 통한 빌드 자동화를 구현하기 위해 필요한 작업은 총 다섯 가지이다.
- CodeBuild Project 생성
- buildspec.yml 작성
- CodeBuild 테스트
CodeBuild Project 생성
Webhook을 이용해 Github의 변경사항을 탐지하여 AWS CodeBuild에서 Build를 할 수 있도록 해야 한다. CodeBuild용 GitHub 풀 요청 및 Webhook 필터 샘플를 참고하여 진행했다.
Project name과 Description를 작성한 후 enable build Badge
를 체크했다. build badge는 ‘프로젝트의 최신 빌드 상태를 표시하는 내장 가능형 이미지’로, 보통 저장소 Readme.md 파일에 최신 빌드의 상태를 표시하는 데 유용하다고 한다.

Source 부분에서 나의 Github 저장소와 CodeBuild를 연결한다. OAuth를 통해 인증했고 springboot 프로젝트가 담긴 bmonz-api.git을 지정했다.

소스 변경 후 자동으로 빌드시키기 위해 Webhook
을 선택했다. 개발 서버에 배포될 환경을 구성하는 중이므로 Filter 조건을 지정하여 PR를 통해 develop 브랜치에 Merge를 했을 때만 Build하도록 설정했다.

다음은 CodeBuild를 실행할 환경을 설정해야 한다. BMONZ는 Amazon Linux2 환경의 Lightsail instance에서 실행될 것이며 JAVA 21로 빌드되어야 한다. CodeBuild에서 JAVA 21로 빌드할 수 있는 이미지를 골라야 했고 CodeBuild User Guide를 참고하여 aws/codebuild/amazonlinux2-x86_64-standard:5.0-24.02.08
를 선택했다.

Service role은 신규로 생성했다. 현재 단계에서는 별다른 설정을 하지 않고 추후 단계인 IAM Policy 수정 단계에서 권한을 추가할 예정이다.

Buildspec은 Use a buildspec file
을 선택했다. springboot 프로젝트에도 buildspec.yml을 추가할 예정이다.
buildspec : CodeBuild가 빌드하는 데 사용하는 YAML 형식의 빌드 명령 및 관련 설정의 모음
해당 파일은 일반적으로 소스 디렉토리의 루트에 위치시킨다. 그러나, 배포 환경별로 서로 다른 내용의 buildspec이 필요한 경우가 많고 나는 resource 디렉토리 하위에서 관리하는 것을 선호하므로 buildspec.yml파일의 위치가 추후에는 변경될 가능성이 높다.

Artifacts는 CodeDeploy에 전달해 줄 배포 관련 설정 파일의 묶음으로 이해해야 한다. 실제 서비스 배포 artifact인 Docker Image는 buildspec.yml을 통해 ECR에 push할 예정이다.

CodeBuild AWS 설정을 생성한 후 마지막으로 IAM에 CodeBuild → ECR image push를 위한 정책을 추가해야 한다. AWS 가이드를 따라 아래 정책을 위에서 생성한 Service Role에 추가한다.
{
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:CompleteLayerUpload",
"ecr:GetAuthorizationToken",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:UploadLayerPart"
],
"Resource": "*"
}
아래와 같이 입력하고 저장하면 AWS Console에서의 설정은 끝난다.

이제 프로젝트 소스에서 buildspec.yml을 작성해야 한다.
buildspec.yml 작성
CodeBuild의 빌드 사양 참조를 통해 기본적인 buildspec.yml을 작성한다. 컨셉은 bootBuildImage
로 이미지를 생성한 후 ECR로 이미지를 Push하는 것이다.
파일을 생성하기에 앞서, 개인 브랜치(feature*)를 체크아웃 한 후 해당 브랜치에서 buildspec.yml을 생성한다. feature 브랜치에서 develop 브랜치로 MERGE하면서 CodeBuild를 테스트 할 것이다.
우선, 생성된 이미지를 Push할 ECR에 로그인 해야 한다. CodeBuild의 Edit Environment에서 필요한 환경변수들을 등록한다.

등록된 환경변수들을 사용하여 buildspec.yml에 ECR 로그인 command를 입력한다. BUILD_TAG
는 Github의 어떤 커밋을 기준으로 이미지가 Build 되었는지 확인하기 위한 용도다.
phases:
install:
runtime-versions:
java: corretto21
pre_build:
commands:
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- BUILD_TAG=${COMMIT_HASH:=latest}
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
build:
commands:
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker image...
- docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$BUILD_TAG
build
: bootBuildImage
명령어를 명시하여 springboot 프로젝트를 Dockerize한다.artifacts
: 추후 배포 단계에서 사용될 파일들을 지정하는 곳이다. appspec.yml
은 CodeDeploy에서 사용할 설정파일이며 그 외 sh파일은 배포 서버인 lightsail에서 사용될 스크립트이다. 해당 파일들은 S3에 저장된다.cache
: 빌드 시 변경 사항이 없는 의존성들은 S3에 저장된 캐시를 이용한다.
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- chmod +x gradlew
- ./gradlew bootBuildImage --imageName=$IMAGE_NAME:$IMAGE_TAG
- docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$BUILD_TAG
...
# CodeDeploy 연계를 위한 appspec 및 scripts 생성
## appspec은 root directory에 위치해야 하므로 discard-paths 적용
artifacts:
files:
- scripts/common/ecr-login.sh
- scripts/$CONFIG_ENV/docker-restart.sh
- scripts/$CONFIG_ENV/appspec.yml
discard-paths: yes
...
cache:
paths:
- '/root/.gradle/caches/**/*'
작성이 완료된 buildspec.yml을 Github에 push한 후 feature 브랜치를 develop으로 MERGE 해보자.
CodeBuild 테스트
위 설정들이 정상적으로 적용되었다면, Github에서 PR로 인한 MERGE 시 CodeBuild를 통해 빌드가 수행된다.

CodeBuild가 완료되면 buildspec.yml에 작성되었던 post_build
를 통해 자동으로 ECR로 docker image가 push된다.

CodeDeploy 구성
CodeDeploy를 이용하기 위해서는 아래 컴포넌트들에 대한 구성이 필요하다.
- IAM 권한 작업
- On-premises instance 설정
- CodeDeploy Agent 설정
- CodeDeploy Application 생성
IAM 권한 작업
배포를 위해서는 ECR, S3, Lightsail 모두에 접근 가능해야 하며 그에 맞는 권한이 있어야 한다.
CodeDeploy Service Role 생성
CodeDeploy가 사용할 서비스 역할에 포함된 정책은 다음과 같다.
- AmazonEC2ContainerRegistryReadOnly : ECR에 접근하여 배포 이미지 pull
- AmazonS3FullAccess : artifacts파일 다운로드
- AWSCodeDeployFullAccess : CodeDeploy 사용
- AWSCodeDeployRole : CodeDeploy 사용

IAM User 생성
Lightsail은 EC2와는 달리 인스턴스에 Role을 직접 할당할 수 없다. 따라서 User를 생성하여 정책을 매핑시킨 후 이 User를 Lightsail에서 사용해야한다.

On-premises instance 설정
EC2의 경우 CodeDeploy에서 바로 지정할 수 있지만 Lightsail은 그렇지 않다. 따라서, 배포 대상 서버인 Lightsail를 CodeDeploy에서 식별할 수 있도록 하기 위해 instance에 태그를 지정해야 한다.

그 다음엔 배포 서버에 앞서 만든 IAM User를 적용하고 해당 서버(Lightsail instance)를 AWS의 on-premise 서버로 등록해주어야 한다. AWS CLI를 통해 작업한다.
# 1. 배포 서버에 IAM User 등록
$ aws configure
## 엑세스 키 입력
## 비밀 키 입력
## iam_usr_arn 입력
## region 입력
# 2. aws on-premise-instace(배포 서버, ligttsail) 등록
# --instance-name : Lightsail 인스턴스명
$ aws deploy register-on-premises-instance --instance-name AmazonLinux --iam-user-arn arn:aws:iam::XXXXXXXX:user/LightsailCodeDeployBmonz --region ap-northeast-2
# 3. 배포 서버에 대한 정보로써 태그를 등록 (해당 태그를 이용해 CodeDeploy가 배포 위치를 찾음)
# --tags : Lightsail 웹에서 등록한 Tag정보를 그대로 입력
$ aws deploy add-tags-to-on-premises-instances --instance-names AmazonLinux --tags Key=Name,Value=CodeDeployBmonzLightsail --region ap-northeast-2
CodeDeploy Agent 설치
배포 서버에서 AWS CodeDeploy에 신호를 받아 실제 스크립트를 수행할 agent를 설치한다. (Amazon Linux용 CodeDeploy 에이전트 설치 또는 RHEL)
설치가 완료 후, CodeDeploy가 실행될 때 필요한 권한들을 이용할 수 있도록 사전에 만들었던 IAM 유저 정보를 입력해준다.
mkdir /etc/codedeploy-agent/
mkdir /etc/codedeploy-agent/conf
cat <<EOT >> /etc/codedeploy-agent/conf/codedeploy.onpremises.yml
---
aws_access_key_id: <Access Key ID>
aws_secret_access_key: <Secret Access Key>
iam_user_arn: <IAM User ARN>
region: <Desired Region>
EOT
cd /etc/codedeploy-agent/conf/ && ls
# 설정 파일들에 대한 onwer가 누구인지에 따라 다르겠지만,
# 필요할 경우 해당 설정 파일의 '읽기 권한'에 대한 수정이 필요할 수 있다.
codedeployagent.yml codedeploy.onpremises.ym
배포 시그널을 받아 실제 스크립트를 수행해 줄 Agent설치까지 완료되었다. 이제 AWS에서 CodeDeploy 서비스를 생성해야 한다.
CodeDeploy Application 생성
기본 설정정보는 아래 이미지와 같다. 중요한 점은 앞서 지정한 Lightsail의 Name 태그 정보를 On-premises instances 환경 설정값으로 넣어주어야 한다.

CodePipeline 구성
CodeBuild와 CodeDeploy 구성이 끝났다. 이제 이 둘을 이어줄 CodePipeline을 구성해야 한다. CodeBuild를 통해 Artifacts가 생성되면 변경사항을 탐지하여 CodeDeploy를 실행시켜주는 파이프라인을 구성하는 단계다.
- CodePipeline Source edit 설정
- CodePipeline Deploy edit 설정
- CodePipeline 테스트
CodePipeline Source edit 설정
CodeBuild를 통해 S3 버킷에 생성되는 artifacts를 지정해준다. artifacts는 CodeDeploy를 실행시킬 때 필요한 설정정보 및 배포 스크립트가 포함되어 있다. 이 배포 스크립트를 이용해 ECR에서 배포 이미지를 pull 할 것이다.

- S3 object key : CodeBuild에서 생성한 artifacts object key
- Output artifacts : ECR을 통한 이미지 배포 시 Default 세팅인 ‘SourceArtifact’를 이용해야 함
CodePipeline Deploy edit 설정
앞서 생성한 CodeDeploy를 불러와 적용한다.

- Input artifacts : 기본값인 ‘SourceArtifact’를 선택
- Application name : CodeDeloy구성 시 생성한 Application 선택
- Deployment Group : CodeDeloy구성 시 생성한 Application 선택
CodePipeline 테스트
모든 구성이 완료되었다. 이젠 Github에서 PR에 대한 Merge발생 시 어플리케이션 서버로 도커 이미지가 배포되는지 확인한다.
Intellij에서 테스트를 위한 코드를 수정하여 Commit&push 후 PR을 생성한다. Github에서 생성된 PR에 대해 Merge를 수행한다.

CodeBuild에선 Github Webhook을 통해 전달받은 API로 인해 Build 작업이 수행된다.

CodeBuild에서 Docker Image를 ECR에 push하고 S3에 artifacts를 저장하면 CodePipeline에서 S3의 변경사항을 탐지하여 CodeDeploy를 수행시킨다.

CodePipeline이 정상적으로 수행되고 난 후 어플리케이션 서버에서 docker ps
를 통해 Docker Image가 정상적으로 배포되었는지 확인해본다.

마지막으로 어플리케이션 변경사항이 정상적으로 반영되었는지 확인한다.
