Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI

on:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 CI가 main 브랜치 push에서만 실행됩니다. 이번 PR의 기준 브랜치가 Joonseok이고 학습 PR 검증 목적도 있으므로 pull_request 또는 대상 브랜치 전략을 함께 정의하는 것을 권장합니다. CI는 코드가 합쳐진 뒤보다 합치기 전에 피드백을 주는 역할이 크기 때문에 실행 시점을 명확히 잡을 필요가 있습니다.

push:
branches: [ "main" ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: main 체크아웃
uses: actions/checkout@v4

- name: 자바 설정
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'

- name: gradlew 실행 권한 부여
run: chmod +x gradlew
working-directory: ./Joonseok

- name: gradle 세팅
uses: gradle/actions/setup-gradle@v4

- name: gradle 빌드
run: ./gradlew clean build -x test
working-directory: ./Joonseok

- name: Docker Hub 로그인
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: Docker Buildx 설정
uses: docker/setup-buildx-action@v3

- name: Docker 이미지 빌드 및 푸시
uses: docker/build-push-action@v7
with:
context: ./Joonseok
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGENAME }}:latest
platforms: linux/amd64, linux/arm64
59 changes: 59 additions & 0 deletions Joonseok/.github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CD

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge CD 워크플로를 루트 .github/workflows로 옮기세요

현재 cd.ymlJoonseok/.github/workflows 아래에 있어 GitHub Actions가 워크플로로 읽지 않습니다. GitHub는 저장소 루트의 .github/workflows를 검색해 실행할 워크플로를 찾기 때문에(GitHub Docs), CI가 성공해도 이 CD는 트리거되지 않습니다. 파일은 루트 .github/workflows/cd.yml로 옮기고, Joonseok 하위 경로는 각 step의 working-directory나 파일 경로로 표현해 보세요. 다음에는 Actions의 워크플로 발견 규칙과 모노레포 경로 기준을 같이 공부하면 좋겠습니다.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub Actions 워크플로는 저장소 루트의 .github/workflows/ 아래에 있어야 자동으로 인식됩니다. 현재처럼 Joonseok/.github/workflows/에 두면 CD가 실행되지 않을 가능성이 높으므로, 루트 워크플로에서 working-directory: ./Joonseok를 지정하는 구조를 검토하는 것을 권장합니다.


on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]

permissions:
contents: read

env:
BACKEND_IMAGE: ${{ secrets.DOCKER_USERNAME }}/kkumteul-backend
FASTAPI_IMAGE: ${{ secrets.DOCKER_USERNAME }}/kkumteul-ai

jobs:
deploy:
name: Deploy to EC2
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Copy deploy files to EC2
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: 22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

배포 파일을 복사할 때 경로가 저장소 루트 기준으로 해석됩니다. 현재 설정은 docker-compose.ymlnginx/conf.d/default.conf를 루트에서 찾지만, 이번 PR의 Nginx 설정은 Joonseok/nginx/conf.d/default.conf에 있습니다. 워크플로 위치를 루트로 옮기는 경우에도 source 경로와 EC2에서 실행할 compose 파일 위치를 함께 맞출 필요가 있습니다.

source: "docker-compose.yml,nginx/conf.d/default.conf"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 배포 파일 경로를 체크아웃 기준으로 맞추세요

source는 체크아웃된 저장소 루트를 기준으로 docker-compose.ymlnginx/conf.d/default.conf를 찾는데, 저장소 전체 파일 목록(rg --hidden --files)을 확인해도 docker-compose.yml은 없고 nginx 설정은 Joonseok/nginx/conf.d/default.conf에만 있습니다. CD가 실행되면 scp 단계에서 필요한 배포 파일을 못 복사해 이후 docker compose 흐름도 검증되지 않습니다. compose 파일을 커밋하고 Joonseok/docker-compose.yml,Joonseok/nginx/conf.d/default.conf처럼 실제 위치와 맞추세요. 다음 학습 포인트는 GitHub Actions의 working directory와 배포 아티팩트 경로입니다.

Useful? React with 👍 / 👎.

target: "/home/${{ secrets.SERVER_USER }}/app"

- name: Deploy over SSH
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: 22
envs: BACKEND_IMAGE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 원격 스크립트에 FASTAPI_IMAGE도 전달하세요

script에서는 ${FASTAPI_IMAGE}.env를 만들고 docker pull도 하는데, appleboy/ssh-action에서 원격 셸로 넘길 변수 목록인 envs에는 BACKEND_IMAGE만 있습니다(ssh-action 문서). 이 상태에서는 원격 서버에서 FASTAPI_IMAGE가 빈 값이 되어 FASTAPI_IMAGE=:latestdocker pull :latest가 실행될 수 있습니다. envs: BACKEND_IMAGE,FASTAPI_IMAGE로 맞추거나 GitHub expression으로 값을 직접 작성해 보세요. 다음에는 GitHub runner 환경변수와 SSH 원격 셸 환경의 차이를 공부하면 좋겠습니다.

Useful? React with 👍 / 👎.

script: |
cd /home/${{ secrets.SERVER_USER }}/app
echo "BACKEND_IMAGE=${BACKEND_IMAGE}:latest" > .env
echo "FASTAPI_IMAGE=${FASTAPI_IMAGE}:latest" >> .env

echo "[1/4] Pull latest images..."
docker pull ${BACKEND_IMAGE}:latest
docker pull ${FASTAPI_IMAGE}:latest

echo "[2/4] Restart with docker compose..."
docker compose up -d --remove-orphans

echo "[3/4] Force-recreate nginx for config changes..."
docker compose up -d --force-recreate nginx

echo "[4/4] Cleanup unused images..."
docker image prune -f
7 changes: 7 additions & 0 deletions Joonseok/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM eclipse-temurin:21-jdk
WORKDIR /app

ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} project.jar
Comment on lines +4 to +5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Dockerfile이 실행 가능한 bootJar만 복사하게 하세요

Spring Boot Gradle 플러그인은 기본적으로 실행 가능한 bootJar-plain.jar를 함께 만들기 때문에(Spring Boot Docs), build/libs/*.jar는 보통 두 JAR에 매칭됩니다. Docker COPY는 여러 소스를 파일명 project.jar 하나로 복사할 수 없어 CI의 이미지 빌드가 실패합니다. jar { enabled = false }로 plain jar를 끄거나 bootJar 산출물 이름을 고정한 뒤 명시 경로를 복사하세요. 다음에는 bootJar와 plain jar의 역할 차이를 공부하면 좋겠습니다.

Useful? React with 👍 / 👎.


ENTRYPOINT ["java", "-Dspring.profiles.active=docker", "-jar", "project.jar"]
131 changes: 131 additions & 0 deletions Joonseok/keyword_summary/ch10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<aside>
💡

</aside>

- 클라우드 컴퓨팅이란?

<aside>
🖥️

### 저희는 어떻게 웹 서비스를 이용하는거죠…?

</aside>

인터넷을 통해 필요할 때마다 컴퓨팅 리소스, 즉 물리적 또는 가상 서버, 데이터 스토리지, 네트워킹 기능, 애플리케이션 개발 툴, 소프트웨어, AI 기반 분석 플랫폼 등에 사용한 만큼만 접근할 수 있는 서비스

온-프레미스 방식과 대조되는 방식으로, 보안성이 다소 떨어질 수 있으나, 유연성과 확장성이 매우 뛰어나다.

- 비용 및 효율성
- 속도 및 민첩성
- 무제한 확장성
- 전략적 가치 향상

현대적인 클라우드 아키텍처는 이하의 항목을 포함한다.

- 데이터 센터
- 네트워킹 기능
- 가상화
- AWS? GCP?

<aside>

### 클라우드 컴퓨팅 플랫폼. 뭐 써야 돼요?

</aside>

### AWS

Amazon Web Service의 약자.
클라우드 컴퓨팅에 필요한 거의 모든 서비스를 제공하며, 사용한 만큼의 비용만 지불하면 되는 형태이다.

EC2라고 불리는 다양한 가상 컴퓨터를 제공한다. 리소스 선택폭도 굉장히 넓은 편

저장공간 역시 넓다

네트워크 인프라 역시 더 크다.

### GCP

Google Cloud Platform
AI 및 기계 학습 부문에서 빠르게 주목받고 있다.
가격 측면에서 더욱 직관적이지만, 솔루션이나 가격 유연성이 부족하다.

AWS 만큼의 선택폭은 없지만, 사용하면 할 수록 자동 할인이 있다.

AI 학습이나, Google Cloud와 연결할 수 있다는 장점이 있다.

프라이빗, 데이터 집약적 운영에 더 강하다.

- 환경변수 처리 방법과 왜 환경변수로 민감 정보를 가려야 하는가?
1. 서버에 직접 등록한다.
Github가 제공하는 CI/CD 등 기능 사용 불가
2. Git Secrets에 등록한다
CI/CD 사용 가능

환경변수는 대부분 비용과 직결되는 문제로, 해커에 의한 악의적 요청으로 지불해야 할 대금이 폭증하지 않도록 가린다.

- yml 환경 분리 방법

각 환경마다 빌드해야 할 것과 빌드 하지 말아야 할 것들을 분리하여
`@Profile(value = NAME )` 으로 분기한다

메인 설정 파일인 application.yml(applicaiton.properties)에 환경변수로
`spring.profiles.active=${이름}` 으로 환경마다 다른 설정 파일을 주입하도록 한다.

- Docker와 .jar vs Docker 이미지

Docker는 개발자와 개발자, 개발 환경과 운영 환경의 차이를 없애기 위해 프로젝트를 애플리케이션 단위로 포장하는 툴이다.

.jar : java 빌드 산출물. JVM에 의해 읽히는 바이트 파일 등

Docker 이미지 : Dockerfile에 의해 생성되는 산출물. 애플리케이션 실행에 필요한 모든 것들을 담고 있다.
---
- Sticky Session vs Session Clustering

stateful 서버의 한계점. 서버를 확장할 때, 세션 저장소 의존성이 생기므로, 확장성과 유연성이 떨어진다.

### Stick sesison

세션은 각 서버에 저장되어 종속적인 방식.

인프라가 단순해지고, 로컬 메모리 접근이므로 빠르다.

- 세션이 서버 종속이므로 서버가 다운되면 세션도 전부 사라진다.
- 한 서버에 무거운 세션이 pinned → 부하 불균등 문제

### Session Clustering

모든 서버가 세션을 공유한다.

1. 세션 복제식
2. 외부 저장 서버

장점 : 스티키 세션의 단점을 해결

단점 : 접속마다 네트워킹 왕복 필요 → 오버헤드 상승

- blue-green 배포

서버를 2개 띄우고, 트래픽을 통째로 전환하는 무중단 배포 전략

- Blue : 현재 트래픽을 받는 서버
- Green : 유휴 상태에 있는 서버
- 새 버전 업로드
- 스모크 테스트
- 헬스 체크…

모든 테스트가 종료되면 모든 트래픽을 현재 Blue → Green으로 이동 후 Green을 Blue 서버로 지정

장애 발생 시, 기존 Blue로 즉시 전환하면 되므로 문제 없음

- Gzip 압축 및 캐싱

### Gzip 압축

HTTP 응답 본문을 DEFLATE 알고리즘으로 압축하고, wire로 보내는 본문 인코딩 방법.
HTTP 응답 본문은 반복 패턴이 많아서 크게 이득볼 수 있다.

- 클라이언트가 `Accept-Encoding: gzip, deflate, br`로 압축 해제 가능함을 알림
- 서버가 압축 후 `Content-Encoding: gzip` + `Vary: Accept-Encoding`
22 changes: 22 additions & 0 deletions Joonseok/nginx/conf.d/default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
upstream backend {
least_conn;
server app1:8080 weight=3;
server app2:8080 weight=1;
}

server {
listen 80;
server_name _;

location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public class SecurityConfig {
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/auth/**" // sign-up, login request allow
"/auth/**", // sign-up, login request allow
"/server/**" // load balancing check
};

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.umc.study.global.web.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Map;

@RestController
@RequestMapping("/server")
public class ServerInfoController {

@GetMapping("/info")
public ResponseEntity<Map<String, String>> getServerInfo() throws UnknownHostException {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컨트롤러 메서드가 UnknownHostException을 그대로 밖으로 전달하면 서버 정보 조회 실패가 기본 500 응답으로 노출될 수 있습니다. 학습용 API라도 Controller는 응답 계약을 유지하고, 실패 처리는 서비스 또는 예외 핸들러에서 명확한 메시지로 변환하는 구조를 권장합니다. 예외 처리 계층과 Controller 책임 분리를 함께 점검하면 좋습니다.

InetAddress host = InetAddress.getLocalHost();
return ResponseEntity.ok(Map.of(
"hostname", host.getHostName(),
"ip", host.getHostAddress()
));
}
}