스프링 부트와 AWS로 혼자 구현하는 웹 서비스(9)
📌 Travis CI배포 자동화
코드가 푸시되면 자동으로 배포한다.
여러 개발자의 코드가 실시간으로 병합되고 테스트가 수행되는 환경, master 브랜치가 푸시되면 배포가 자동으로 이루어지는 환경을 구축해야 한다.
⚡ CI & CD 소개
- 스크립트를 개발자가 직접 실행함으로써 발생하는 불편 해결
✔ CI(Continuous Integration - 지속적 통합)
- 코드 버전 관리를 하는 VCS 시스템(Git, SVN 등)에 PUSH가 되면 자동으로 테스트와 빌드가 수행되어 안정적인 배포 파일을 만드는 과정
✔ CD(Continuous Deployment - 지속적인 배포)
- 이 빌드 결과를 자동으로 운영 서버에 무중단 배포까지 진행되는 과정
- 하나의 프로젝트를 여러 개발자가 함께 개발을 진행
- 각자가 개발한 코드가 합쳐져야 하는게 번거롭고 힘듦
- 더이상 수동으로 코드를 통합할 필요가 없어지면서 개발에만 집중 가능!
❗ CI 도구를 도입했다고 해서 CI를 하고 있는 것은 아님
- 모든 소스 코드가 현재 실행되고 누구든 현재의 소스에 접근할 수 있는 단일 지점을 유지할 것
- 빌드 프로세스를 자동화해서 누구든 소스로부터 시스템을 빌드하는 단일 명령어를 사용할 수 있게 할 것
- 테스팅을 자동화해서 단일 명령어로 언제든지 시스템에 대한 건전한 테스트 수트를 실행할 수 있게 할 것
- 누구나 현재 실행 파일을 얻으면 지금까지 가장 완전한 실행 파일을 얻었다는 확신을 하게 할 것
(마친 파울러의 블로그 참고 : http://bit.ly/2Yv0vFp)
⚡ Travis CI 연동하기
✔ Travis CI : 깃허브에서 제공하는 무료 CI 서비스(오픈형 웹서비스)
- 젠키스와 같은 CI 도구는 설치형이므로 EC2 인스턴스가 하나 더 필요함
Travis CI 웹서비스 설정
- http://travis-ci/com/ 에서 깃허브 계정으로 로그인
- 계정명 -> Settings -> 저장소 이름 검색
plan, email 인증 등 절차를 거쳐야 함
프로젝트 설정
yml 파일로 진행
✔ 프로젝트에 존재하는 .travis.yml
파일로 설정build.gradle
과 같은 위치에 .travis.yml
생성 후 다음 코드 추가
language: java
jdk:
- openjdk17
branches:
only:
- main
#Travis CI 서버의 Home
cache:
directories:
- '$HOME/.m2/repository'
- '$HOME/.gradle'
script: "./gradlew clean build"
#CI 실행 완료 시 메일로 열람
notifications:
email:
recipients:
- 본인의 email 주소
🔶 branches
- Travis CI를 어느 브랜치가 푸시될 때 수행할 지 지정
🔶 cache
- 그레이들을 통해 의존성을 받게 되면 이를 해당 디렉토리에 캐시하여, 같은 의존성은 다음 배포 때부터 다시 받지 않도록 설정
🔶 script
- main 브랜치에 푸시되었을 때 수행하는 명령어
- 여기서는 프로젝트 내부에 둔 gradlew을 통해 clean & build를 수행
🔶 notifications
- Travis CI 실행 완료 시 자동으로 알람이 가도록 설정
- Travis CI 저장소를 확인해보면 passed 되어 있음
- mail을 확인
⚡ Travis CI와 AWS S3 연동
✔ S3 : AWS에서 제공하는 일종의 파일 서버
- 이미지 파일을 비롯한 정적파일들을 관리하거나 지금 진행하는 것처럼 배포 파일들을 관리하는 등의 기능 지원
- 이미지 업로드를 구현한다면 S3를 사용하는 경우가 많음
Travis CI와 S3 연동
- 실제 배포는 AWS CodeDeploy 서비스 이용
- S3 연동이 먼저 필요한 이유 : Jar 파일은 전달하기 위해서
- AWS S3을 이용하는 이유 : CodeDeploy는 저장 기능이 없어서 Travis CI가 빌드한 결과물을 받아서 CodeDeploy가 가져갈 수 있도록 보관할 수 있는 공간 필요함
AWS Key 발급
- AWS 서비스에 외부 서비스가 접근할 수 없음
- 접근 가능한 권한을 가진 Key를 생성해서 사용해야 함
- IAM(Identity and Access Management)
✔ IAM : AWS에서 제공하는 서비스의 접근 방식과 권한 관리
- Travis CI가 AWS의 S3와 CodeDeploy에 접근할 수 있도록 해보겠음
- AWS 웹 콘솔에서 IAM 검색하여 이동
- 왼쪽 사이드바에서 사용자 -> 사용자 생성
- 이름 설정하고 직접 정책 연결 선택
AmazonS3FullAccess
,AWSCodeDeployfull...
선택- 실제 서비스 회사에서는 권한도 S3와 CodeDeploy를 분리해서 관리하기도 하지만 여기선 간단하게 둘을 합쳐서 관리
- 태그 Name은 본인이 인지 가능한 이름
책에선 처음 생성할 때 생성하면 프로그래밍 방식 엑세스를 허용하는 곳 이 있는데 난 없다..
엑세스 키와 비밀 엑세스 키가 생성된다는데(Travis CI에서 사용될 키) 역시나 ....
AWS S3 연동에는 Access Key & Secret Key를 사용하는 방법과 IAM Role 사용 방법이 있음
더 권장되는 방법은 IAM Role 사용 방법이고,
Access Key & Secret Key 방법은 2024년 하반기부터 지원하지 않을 예정이이라고 함....
-> 아무튼 나는 CLI 선택하고 엑세스키를 생성하긴 했다..
Travis CI에 키 등록
- Settings 밑에
Environment Variables
항목에AWS_ACCESS_KEY
,AWS_SECRET_KEY
등록 .travis.yml
에서$AWS_ACCESS_KEY
,$AWS_SECRET_KEY
이란 이름으로 사용 가능
S3 버킷 생성
- AWS의 S3 서비스는 일종의 파일 서버임
- 파일들을 저장하고 접근 권한을 관리, 검색 등을 지원하는 파일 서버의 역할
- S3는 보통 게시글을 쓸 때 나오는 첨부파일 등록을 구현할 때 많이 이용됨
- 파일 서버의 역할을 하기 때문
✔ Travis CI에서 생성된 Build 파일을 저장하도록 구성
- S3에 저장된 Build 파일은 이후 AWS의 CodeDeploy에서 배포할 파일로 가져가도록 구성할 예정
- AWS에서 S3 검색 -> 버킷 생성
- 버킷 이름 : 배포할 Zip 파일이 모여있는 장소임을 의미하도록 지음
- 퍼블릭 에세스는 모두 차단
- 버전 관리는 별다른 설정을 할 것이 없음
- 생성
.travis.yml
.travis.yml
에 코드 추가
...
before_deploy:
- zip -r springboot-webservice *
- mkdir -p deploy
- mv springboot-webservice.zip deploy/springboot-webservice.zip
deploy:
- provider: s3
access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
secret_access_key: $AWS_SECRET_KEY #Travis repo settings에 설정된 값
bucket: yr-springboot-build #S3 버킷
region: ap-northeast-2
skip_cleanup: true
acl: private #zip 파일 접근을 private로
local_dir: deploy # before_deploy에서 생성한 디렉토리
wait-until-deployed: true
...
.travis.yml 전체 코드
language: java
jdk:
- openjdk17
branches:
only:
- main
#Travis CI 서버의 Home
cache:
directories:
- '$HOME/.m2/repository'
- '$HOME/.gradle'
script: "./gradlew clean build"
before_deploy:
- zip -r springboot-webservice *
- mkdir -p deploy
- mv springboot-webservice.zip deploy/springboot-webservice.zip
deploy:
- provider: s3
access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
secret_access_key: $AWS_SECRET_KEY #Travis repo settings에 설정된 값
bucket: yr-springboot-build #S3 버킷
region: ap-northeast-2
skip_cleanup: true
acl: private #zip 파일 접근을 private로
local_dir: deploy # before_deploy에서 생성한 디렉토리
wait-until-deployed: true
on:
all_branches: true
#CI 실행 완료 시 메일로 열람
notifications:
email:
recipients:
- nir2y@naver.com
🔶 before_deploy
- deploy 명령어가 실행되기 전에 수행됨
- CodeDeploy는 Jar 파일은 인식하지 못하므로 Jar+ 기타 설정 파일들을 모아 압축(zip)함
🔶 zip -springboot-webservice
- 현재 위치의 모든 파일을
springboot-webservice
이름으로 압축(zip)함 - 명령어의 마지막 위치는 본인의 프로젝트 이름이어야 함
🔶 mkdir -p deploy
- deploy라는 디렉토리를 Travis CI가 실행 중인 위치에서 생성됨
🔶 mv springboot-webservice.zip deploy/springboot-webservice.zip
- springboot-webservice.zip 파일을 deploy/springboot-webservice.zip으로 이동시킴
🔶 deploy
- S3로 파일 업로드 혹은 CodeDeploy로 배포 등 외부 서비스와 연동될 행위들을 선언
🔶 local_dir: deploy
- 앞에서 생성한 deploy 디렉토리를 지정
- 해당 위치의 파일들만 S3로 전송
🔶 on: all_branches: true
- 이건 책에 없는데 깃의 기본 브랜치가 master라서 내 main브랜치를 허용하지 않는다고 에러가 나서 추가해줬다..!!
✔ 깃허브로 푸시
- 자동으로 빌드되는지 확인
- 빌드가 성공하는지 확인
⚡ Travis CI와 AWS S3, CodeDeploy 연동하기
✔ 배포 대상인 EC2가 CodeDeploy를 연동 받을 수 있게 IAM 역할을 하나 생성
EC2에 IAM 역할 추가하기
✔ IAM 검색 -> 역할 -> 역할 만들기
- 역할
- AWS 서비스에만 할당할 수 있는 권한
- EC2, CodeDeploy, SQS 등
- 사용자
- AWS 서비스 외에 사용할 수 있는 권한
- 로컬 PC, IDC 서버 등
지금 만들 권한은 EC2에서 사용할 것
✔ AWS 서비스 -> EC2 선택
- 정책에서 EC2RoleForA 검색하여 AmazonEC2RoleforAWS-CodeDeploy 선택
- 태그는 본인이 원하는 이름
- Name : ec2-codedeploy-role
- 역할 이름 : ec2-codedeploy-role
- 역할 생성
✔ EC2 인스턴스 목록 -> 본인의 인스턴스를 마우스 오른쪽을 눌러서 보안설정 -> IAM 역할 수정
-> 역할 선택하고 재부팅
CodeDeploy 에이전트 설치
CodeDeploy의 요청을 받을 수 있게 에이전트 설치
✔ EC2에 접속해서 명령어 입력
aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2
✔ 성공했다면 다음 메시지 출력
download: s3://aws-codedeploy-ap-northeast-2/latest/install to ./install
✔install 파일에 실행 권한 추가
chmod +x ./install
✔ 설치
sudo ./install auto
만약 /usr/bin/env:ruby:No such file or directory 메시지가 출력되면
sudo yum install ruby
- 다시 위에 설치
✔ 설치를 다 했다면 Agent가 정상적으로 실행되고 있는지 상태 검사
sudo service codedeploy-agent status
✔ running 메시지가 출력되면 정상!
The AWS CodeDeploy agent is running as PID xxx
CodeDeploy를 위한 권한 생성
✔ IAM 역할 생성
- AWS 서비스 -> CodeDeploy 선택
- 다 넘어가고 역할 이름 : codedeploy-role (태그도)
CodeDeploy 생성
AWS의 배포 삼형제
- Code Commit
- 깃허브와 같은 코드 저장소의 역할
- 프라이빗 기능을 지원한다는 강점이 있지만, 현재 깃허브에서 무료로 프라이빗 지원을 하고 있어서 거의 사용되지 않음
- Code Build
- Travis CI와 마찬가지로 빌드용 서비스임
- 멀티 모듈을 배포해야 하는 경우 사용해 볼만하지만, 규모가 있는 서비스에서는 대부분 젠킨스/팀시티 등을 이용하니 이거 역시 사용할 일이 거의 없음
- CodeDeploy
- AWS의 배포 서비스
- 앞에서 언급한 다른 서비스들은 대체재가 있고, 딱히 대체재보다 나은 점이 없지만, CodeDeploy는 대체재가 없음
- 오토 스케일링 그룹 배포, 블루 그린 배포, 롤링 배포, EC2 단독 배포 등 많은 기능을 지원
✔ CodeDeploy 서비스 -> 애플리케이션 생성
- 애플리케이션 이름과 컴퓨팅 플랫폼 선택
- 애플리케이션 이름 : springboot-webservice
- 컴퓨팅 플랫폼 : EC2/온프레미스
- 생성
✔ 배포 그룹 생성
- 배포 그룹 이름과 서비스 역할 등록
- 배포 그룹 이름 : springboot-webservice-group
- 서비스 역할 : codedeploy-role
- 배포 유형 : 현재 위치 (만약 배포할 서비스가 2대 이상이라면 블루/그린 선택)
- 환경 구성 : Amazon EC2 인스턴스 체크
- Name : springboot-webservice
- 배포 구성 선택
- 로드밸런싱은 체크 해제
배포 구성이란?
한번 배포할 때 몇 대의 서버에 배포할지 결정
- 2대 이상이라면 1대씩 배포할지, 30% or 50%로 나눠서 배포할지 등등 여러 옵션을 선택
- 1대 서버라면 전체 배포하는 옵션
Travis CI, S3, CodeDeploy 연동
✔ S3에서 넘겨줄 zip 파일을 저장할 디렉토리를 하나 생성
- EC2 서버에 접속해서 다음과 같이 디렉토리 생성
mkdir ~/app/step2 && mkdir ~/app/step2/zip
- Travis CI의 Build가 끝나면 S3에 zip 파일이 전송됨
- 이 zip 파일은 /home/ec2-user/app/step2/zip 로 복사되어 압축을 풀 예정
- Travis CI의 설정은 .travis.yml로 진행
- AWS CodeDeploy의 설정은
appspec.yml
로 진행
appspec.yml
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/app/step2/zip/
overwrite: yes
🔶version: 0.0
- CodeDeploy 버전을 얘기함
- 프로젝트 버전이 아니므로 0.0 외에 다른 버전을 사용하면 오류가 발생
🔶 source
- CodeDeploy에서 전달해 준 파일 중 destination으로 이동시킬 대상을 지정함
- 루트 경로(/)를 지정하면 전체 파일을 얘기함
🔶 destination
- source에서 지정된 파일을 받을 위치
- 이후 Jar를 실행하는 등은 destination에서 옮긴 파일들로 진행됨
🔶 overwrite
- 기존에 있던 파일들이 있으면 덮어쓸지를 결정
- 현재 yes라고 했으니 파일들을 덮어쓰게 됨
.travis.yml
...
- provider: codedeploy
access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
secret_access_key: $AWS_SECRET_KEY # Travis repo settings에 설정된 값
bucket: yr-springboot-build #S3 버킷
key: springboot-webservice.zip #빌드 파일을 압축해서 전달
bundle_type: zip
application: springboot-webservice #웹 콘솔에서 등록한 Codedeploy 애플리케이션
deployment_group: springboot-webservice-group #웹 콘솔에서 등록한 CodeDeploy 배포 그룹
region: ap-northeast-2
wait-until-deployed: true
on:
all_branches: true
- 커밋하고 푸시하면 Travis CI가 자동으로 시작
- Travis CI가 끝나면 CodeDeploy 화면 아래 배포가 수행되는 것을 확인할 수 있음
- 배포가 끝났다면 다음 명령어로 파일들이 잘 도착했는지 확인
cd /home/ec2-user/app/step2/zip
- 파일 목록 확인
[ec2-user@springboot-webservice ~]$ cd /home/ec2-user/app/step2/zip
[ec2-user@springboot-webservice zip]$ ll
total 32
-rw-rw-r--. 1 root root 576 Sep 28 03:23 HELP.md
-rw-rw-r--. 1 root root 110 Sep 28 03:23 appspec.yml
drwxr-xr-x. 9 root root 142 Sep 28 03:26 build
-rw-rw-r--. 1 root root 1538 Sep 28 03:23 build.gradle
drwxr-xr-x. 3 root root 21 Sep 28 03:26 gradle
-rwxrwxr-x. 1 root root 8527 Sep 28 03:23 gradlew
-rw-rw-r--. 1 root root 2868 Sep 28 03:23 gradlew.bat
-rw-rw-r--. 1 root root 43 Sep 28 03:23 settings.gradle
drwxr-xr-x. 4 root root 47 Sep 28 03:26 src
⚡ 배포 자동화 구성
Deploy.sh 파일 추가
- step2 환경에서 실행될 deploy.sh 생성
- scrips 디렉토리를 생성해서 여기에 스크립트 생성
#!/bin/bash
REPOSITORY=/home/ec2-user/app/step2
PROJECT_NAME=springboot-webservice
echo "> Build 파일복사"
cp $REPOSITORY/zip/*.jar %REPOSITORY/
echo "> 현재 구동 중인 애플리케이션 pid 확인"
CURRENT_PID=$(pgrep -fl springboot-webservice | grep jar | awk '{print $1}')
echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID"
if [ -z "$CURRENT_PID" ]; then
echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
fi
echo "> 새 애플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)
echo "> JAR Name: $JAR_NAME"
echo "> $JAR_NAME 에 실행권한 추가"
chmod +x $JAR_NAME
echo "> $JAR_NAME 실행"
nohup java -jar \
-Dspring.config.location=classpath:/application.yml,/home/ec2-user/app/application-oauth.yml,/home/ec2-user/app/application-real-db.yml,classpath:/application-real.yml \
-Dspring.profiles.active=real \
$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
🔶 CURRENT_PID
- 현재 수행 중인 스프링 부트 애플리케이션의 프로세스 ID를 찾음
- 실행 중이면 종료하기 위해서
- 스프링 부트 애플리케이션 이름(springboot-webservice)으로 된 다른 프로그램들이 있을 수 있어 해당 이름으로 된 jar(pgrep -fl springboot-webservice | grep jar) 프로세스를 찾은 뒤 ID를 찾음(| awk '{print $1}')
🔶 chmod +x $JAR_NAME
- Jar 파일은 실행 권한이 없는 상태임
- nohup으로 실행할 수 있게 실행 권한을 부여함
🔶 $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
- nohup 실행 시 CodeDeploy는 무한 대기
- 이 이슈를 해결하기 위해 nohup.out 파일을 표준 입출력용으로 별도로 사용
- 이렇게 하지 않으면 nohup.out 파일이 생기지 않고, CodeDeploy 로그에 표준 입출력이 출력됨
- nohup이 끝나기 전까지 CodeDeploy도 끝나지 않으니 꼭 이렇게 해야 함!!!
BashSupport 플러그인을 받으면 .sh파일을 편집할 때 도움
⚡ .travis.yml 수정
- 현재는 프로젝트의 모든 파일을 zip 파일로 만드는데, 실제로 필요한 파일은 Jar, appspec.yml, 배포를 위한 스크립트들임
- 나머지는 배포에 필요하지 않음
before_deploy 수정
before_deploy:
- mkdir -p before-deploy #zip에 포함시킬 파일들을 담을 디렉토리 생성
- cp scripts/*.sh before-deploy/
- cp appspec.yml before-deploy/
- cp build/libs/*.jar before-deploy/
- cd before-deploy && zip -r before-deploy # before-deploy로 이동 후 전체 압축
- cd ../ && mkdir -p deploy #상위 디렉토리로 이동 후 deploy 디렉토리 생성
- mv before-deploy/before-deploy.zip deploy/springboot-webservice.zip # deploy로 zip 파일 이동
🔶 Travis CI
는 S3로 특정 파일만 업로드가 안됨
- 디렉토리 단위로만 업로드할 수 있기 때문에 before-deploy 디렉토리는 항상 생성함
🔶 ≈before-deploy`에는 zip 파일에 포함시킬 파일들을 저장함
🔶 zip -r
명령어를 통해 before-deploy 디렉토리 전체 파일을 압축
appspec.yml 수정
permissions:
- object:
pattern: "**"
owner: ec2-user
group: ec2-user
hooks:
ApplicationStart:
- location: deploy.sh
timeout: 60
runas: ec2-user
🔶 permissions
- CodeDeploy에서 EC2 서버로 넘겨준 파일들을 모두 ec2-user 권한을 갖도록 함
🔶 hooks
- CodeDeploy 배포 단계에서 실행할 명령어를 지정
- ApplicationStart라는 단계에서 deploy.sh를 ec2-user 권한으로 실행하게 함
- timeout: 60으로 스크립트 실행 60초 이상 수행되면 실패
(무한정 기다리게 둘 수 없음)
🔶 깃허브로 커밋 및 푸시
🔶 Travis CI에서 성공
실제 배포 과정 체험
build.gradle
에서 프로젝트 버전 변경
version '1.0.1-SNAPSHOT'
변경된 내용을 알 수 있게 src/main/resources/templates/index.mustache 내용에 Ver2 텍스트 추가
<h1> 스프링 부트로 시작하는 웹서비스 ver.2</h1>
CodeDeploy 로그 확인
- CodeDeploy와 같이 AWS가 지원하는 서비스에서는 오류가 발생했을 때 로그 찾는 방법을 모르면 오류를 해결하기가 어려움
- 배포가 실패하면 어느 로그를 봐야 할까?
- CodeDeploy에 관한 대부분의 내용은
/opt/codedeploy-agent/deployment-root
에 있음- 해당 디렉토리로 이동한 뒤 목록을 보면
[ec2-user@springboot-webservice step2]$ cd /opt/codedeploy-agent/deployment-root
[ec2-user@springboot-webservice deployment-root]$ ll
total 16
drwxr-xr-x. 7 root root 101 Sep 28 05:44 CodeDeployID
drwxr-xr-x. 2 root root 16384 Sep 28 05:44 deployment-instructions
drwxr-xr-x. 2 root root 46 Sep 28 04:12 deployment-logs
drwxr-xr-x. 2 root root 6 Sep 28 05:44 ongoing-deployment
- 최상단의 영문과 대시(-)가 있는 디렉토리명은 CodeDeploy ID임
- 사용자마다 고유한 ID
- 해당 디렉토리로 들어가보면 배포한 단위별로 배포 파일들이 있음
- 본인의 배포 파일이 정상적으로 왔는지 확인 가능
- opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log
- CodeDeploy 로그 파일
- CodeDeploy로 이루어지는 배포 내용 중 표준 입/출력 내용은 모두 여기에 담겨져있음
- 작성한 echo 내용도 모두 표기됨
정리
- 테스트, 빌드, 배포까지 모두 자동화되었음
- 하지만 문제는 배포하는 동안 스프링 부트 프로젝트는 종료 상태가 되어 서비스를 이용할 수 없음
- 서비스 중단 없이 배포하는 방법이 뭘까?
스프링 부트와 AWS로 혼자 구현하는 웹 서비스_이동욱 지음 책을 읽고 정리하는 글입니다
PREV
NEXT
'프로젝트 > 스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 카테고리의 다른 글
스프링 부트와 AWS로 혼자 구현하는 웹 서비스(10) (1) | 2024.01.22 |
---|---|
스프링 부트와 AWS로 혼자 구현하는 웹 서비스(8) (1) | 2024.01.22 |
스프링 부트와 AWS로 혼자 구현하는 웹 서비스(7) (1) | 2024.01.22 |
스프링 부트와 AWS로 혼자 구현하는 웹 서비스(6) (0) | 2024.01.22 |
스프링 부트와 AWS로 혼자 구현하는 웹 서비스(5) (1) | 2024.01.22 |