일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 데이터베이스
- 파이썬
- 스프링시큐리티
- springsecurity
- 프로그래머스
- 도커
- 스프링
- AWS
- 오블완
- 스프링부트
- 트랜잭션
- 소셜로그인
- 티스토리챌린지
- docker
- githubactions
- 메시지
- Spring
- 백준
- yaml-resource-bundle
- oauth2
- java
- 재갱신
- CI/CD
- 토이프로젝트
- 국제화
- 리프레시토큰
- JIRA
- springsecurityoauth2client
- springdataredis
- 액세스토큰
- Today
- Total
땃쥐네
[토이프로젝트] 게시판 시스템(board-system) 4. 지속적 무중단 배포(AWS EC2, GitHub Actions, Docker, Nginx) 본문
[토이프로젝트] 게시판 시스템(board-system) 4. 지속적 무중단 배포(AWS EC2, GitHub Actions, Docker, Nginx)
ttasjwi 2024. 9. 29. 18:37
이번 글에서는
브랜치가 푸시될 때마다 프로젝트 작업물이 EC2에 무중단으로 배포될 수 있도록 해보겠습니다.
1. GitHub Actions 에 DockerHub / EC2 접근 자격증명 부여
이전 글에서 지속적 통합을 GitHub Actions 를 통해 했듯, 지속적 배포 역시 GitHub Actions 를 통해 수행할 것입니다.
그런데 이전 글에서는 DockerHub, EC2 에 접근하는 작업을 하지 않았지만 이번에는 접근하는 작업을 해야합니다.
제 DockerHub 에 도커 이미지를 push 한다거나, EC2 에 SSH 에 접속한다거나 하는 작업에 있어서는 몇 가지 자격증명이 필요합니다. 이것들을 우선적으로 준비해보겠습니다.
1.1 [GitHub Actions] DockerHub 액세스 토큰 준비
GitHub Actions 에서 우리 프로젝트를 빌드하고,
그 빌드 결과물인 jar 를 기반으로 하여 도커 이미지를 생성하고 이를 DockerHub 에 푸시를 해야하는데요.
이 과정에서는 DockerHub 리포지토리에 대한 푸시 권한이 있는지 자격증명이 필요합니다.
Docker 에 접속하여, 우측 상단의 프로필 사진을 클릭하고, Account settings 에 접근합니다.
하단에 보면 Security 란이 있습니다.
Personal Access Tokens 를 통해 액세스토큰을 발급받아봅시다.
(Two-factor authentication 은 아이디/패스워드로만 접근할 수 있게 하는걸 넘어서, 별도의 인증을 추가로 하여 인증할 수 있게 하는 것인데 보안 강화 관점에서 놓고보면 하는게 좋습니다. 근데 이번 글에서는 그 부분을 다루진 않겠습니다. 저도 설정은 하지 않았는데 이후 추가적으로 설정할 예정입니다.)
Generate new Token
액세스 토큰을 설명하는 부분, 그리고 권한을 어디까지 허가할 것인지에 대해 지정하는 부분입니다.
push하는 권한까지 주도록 read/write 로 지정했습니다.
이렇게 하면 액세스 토큰이 발급되는데요.
이 액세스토큰은 남들이 접근할 수 없는 안전한 곳에 잘 저장해서 보관해둡시다.
혹시 분실했거나 유출됐다고 생각하면 만료시키고 재발급하면 됩니다.
Github 리포지토리의 settings 에 들어가보면 이렇게 Secrets and variables가 있는데요.
여기서 리포지토리 secret을 추가하겠습니다. New repository secret 을 통해 secret 을 추가합니다
이렇게 Secret 으로 추가할 대상의 이름과 그 실제 값을 기입하는 부분입니다
이름은 보통 상수로 취급되므로 ABCD_EFGH 형식(스네이크 케이스) 으로 저장해주면 됩니다.
저는 여기서 Docker Hub 의 아이디(Username)과 액세스토큰(AccessToken)을 알아보기 쉬운 이름으로 저장했습니다.
참고로 Secret 으로 저장한 값은 이후 다시 조회할 수 없고 수정만 가능합니다.
Github actions가 Docker 에 push를 하기 위해서는 사용자 아이디 및 패스워드가 필요한데 우리의 password 를 직접 전달하는 것보다 접근 권한이 있는 토큰을 전달하는 것이 좀 더 보안상 안전하기 때문에 이 방식을 사용했습니다.
1.2 [GitHub Actions] EC2 SSH 키 등록
이전에 만들어둔 SSH Private 키가 있어야 EC2에 SSH로 접근할 수 있기 때문에 이 값 역시 Secret 으로 등록해야합니다.
여기서 주의할 점은 저기
----- BEGIN ... 부터 해서 --- PRIVATE KEY----- 이 부분도 같이 복사해서 넣어주셔야 합니다
EC2 SSH Key 를 등록하고 EC_USERNAME 도 같이 등록합니다.
SSH 접속 시 뜨는 사용자 이름 있죠? 그 값을 넣어줍니다.
amazon linux 이면 기본적으로 ec2-user 이고, ubuntu 이시면 ubuntu 로 알고 있어요.
1.3 [AWS] EC2 보안그룹 수정
지지난 글에서, EC2 에 대한 SSH 접근권한을 모든 IP 에서 접근 가능하도록 했었는데
아무 IP에서 접근가능한 것은 매우 보안상 위험한 일입니다. 따라서 인바운드 규칙을 약간 수정해보도록 하겠습니다.
'인바운드 규칙 편집' 에 들어가 접근 설정을 변경해보겠습니다
SSH 로 들어오는 사용자 IP 를 내 IP 로 변경하면
등록 시점의 IP 로만 SSH 접근이 가능해집니다.
특정 IP 로만 지정하는 것도 가능합니다.
저는 집, 카페, 도서관 등을 오가는데 이 상황에 맞춰서 IP 를 그 때 그때 바꿔서 사용했어요.
근데 문제는 이렇게 바꿔놓으면 GitHub Actions 에서는 SSH 를 통해 EC2에 접근할 수 없게 됩니다.
그렇다고 항상 GitHub Actions 의 IP를 계속 열어두는 것도 보안상 위험해지고요.
GitHub Actions 의 배포 스크립트가 실행될 때만 잠깐 GitHub Actions 에 대해 SSH 접근 권한을 갖도록 보안그룹 규칙을 수정하고, 배포 작업이 끝날 때 다시 SSH 접근 권한을 갖지 못 하도록 변경할 수 있도록 하면 좋을 것 같습니다.
1.4 [GitHub Actions] EC2 보안그룹 설정 접근권한 부여
AWS 계정에서 우측 상단의 보안자격증명(IAM)에 들어가볼게요.
액세스 관리 > 사용자 생성
이와 같이, 사용자 이름을 지정하여 생성하는데 우리가 직접 수동으로 접속하는 계정이 아니므로 패스워드는 생성하지 않겠습니다.
사용자에 AmazonEC2FullAccess 권한을 부여하겠습니다.
이렇게 EC2 관련 권한을 가진 사용자가 생성됐습니다.
해당 사용자 상세 페이지 > 보안 자격 증명 > 액세스 키 만들기
일단 AWS에서는 액세스 키 대신 다른 방식(IAM Roles AnyWhere) 을 권장하기는 하는데...
장기자격증명이 편하기 때문에 액세스토큰을 쓰겠습니다.
이어서 사용목적을 적당히 기술하고 나면 액세스키를 얻을 수 있습니다.
access key, secret_access_key 를 잘 저장해두고 향후 사용하도록 합시다.
Repository Secrets 에 보안 요소를 추가했습니다.
AWS_ACCESS_KEY_ID : 아까 받은 액세스 키
AWS_SECRET_ACCESS_KEY : 아까 받은 시크릿 액세스 키
AWS_REGION : 리전명(ap-north-east-2)
EC2_SECURITY_GROUP_ID : EC2 보안그룹 id
이 자격증명을 사용하면 EC2 에 관련된 AWS API 를 사용할 수 있습니다. 보안그룹을 수정하는 것도 가능해지죠.
1.5 그 외 추가한 변수들
그 외 제가 따로 설정한 값들을 정리해봤어요.
개발자 필요에 따라 추가적으로 정의해서 넣을 수 있고 필요없다 생각되는건 빼도 괜찮습니다.
- APPLICATION_BLUE_YML : blue 프로필 에 대한 설정파일입니다. 아래에서 후술합니다.
- APPLICATION_GREEN_YML : green 프로필에 대한 설정파일입니다. 아래에서 후술합니다.
- AWS_ACCESS_KEY_ID : AWS EC2 보안그룹 설정에 접근하기 위한 액세스 키입니다.(위에서 설정함)
- AWS_REGION : AWS 의 리전명입니다. 저는 ap-northeast-2 를 지정했어요.
- AWS_SECRET_ACCESS_KEY: AWS EC2 보안그룹 설정에 접근하기 위한 시크릿 키입니다. (위에서 설정함)
- BLUE_PORT : blue 서버가 배포될 포트입니다. 저는 8081 을 지정했어요
- DOCKERHUB_ACCESS_TOKEN : 도커허브 액세스 토큰입니다. (위에서 설정함)
- DOCKERHUN_USERNAME: 도커허브 아이디입니다.
- EC2_SECURITY_GROUP_ID : EC2에 대한 보안그룹 식별자입니다. (위에서 설정함)
- EC2_SSH_KEY : EC2 접근을 위한 SSH Key 값입니다. (위에서 설정함)
- EC2_SSH_PORT : EC2에 접근할 때 SSH 포트를 몇 번으로 해서 접근할 것인지의 설정입니다. 저는 22번 포트로 설정했어요.
- EC2_USERNAME: EC2 에서 명령을 실행할 사용자인데 저는 아마존 리눅스를 쓰므로 ec2-user 를 설정했어요 (ubuntu linux 이면 ubuntu 를 해야합니다.)
- GREEN_PORT : green 서버가 실행될 포트입니다. 저는 8082 를 지정했어요
- LIVE_SERVER_IP : EC2의 퍼블릭 ip 를 지정했어요. (위에서 설정함)
2. HealthCheck API 구성
배포 절차에 들어가기전에, 배포의 편의를 위해 health-check 를 도와줄 api를 하나 만들어둘거에요.
blue, green 서버를 띄워 관리를 할 건데 현재 살아있는 서버가 blue 인지 green 인지 알 수 있도록 하기 위해서 필요한 api 입니다.
blue 라면 새로 띄울 서버는 green 이 되겠고
green 이라면 새로 띄울 서버는 blue가 되도록 말이죠.
스프링 프로젝트를 건들여볼게요.
2.1 브랜치 생성
이전 PR이 푸시되었지만 로컬 master 는 여전히 이전 위치죠.
origin/master 쪽으로 master를 merge 시킨 뒤(앞의 커밋이므로 뒷쪽에 포인터가 옮겨지는데 이를 fast-forward merge 라고 합니다.)
이 곳에서 브랜치를 생성합니다.
참고로 이전에 작업했던 작업물인 CI 기능 브랜치는 지워도 상관 없습니다.
팀내 규약에 따라 유지는 하고 싶다면 그대로 냅둬도 괜찮습니다.
2.2 프로필 설정
기존에 작성했던 부분은 local 에서만 실행되는 것을 고려해서 작성됐는데
이번에는 우리 프로젝트를 여러 환경에서 다르게 실행할 수 있도록 프로필 설정을 해볼거에요.
resources 아래에 위와 같이 파일들을 생성합니다.
main - application.yml / application-blue.yml / application-green.yml / application-local.yml
test - application-test.yml
# main/resources/application.yml
spring.application.name: board-system
spring:
profiles:
active: local
---
# main/resoureces/application-local.yml
spring.config.activate.on-profile: local
server:
port: 8080
profile: local
main application.yml 은 기본적으로 local 프로필로 실행될 수 있도록 해뒀고(외부에서 local 말고 다른 값을 전달해주면 그 값으로 실행 가능해져요.)
application-local.yml 에는 local 프로필일 때만 적용할 설정들을 모아둡니다.
# main/resources/application-blue.yml
spring.config.activate.on-profile: blue
server:
port: 8080
profile: blue
# main/resources/application-green.yml
spring.config.activate.on-profile: green
server:
port: 8080
profile: green
application-blue.yml 및 application-green.yml 파일에는 각 환경별 민감한 정보를 담아두도록 하겠습니다
토이프로젝트 관점에서는 그닥 위험하지 않게 느껴보이는 값들이긴 합니다만
이게 실무라고 생각해보면 노출되서 좋을 건 없으니까요.
### secret properties files ###
/src/main/resources/application-blue.yml
/src/main/resources/application-green.yml
루트 디렉토리의 .gitignore 쪽에는 blue, green 설정 파일을 열외시키도록 해서 버전 관리 대상에서 제거합니다.
cat src/main/resources/application-blue.yml | base64
git bash 같은 터미널을 사용하신다면 이렇게 파일의 내용물을 base64 인코딩할 수 있습니다.
이런 도구를 사용할 수 없다면 웹의 도구를 사용하셔도 무방합니다
이 BASE64 인코딩된 값을 SECRET에 포함하여 관리하도록 할게요.
# test/resources/application.yml
spring.application.name: board-system-test
spring:
profiles:
active: test
---
# test/resources/application-test.yml
spring.config.activate.on-profile: test
server:
port: 9090
profile: test
test 모듈 쪽 설정은 새 애플리케이션을 구성한다는 느낌으로 루트 application.yml 부터 다시 구성했습니다.
@ActiveProfiles("test")
@SpringBootTest
class MainTests {
@Test
fun contextLoads() {
}
}
Spring Boot를 끌어오는 테스트의 경우 명시적으로,
@ActiveProfiles 어노테이션을 통해 test 프로필로 지정해서 작동되게 할거에요.
이대로 브랜치를 Push 한 뒤 PR 을 작성해서 빌드 테스트가 돌아가는지 확인해보면 잘 되네요.
2.3 HealthCheck API 개발
package com.ttasjwi.board.system
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
@ConfigurationPropertiesScan
@SpringBootApplication
class Mainfun main(args: Array<String>) {
runApplication<Main>(*args)
}
Main 클래스에서는 어노테이션 @ConfigurationPropertiesScan 을 활성화하여, 프로퍼티 클래스 역시 스캔의 대상으로 삼도록 합니다
package com.ttasjwi.board.system.deploy.config
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.ConstructorBinding
@ConfigurationProperties(prefix = "server")
class DeployProperties
@ConstructorBinding constructor(
val profile: String,
)
설정 파일의 값을 바인딩할 프로퍼티 클래스 DeployProperties 객체를 등록합니다.
@RestController
class HealthCheckController(
private val deployProperties: DeployProperties,
){
@GetMapping("/api/v1/deploy/health-check")
fun healthCheck(): ResponseEntity<String> {
return ResponseEntity.status(HttpStatus.OK)
.body(
deployProperties.profile
)
}
}
컨트롤러입니다.
현재 살아있다면, 200 상태코드와 함께 현재 프로필 이름을 문자열로 반환하도록 했습니다.
(+ 잘 생각해보면 현재 살아있는지 여부, 그리고 이게 blue 인지 green 인지 여부가 공개적으로 노출되는 상황입니다.
이 부분은 향후 추가적인 코드를 작성해서 보안을 강화할 예정입니다.)
package com.ttasjwi.board.system.deploy.api
import com.ttasjwi.board.system.deploy.config.DeployProperties
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
@ActiveProfiles("test")
@WebMvcTest(controllers = [HealthCheckController::class])
@EnableConfigurationProperties(DeployProperties::class)
@AutoConfigureMockMvc
@DisplayName("HealthCheckController 중형 테스트: 스프링 MVC와 컨트롤러가 잘 결합하여 동작하는가?")
class HealthCheckControllerMiddleTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Test
@DisplayName("healthCheck : 현재 서버가 살아있다면 200 상태코드와 함께 활성화된 프로필을 문자열로 응답한다")
fun healthCheckTest() {
mockMvc
.perform(get("/api/v1/deploy/health-check"))
.andDo(print())
.andExpectAll(
status().isOk,
content().contentType("text/plain;charset=UTF-8"),
content().string("test")
)
}
}
테스트 코드입니다.
스프링부트 MVC 와 결합하여, 컨트롤러 기능이 잘 되는지 테스트하기 위해 HealthCheckControllerMiddleTest 클래스를 작성했어요.
EnableConfigurationProperties 어노테이션을 통해 ServerProperties 속성을 함께 활성화 시켰습니다.
로컬에서 테스트 폴더를 실행할 떄 More > Run/Debug > Run with Coverage > Test in ... 을 클릭하면
테스트를 돌리고 대강적인 테스트 커버리지도 눈에 보여주는데요.
클래스, 메서드, 라인 커버리지, 브랜치 커버리지에서 100% 가 나오네요. 굿굿
커밋을 push 해보면 빌드테스트가 돌아가고 잘 되는 것을 확인할 수 있네요.
3. Dockerfile 를 통해 도커 이미지 빌드
gradle 을 통해 빌드된 jar 파일을 기반으로 Docker 이미지를 만들어보도록 하겠습니다.
FROM amazoncorretto:21-alpine3.20-jdk
ARG JAR_FILE=build/libs/*.jar
ARG PROFILES
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java -DSpring.profiles.active=${PROFILES} -Dspring.config.location=classpath:/,file:/app/config/board-system/ -jar app.jar"]
프로젝트 루트 경로에 Dockerfile 이름으로 파일을 만듭니다.
amazoncorretto의 21-alpine-jdk 이미지를 기반으로 java 를 실행하는 이미지입니다.
외부에서 build/libs/*.jar 패턴에 매칭되는 jar 파일을 가져와서 java 를 실행시킬건데요.
-DSpring.profiles.active=${PROFILES} 를 통해 프로필 값을 외부에서 전달시켜 실행하고
-DSpring.config.location=classpath:/,file:/app/config/board-system/ 을 통해 클래스패스 설정파일 뿐 아니라 외부 설정파일 폴더를 함께 하여 실행해요.
근데 여기서 문제는 build 과정에서 실행 불가능한 jar 파일인 xxx-plain.jar 도 JAR 파일 매칭 대상에 포함될 수 있다는 점인데
tasks.getByName("bootJar") {
enabled = true
}
tasks.getByName("jar") {
enabled = false
}
이 부분은 build.gradle.kts 에서 위 문구를 추가해서 해결할거에요.
이렇게 하면 plain jar 는 생성되지 않을거에요.
gradle 에 대해 clean, build 명령을 통해 빌드를 해보면 plain jar는 만들어지지 않네요.
$ docker build -t board-be .
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
board-be latest cfe5147834c4 44 minutes ago 343MB
프로젝트 루트경로에서 이제 이 명령을 입력하면 도커 이미지가 빌드됩니다.
-t 는 tag 옵션인데 board-be 이름으로 이미지를 만들라는 명령이에요.
. 은 현재 경로의 Dockerfile 을 기반으로 도커 이미지를 만들라는 명령이고요.
$ docker run -d -p 8080:8080 -e PROFILES=local --name local board-be
1855ca3e19ac55c998866a763d52c6e1a4f4d584702e3a43b42ef1e61d7b108f
이렇게 이미지를 빌드하고 이미지를 기반으로 컨테이너를 한번 띄워봅시다.
docker run 은 컨테이너를 실행하라는 명령이고,
-d 옵션은 백그라운드에서 실행
-p 8080:8080 은 외부포트 8080 / 내부포트 8080 연결 옵션
-e PROFILES=xxx 는 외부에서 PROFILE 값을 전달받는 옵션
--name 은 이 컨테이너의 이름을 지정하는 옵션
뒤의 board-be는 board-be 이미지를 사용하라는 옵션
입니다.
헬스체크 엔드포인트에 접속하면 local 프로필로 잘 실행된 것을 볼 수 있어요.
4. Nginx 컨테이너 실행 및 설정
EC2 인스턴스에 Nginx 컨테이너 서버를 띄우고, 설정을 구성해보겠습니다.
Nginx 는 향후 사용자로부터 들어오는 요청을 받고 뒤에 있는 백엔드 서버로 포워딩 시키는 역할을 할겁니다.
4.1 Nginx 컨테이너 실행
docker pull nginx
# nginx 이름의 컨테이너를 실행해라 / 백그라운드에서 / 외부 80번 포트 - 내부 80 포트 / nginx 이미지로
docker run --name nginx -d -p 80:80 nginx# bash 쉘로 nginx 컨테이너 접속
docker exec -it nginx bash
우선 EC2 인스턴스에서 nginx 이미지를 끌어다 와서 실행합니다.
외부 포트 80번 포트를 내부 포트 80번에 연결하였습니다. 이제 EC2 외부 포트 80으로 들어오는 요청(HTTP)은 Nginx 서버로 가게 됩니다.
여기까지 하면 public ip 로 서비스 접근 시, 이런 화면을 볼 수 있습니다.
4.2 Nginx 설정파일 수정
# vim 설치
apt update
apt upgrade -y
apt get vim -y
cd etc/nginx/conf.d
vim default.conf
nginx 설정을 수정하기 앞서, vim 을 설치합니다.
Nginx 의 여러가지 설정 파일은 etc/nginx/conf.d 에 위치해있는데요. 여기서 default.conf 파일을 수정합니다
# 로드밸런싱 그룹 - blue 그룹 (세미콜론 써야함 주의)
upstream blue {
server (e2-private-ip):8081;
}
# 로드밸런싱 그룹 - green 그룹
upstream green {
server (e2-private-ip):8082;
}
server {
include /etc/nginx/conf.d/service-env.inc;
location / {
proxy_pass http://$service_url;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}
}
좀 많은 설정값들이 있을텐데, 이렇게 값을 추가해주시면 됩니다.
다른 값은 지우거나 건드시지 말고 저렇게 추가해주시면 됩니다.
- upstream 설정 부분
- 로드밸런싱 그룹을 설정하는 부분입니다.
- 우리는 blue, greene 그룹을 설정했고 각 그룹에 한대의 서버들만 뒀습니다.
- nginx 컨테이너 서버 입장에서는 바깥에 위치한 blue, green 서버의 주소를 모르므로 이를 ec2의 private ip 기준으로 하여 잡아줍시다.
- server 설정
- include : 외부 설정파일을 설정에 포함
- 여기에 $service_url 변수값 설정을 두고 향후 동적으로 blue, green 값을 변경해가면서 블루그린 배포를 가능하게 할 예정입니다.
- location / : / (루트) 로 들어온 요청을 어떻게 처리할 지 설정하는 부분
- proxy_pass 설정 : 요청을 대상 서버로 포워딩 시킵니다.
- proxy_set_header : 대상 서버에 보낼 때 헤더 추가 설정
- X-Real-IP : 클라이언트의 실제 IP 주소
- X-Forwarded-For : 클라이언트 ip 주소 및 경유한 프록시 ip 주소들…
- Host: 대상 서버 url
- include : 외부 설정파일을 설정에 포함
Nginx 는 뒷단에 있는 서버로 요청을 포워딩시키는, 프록시 역할을 수행합니다. 따라서 혹시 서버에서 문제가 생겼는데 우리가 개발한 애플리케이션에 문제가 없어보이는데 뭔가 문제가 발생했다면 Nginx 서버를 의심하고 이 설정파일을 변경하는 것을 고려하시면 되지 않을까 합니다.
vim service-env.inc
위에서 service-env.inc 파일을 궁금해하실텐데요. 이제 이 파일을 만들어봅시다.
set $service_url green;
위와 같이 파일을 지정합니다.
현재는 $service_url 변수값이 green 이지만 향후 동적으로 외부에서 blue, green 으로 수정할 수 있습니다.
우리는 아까 default.conf 파일에서 include 문을 통해 설정 파일을 끌어오는 것을 지정했습니다.
실제로, nginx 서버는 리로드 될때마다 include 문을 읽고 해당 파일을 읽어와 설정을 실행합니다.
우리가 service-env.inc 의 값을 수정할 때마다, default.conf 파일에서 $service_url 값이 동적으로 변할 수 있겠죠?
그리고 우리가 upstream 에 설정한 server 주소 값이 대입되어져서 blue, green 이 바뀔때마다 nginx 가 가리키는 대상 서버가 달라지게 될겁니다.
(+ 그림에서 blue, green은 docker 컨테이너로 표시했는데 향후 띄울 서버들 역시 도커 컨테이너로 띄울 계획이라서 그렇습니다.)
cat service-env.inc
set $service_url green;
cat 명령어를 통해 파일에 있는 값이 잘 의도한 대로 들어가 있는지 확인합니다.
이렇게 하면 nginx 에 설정은 거의 끝났습니다.
혹시 실수를 해서 망했다면? Nginx 컨테이너를 죽여버리고 다시 Nginx 컨테이너 띄우고 처음부터 하면 됩니다.
도커를 써서 좋은 점이 이건데요. EC2 인스턴스에 바로 Nginx 를 설치해서 쓰다가 시스템의 중요한 부분을 건들이거나 내가 뭔자 잘못을 저질렀는데 어디까지 파급이 퍼졌는지 모르겠다면 인스턴스부터 다시 처음부터 띄워야했을 지도 모르는데, 컨테이너 단위로 굴리기 떄문에 컨테이너 차원에서 잘못됐다면 그 컨테이너를 죽여버리면 그만입니다.
5. [EC2] 도커 컴포즈 설정
EC2 에 Docker 이미지를 가져와 실행할 수 있도록 설정을 준비해두려고 합니다.
docker-compose 를 이용하여, 어떤 이미지를 가지고 어떤 설정을 통해 컨테이너를 실행시킬 것인지 명세를 정의합시다.
우선
vim docker-compose-blue.yml
# docker-compose-blue.yml
version: "3.8"
services:
blue:
image: ttasjwi/board-be:latest
container_name: blue
ports:
- "8081:8080"
environment:
- PROFILES=blue
volumes:
- /home/config/board-system/application-blue.yml:/app/config/board-system/application-blue.yml
docker-compose-blue 파일은
blue 이름으로 컨테이너를 실행하고
외부포트 8081 에 내부포트 8080 을 연결하며
PROFILES 값으로 blue 를 전달하도록 합니다.
이때 외부 파일 /home/config/board-system/application-blue.yml 을
컨테이너 내부 파일 /app/config/board-system/application-blue.yml 에 연결해줄 거에요.
vim docker-compose-green.yml
# docker-compose-green.yml
version: "3.8"
services:
green:
image: ttasjwi/board-be:latest
container_name: green
ports:
- "8082:8080"
environment:
- PROFILES=green
volumes:
- /home/config/board-system/application-green.yml:/app/config/board-system/application-green.yml
docker-compose-green 파일은
green 이름으로 컨테이너를 실행하고
외부포트 8082 에 내부포트 8080 을 연결하며
PROFILES 값으로 green 를 전달하도록 합니다.
이때 외부 파일 /home/config/board-system/application-green.yml 을
컨테이너 내부 파일 /app/config/board-system/application-green.yml 에 연결해줄 거에요.
mkdir -p /home/ec2-user/config/board-system
그리고 스프링 애플리케이션 실행에 필요한 설정 파일을 모아둘 ~/config/board-system 경로를 생성해두겠습니다.
6. [GitHub Actions] 배포 스크립트 작성
이제 본격적으로 GitHub Actions 배포 스크립트를 작성해보겠습니다.
.github/workflows/deploy.yml 파일을 만들고 스크립트를 차근차근 작성하며 내용을 정리하겠습니다.
6.1 완성본
name: Deploy
on:
push:
branches:
- master
jobs:
Deploy:
runs-on: ubuntu-latest
steps:
- name: 체크 아웃
uses: actions/checkout@v4
- name: Java 21 설정
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Gradle 캐싱
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: gradlew 파일 실행권한 부여
run: chmod +x gradlew
shell: bash
- name: 프로젝트 빌드
run: |
./gradlew build
- name: DockerHub 로그인
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
- name: 도커 이미지 빌드
run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/board-be .
- name: 도커 이미지 푸시
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/board-be:latest
- name: 배포 대상 포트/PROFILE 확인
run: |
response=$(curl -s -w "%{http_code}" "http://${{ secrets.LIVE_SERVER_IP }}/api/v1/deploy/health-check")
STATUS="${response: -3}" # 마지막 3글자 (HTTP 상태 코드)
BODY="${response::-3}" # 나머지 (응답 본문)
echo "STATUS=$STATUS"
if [ "$STATUS" = "200" ]; then
CURRENT_UPSTREAM="$BODY"
else
CURRENT_UPSTREAM="green"
fi
echo "CURRENT_UPSTREAM=$CURRENT_UPSTREAM"
echo "CURRENT_UPSTREAM=$CURRENT_UPSTREAM" >> $GITHUB_ENV
if [ "$CURRENT_UPSTREAM" = "blue" ]; then
echo "CURRENT_PORT=${{ secrets.BLUE_PORT }}" >> $GITHUB_ENV
echo "STOPPED_PORT=${{ secrets.GREEN_PORT }}" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=green" >> $GITHUB_ENV
elif [ "$CURRENT_UPSTREAM" = "green" ]; then
echo "CURRENT_PORT=${{ secrets.GREEN_PORT }}" >> $GITHUB_ENV
echo "STOPPED_PORT=${{ secrets.BLUE_PORT }}" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=blue" >> $GITHUB_ENV
else
echo "error"
exit 1
fi
- name: GitHub Actions 실행자 IP 얻어오기
id: GITHUB_ACTIONS_IP
uses: haythem/public-ip@v1.3
- name: AWS CLI 설정
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: GitHub Actions - SSH 및 컨테이너 실제 포트 접근 권한 부여
run: |
aws ec2 authorize-security-group-ingress \
--group-id ${{ secrets.EC2_SECURITY_GROUP_ID }} \
--ip-permissions \
'IpProtocol=tcp,FromPort=${{ secrets.EC2_SSH_PORT }},ToPort=${{ secrets.EC2_SSH_PORT }},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]' \
'IpProtocol=tcp,FromPort=${{ env.STOPPED_PORT }},ToPort=${{ env.STOPPED_PORT }},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]'
- name: SSH Key 설정
run: |
mkdir -p ~/.ssh
echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/board-ec2-key.pem
chmod 600 ~/.ssh/board-ec2-key.pem
echo "Host board-ec2" >> ~/.ssh/config
echo " HostName ${{ secrets.LIVE_SERVER_IP }}" >> ~/.ssh/config
echo " User ${{ secrets.EC2_USERNAME }}" >> ~/.ssh/config
echo " IdentityFile ~/.ssh/board-ec2-key.pem" >> ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
- name: 도커 이미지 풀링 및 컨테이너 실행
run: |
ssh board-ec2 << 'EOF'
set -e
# 필요한 프로필 파일을 서버로 복사합니다.
if [ "${{ env.TARGET_UPSTREAM }}" = "blue" ]; then
echo "${{ secrets.APPLICATION_BLUE_YML }}" | base64 --decode > /home/ec2-user/config/board-system/application-blue.yml
else
echo "${{ secrets.APPLICATION_GREEN_YML }}" | base64 --decode > /home/ec2-user/config/board-system/application-green.yml
fi
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/board-be:latest
docker compose -f docker-compose-${{ env.TARGET_UPSTREAM }}.yml up -d
EOF
- name: 새로 실행한 서버 컨테이너 헬스 체크
uses: jtalk/url-health-check-action@v3
with:
url: http://${{ secrets.LIVE_SERVER_IP }}:${{ env.STOPPED_PORT }}/api/v1/deploy/health-check
max-attempts: 3
retry-delay: 10s
- name: Nginx 의 대상 서버를 새로 실행한 컨테이너쪽으로 전환
run: |
ssh board-ec2 << 'EOF'
set -e
docker exec -i nginx bash -c 'echo "set \$service_url ${{ env.TARGET_UPSTREAM }};" > /etc/nginx/conf.d/service-env.inc && nginx -s reload'
EOF
- name: 기존 배포 컨테이너 정지
run: |
ssh board-ec2 << 'EOF'
set -e
docker stop ${{ env.CURRENT_UPSTREAM }}
docker rm ${{ env.CURRENT_UPSTREAM }}
EOF
- name: GitHub Actions - SSH 및 컨테이너 실제 포트 접근 권한 제거
if: always()
run: |
aws ec2 revoke-security-group-ingress \
--group-id ${{ secrets.EC2_SECURITY_GROUP_ID }} \
--ip-permissions \
'IpProtocol=tcp,FromPort=${{ secrets.EC2_SSH_PORT }},ToPort=${{ secrets.EC2_SSH_PORT }},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]' \
'IpProtocol=tcp,FromPort=${{env.STOPPED_PORT}},ToPort=${{env.STOPPED_PORT}},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]'
우선, 완성본입니다.
하나하나 차근차근 내용을 설명해나가겠습니다.
6.2 스크립트 선언
name: Deploy
on:
push:
branches:
- master
Workflow 이름을 Deploy 로 짓고, 자동 작동 조건을 master 브랜치에 push 됐을 때로 설정합니다.
물론 이 작업은 수동으로 실행하는 것도 가능합니다.
6.3 빌드
jobs:
Deploy:
runs-on: ubuntu-latest
steps:
- name: 체크 아웃
uses: actions/checkout@v4
- name: Java 21 설정
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Gradle 캐싱
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: gradlew 파일 실행권한 부여
run: chmod +x gradlew
shell: bash
- name: 프로젝트 빌드
run: |
./gradlew build
빌드입니다.
빌드테스트쪽과 별 차이는 없습니다.
6.4 도커 이미지 빌드, 푸시
- name: DockerHub 로그인
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
- name: 도커 이미지 빌드
run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/board-be .
- name: 도커 이미지 푸시
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/board-be:latest
DockerHub에 로그인하고 이곳에 저희 이미지를 빌드해서 푸시하는 작업입니다.
DockerHub 사용자 아이디 및 액세스토큰이 필요하기 때문에 앞서 secrets에 비밀사항을 등록해둔 것입니다.
6.5 배포 대상 포트 / PROFILE 확인
- name: 배포 대상 포트/PROFILE 확인
run: |
response=$(curl -s -w "%{http_code}" "http://${{ secrets.LIVE_SERVER_IP }}/api/v1/deploy/health-check")
STATUS="${response: -3}" # 마지막 3글자 (HTTP 상태 코드)
BODY="${response::-3}" # 나머지 (응답 본문)
echo "STATUS=$STATUS"
if [ "$STATUS" = "200" ]; then
CURRENT_UPSTREAM="$BODY"
else
CURRENT_UPSTREAM="green"
fi
echo "CURRENT_UPSTREAM=$CURRENT_UPSTREAM" >> $GITHUB_ENV
if [ "$CURRENT_UPSTREAM" = "blue" ]; then
echo "CURRENT_PORT=${{ secrets.BLUE_PORT }}" >> $GITHUB_ENV
echo "STOPPED_PORT=${{ secrets.GREEN_PORT }}" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=green" >> $GITHUB_ENV
elif [ "$CURRENT_UPSTREAM" = "green" ]; then
echo "CURRENT_PORT=${{ secrets.GREEN_PORT }}" >> $GITHUB_ENV
echo "STOPPED_PORT=${{ secrets.BLUE_PORT }}" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=blue" >> $GITHUB_ENV
else
echo "error"
exit 1
fi
현재 작동 중인 헬스체크 API 를 호출합니다.
200 응답이 왔다면 현재 프로필 값이 반환될거에요.
blue 면 blue의 포트(8081)에 배포중인것이고 green 이면 blue의 포트(8082)에 배포중인것으로 간주해서
CURRENT_PORT 값에 현재 배포중인 포트값을
STOPPED_PORT 에는 현재 배포중이지 않은 반대 포트값을
TARGET_UPSTREAM 에는 다음에 반대쪽 프로필을 변수로 할당합니다.
그리고 이걸 $GITHUB_ENV 에 저장해둘거에요. 그러면 이건 이제 env.xxx 로 아래의 스크립트에서 꺼내 쓸 수 있습니다.
200 응답이 오지 않았다면 현재 배포되지 않았다는건데
현재 port 를 green 으로 취급하고, blue 를 새로 배포하도록 합니다.
6.6 AWS CLI 설정 및 GitHub Actions 에 SSH/배포포트 접근권한 부여
- name: GitHub Actions 실행자 IP 얻어오기
id: GITHUB_ACTIONS_IP
uses: haythem/public-ip@v1.3
- name: AWS CLI 설정
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: GitHub Actions - SSH 및 컨테이너 실제 포트 접근 권한 부여
run: |
aws ec2 authorize-security-group-ingress \
--group-id ${{ secrets.EC2_SECURITY_GROUP_ID }} \
--ip-permissions \
'IpProtocol=tcp,FromPort=${{ secrets.EC2_SSH_PORT }},ToPort=${{ secrets.EC2_SSH_PORT }},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]' \
'IpProtocol=tcp,FromPort=${{ env.STOPPED_PORT }},ToPort=${{ env.STOPPED_PORT }},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]'
이제 SSH를 통해 EC2 컨테이너에 접속하기전에, GitHub Actions에게 EC2 접근권한을 부여해야합니다.
현재 GitHun Actions 를 haythem/public-ip 를 사용하여 얻어오고, 그 결과를 기억하기 위해 해당 작업에 id를 지정합니다.
그리고, AWS 에 접속하여(EC2 접근권한 있는 액세스 키 사용)
보안그룹을 수정합니다.
GitHub Actions의 현재 IP에 대해 SSH 접근 및 현재 정지중인 포트(새로 배포할 포트)에 대한 TCP 접근권한을 부여합니다.
6.7 SSH Key 설정
- name: SSH Key 설정
run: |
mkdir -p ~/.ssh
echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/board-ec2-key.pem
chmod 600 ~/.ssh/board-ec2-key.pem
echo "Host board-ec2" >> ~/.ssh/config
echo " HostName ${{ secrets.LIVE_SERVER_IP }}" >> ~/.ssh/config
echo " User ${{ secrets.EC2_USERNAME }}" >> ~/.ssh/config
echo " IdentityFile ~/.ssh/board-ec2-key.pem" >> ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
SSH 설정부입니다.
작업 디렉토리에 .ssh 폴더를 만들고, 시크릿에 있는 SSH 키를 새로 파일에 담아 할당한 다음 600 권한을 부여합니다.
그 후 ssh config 파일을 직접 작성했습니다.
구글에 검색해보면 많은 분들이 appleboy/ssh-action 액션 을 사용해서 SSH 접속을 했지만 저는 해당 action이 잘 먹히지 않았습니다.
"ssh: unable to authenticate, attempted methods [none publickey], no supported"
이런 오류가 발생하면서 계속 실패하더라구요.
제가 뭘 잘못한게 아닌가? 싶어서 해당 리포지토리를 참고하고 해당 이슈로 구글 검색하고 다 따라해봤는데 안 되는 문제가 있었습니다. 반나절 이렇게 시간을 쓰다가...
저랑 안 맞는 도구인 것 같아서 직접 SSH 접속을 하기로 결정을 내리고 action 을 쓰지 않고 직접 SSH 접속을 시도했어요.
6.8 EC2 - SSH 접속 후 도커 컨테이너 실행
- name: 도커 이미지 풀링 및 컨테이너 실행
run: |
ssh board-ec2 << 'EOF'
set -e
# 필요한 프로필 파일을 서버로 복사합니다.
if [ "${{ env.TARGET_UPSTREAM }}" = "blue" ]; then
echo "${{ secrets.APPLICATION_BLUE_YML }}" | base64 --decode > /home/ec2-user/config/board-system/application-blue.yml
else
echo "${{ secrets.APPLICATION_GREEN_YML }}" | base64 --decode > /home/ec2-user/config/board-system/application-green.yml
fi
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/board-be:latest
docker compose -f docker-compose-${{ env.TARGET_UPSTREAM }}.yml up -d
EOF
도커 이미지 풀링, 컨테이너 실행 과정입니다.
앞에서 SSH 설정을 했으므로, 간단하게 SSH 에 접속할 수 있습니다.
이 때, secrets 에 저장해둔 APPLICATION_XXX_YML 을 가져와 base64 디코딩 후,
/home/ec2-user/config/board-system/ 아래에 application-xxx.yml 로 저장합니다.
가장 최신버전 이미지를 풀링해오고, docker-compose-(대상프로필).yml 로 컨테이너를 실행합니다.
# docker-compose-blue.yml
version: "3.8"
services:
blue:
image: ttasjwi/board-be:latest
container_name: blue
ports:
- "8081:8080"
environment:
- PROFILES=blue
volumes:
- /home/config/board-system/application-blue.yml:/app/config/board-system/application-blue.yml
예를 들어 blue 가 주입되면 docker-compose-blue.yml 이 실행되겠죠?
여기서 ports 설정은 외부 포트 8081 로 실행하게 됩니다.
PROFILES=blue 는 Dockerfile 에 정의된 명시에 따라 컨테이너 실행 변수로 주입됩니다.
volumes 는 외부 파일을 컨네이너 내부 파일로 마운트하는 설정입니다. 우리가 가져온 application-xxx.yml 이 컨테이너 내부에 복사됩니다.
FROM amazoncorretto:21-alpine3.20-jdk
ARG JAR_FILE=board-system-container/build/libs/board-system-container.jar
ARG PROFILES
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java -DSpring.profiles.active=${PROFILES} -Dspring.config.location=classpath:/,file:/app/config/board-system/ -jar app.jar"]
Dockerfile의 ARG PROFILES 자리에 들어가서 해당 프로필로 실행할 수 있게 되는겁니다.
그리고 이 때 컨테이너 내의 /app/config/board-system/ 아래에 있는 파일들도 설정으로 가져오게 합니다.
이 자리에 application-xxx.yml 이 포함되는 구조가 됩니다.
6.9 새로 실행된 컨테이너 헬스체크 및 대상 서버 전환
- name: 새로 실행한 서버 컨테이너 헬스 체크
uses: jtalk/url-health-check-action@v3
with:
url: http://${{ secrets.LIVE_SERVER_IP }}:${{ env.STOPPED_PORT }}/api/v1/deploy/health-check
max-attempts: 3
retry-delay: 10s
- name: Nginx 의 대상 서버를 새로 실행한 컨테이너쪽으로 전환
run: |
ssh board-ec2 << 'EOF'
set -e
docker exec -i nginx bash -c 'echo "set \$service_url ${{ env.TARGET_UPSTREAM }};" > /etc/nginx/conf.d/service-env.inc && nginx -s reload'
EOF
jtalk/url-health-check-action 을 사용해서, 새로 띄운 서버의 health-check 엔드포인트를 호출해볼거에요.
스프링 부트는 실행하자마자 바로 요청을 받을 수 있는 상태가 아니라서, 10초 간격으로 3회동안 요청을 보냅니다.
3번동안 요청을 보내고 처음 OK 응답을 받으면 잘 띄워진거에요. 아니면 잘 안 됐을테니 오류가 발생할거구요.
그 후 잘 띄워진게 확인되면, Nginx 의 대상포트를 새로 띄워진 쪽으로 변경할 수 있도록 nginx 설정을 변경하고 리로드 시킵니다.
6.10 기존 컨테이너 중지
- name: 기존 배포 컨테이너 정지
run: |
ssh board-ec2 << 'EOF'
set -e
docker stop ${{ env.CURRENT_UPSTREAM }}
docker rm ${{ env.CURRENT_UPSTREAM }}
EOF
그 후 기존 배포되던 컨테이너를 중지시킵니다.
다만 우리는 최초에 배포된 서버가 없으니 여기서 예외가 발생하긴 할건데요. 그 부분은 아래에서 보도록 하죠.
6.11 GitHub Actions 에 대한 EC2 접근 권한 제거
- name: GitHub Actions - SSH 및 컨테이너 실제 포트 접근 권한 제거
if: always()
run: |
aws ec2 revoke-security-group-ingress \
--group-id ${{ secrets.EC2_SECURITY_GROUP_ID }} \
--ip-permissions \
'IpProtocol=tcp,FromPort=${{ secrets.EC2_SSH_PORT }},ToPort=${{ secrets.EC2_SSH_PORT }},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]' \
'IpProtocol=tcp,FromPort=${{env.STOPPED_PORT}},ToPort=${{env.STOPPED_PORT}},IpRanges=[{CidrIp=${{ steps.GITHUB_ACTIONS_IP.outputs.ipv4 }}/32}]'
SSH 접속 이전에 GitHub Actions 에 대해 EC2 접근 권한을 부여한 바 있죠.
해당 권한들을 모조리 제거합니다.
이 작업은 선행 작업들이 실패하든 성공하든 무조건 실행되도록 해놨습니다.
이렇게 하면 배포 스크립트의 모든 과정이 끝났습니다.
7. 배포 확인
배포를 위해 기존 브랜치 풀리퀘 작성을 마무리 짓고, push를 해봅니다.
실제 push 를 해보면 GitHub Actions의 작업이 동작합니다.
잘 진행되다가 기존 배포 컨테이너 정지 과정에서 에러가 발생합니다.
기존에 배포한 컨테이너가 없기 때문인데요.
하지만 앞의 과정에서 새 컨테이너가 배포되긴 했습니다.
실제로 EC2 퍼블릭 ip 의 80번 포트, 헬스체크 엔드포인트로 접근하면 blue 라고 뜨는 것을 볼 수 있어요.
blue 프로필로 잘 실행되고 잘 배포된겁니다!
실패했지만 blue가 뜬것을 확인했다면, Re-run jobs 를 실행해봅시다. 그러면 배포가 다시 수동으로 실행됩니다.
이전과 달리 모든 작업이 통과 되는 것을 볼 수 있고
green 프로필이 띄워지는 것을 볼 수 있어요. 대상서버 변경이 잘 된 것입니다.
실제 ec2 접속해서 현재 띄워진 컨테이너들을 보면 blue는 없고 green 만 살아있는 것을 볼 수 있습니다.
이것으로 무중단 배포가 가능해진 것을 확인할 수 있어요.
이로서 저희는 프로젝트 작업물을 master 브랜치에 push 할 때마다 새로 변경된 코드를 라이브 서버에 배포할 수 있게 됐어요.
고객들의 피드백을 좀 더 빠르게 받고 우리 서비스에 빠르게 반영할 수 있는 선순환 구조를 만든 것에 의의가 있다고 생각해요.
이제 뒤의 글들에서는 본격적으로 프로젝트를 개발해 나가볼게요.
'Project' 카테고리의 다른 글
[토이프로젝트] 게시판 시스템(board-system) 6. 커스텀 예외, core 모듈 (0) | 2024.10.02 |
---|---|
[토이프로젝트] 게시판 시스템(board-system) 5. 프로젝트 멀티모듈화 (0) | 2024.09.30 |
[토이프로젝트] 게시판 시스템(board-system) 3. 프로젝트 생성 및 지속적 통합 (0) | 2024.09.27 |
[토이프로젝트] 게시판 시스템(board-system) 2. AWS 환경 구성 (0) | 2024.09.21 |
[토이프로젝트] 게시판 시스템(board-system) 1. 기획 (0) | 2024.09.21 |