Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f18a976
feat : deploy 및 설정 일부 변경, 글로벌 exception에서 액츄에이터 오류는 그냥 다시 throw
userjin2123 Nov 6, 2025
9b66fae
feat : deploy 및 설정 일부 변경, 글로벌 exception에서 액츄에이터 오류는 그냥 다시 throw
userjin2123 Nov 6, 2025
6b4575a
Merge branch 'develop' of https://github.com/codeit-part3-team2/monew…
userjin2123 Nov 6, 2025
988e409
fix : grafana 파일 수정
userjin2123 Nov 6, 2025
ed801c9
fix : repo 경로 설정문제 해결
userjin2123 Nov 6, 2025
5855037
trigger CD pipeline, IAM 권한 추가
userjin2123 Nov 6, 2025
dff4e9f
fix : 권한문제 디버깅 용 run 추가, 클라스터 이름 monew-cluster-1 로 수정
userjin2123 Nov 6, 2025
3677854
fix : 리전 분리
userjin2123 Nov 6, 2025
4d25185
fix : EFS 경로 추가, deploy에서 이름으로 추적
userjin2123 Nov 6, 2025
1819fbc
fix : 배치 application-prod.yml에서 monew.api.url 누락 설정 추가
userjin2123 Nov 6, 2025
940af7e
feat : 서비스 업데이트 뿐만 아니라 중지, 삭제되었을때 재생성 로직 추가
userjin2123 Nov 6, 2025
c9b9498
feat : 기사 수집 스케쥴러 cron으로 변경, deploy 보안그룹 매핑 수정
userjin2123 Nov 6, 2025
a6c219d
refactor: 테스트 생성하는데 어려워 로그 삭제
yeahlimm Nov 4, 2025
6398af4
feat : 관심사 서비스 코드 작성 (슬라이스 테스트)
yeahlimm Nov 4, 2025
65ed736
refactor : 이벤트 추가
yeahlimm Nov 6, 2025
5d81130
feat : 뉴스 백업 스케쥴러 cron으로 수정
userjin2123 Nov 6, 2025
b8a07ab
feat : 프로메테우스 완전 종료 이후 업데이트 시작으로 로직 바꿈
userjin2123 Nov 6, 2025
2c4ecbf
feat : 유저 활동 성능 테스트용 컨트롤러 추가
userjin2123 Nov 6, 2025
4e6fc50
fix : 유저 경로 /api 누락된것 추가
userjin2123 Nov 6, 2025
4120a54
feat : add 뉴스 기사 제목에 `"` 가 있으면 &quot로 html 인코딩 문자로 출력되는 문제 Mapper에서 파싱
userjin2123 Nov 6, 2025
8b681b6
Merge pull request #51 from codeit-part3-team2/deploy
JaehyeokLim Nov 13, 2025
141a438
Merge pull request #61 from codeit-part3-team2/api/feature/interest
JaehyeokLim Nov 13, 2025
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
272 changes: 272 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
name: CD - Deploy to ECS

on:
push:
branches: [ release ]
workflow_dispatch:

env:
ECS_REGION: ap-northeast-2
ECR_PUBLIC_REGION: us-east-1

ECS_CLUSTER: monew-cluster-1
ECR_REPOSITORY: public.ecr.aws/s7e2q2h9/monew

PUBLIC_SUBNETS_CSV: subnet-00530329356c03add,subnet-008505c5a32cd0093,subnet-08601cd2c0aa46873,subnet-03f28f954846ad79d
ECS_SERVICE_SG: sg-0fbe8feeddc4e8195,sg-08a99e539bed73ed3

jobs:
build-and-deploy:
runs-on: ubuntu-latest
strategy:
matrix:
service:
- name: api
dockerfile: Dockerfile.api
tag: monew-api
task_definition: monew-api-task
ecs_service: monew-api-service
container: monew-api-app
port: 8080
target_group_arn: arn:aws:elasticloadbalancing:ap-northeast-2:381437600029:targetgroup/monew-api-target/188870153e8974df
- name: batch
dockerfile: Dockerfile.batch
tag: monew-batch
task_definition: monew-batch-task
ecs_service: monew-batch-service
container: monew-batch-app
port: 8081
target_group_arn: arn:aws:elasticloadbalancing:ap-northeast-2:381437600029:targetgroup/monew-batch-target/8c8fd5dc5924e8f6
- name: monitor
dockerfile: Dockerfile.monitor
tag: monew-monitor
task_definition: monew-monitor-task
ecs_service: monew-monitor-service
container: monew-monitor-app
port: 8082
target_group_arn: arn:aws:elasticloadbalancing:ap-northeast-2:381437600029:targetgroup/monew-monitor-target/f2041a48b08d9ef9
- name: prometheus
dockerfile: Dockerfile.prom
tag: prometheus
task_definition: monew-prometheus-task
ecs_service: monew-prometheus-service
container: monew-prometheus-app
port: 9090
target_group_arn: arn:aws:elasticloadbalancing:ap-northeast-2:381437600029:targetgroup/monew-prometheus-target/01ce4f2072a65a1f
- name: grafana
dockerfile: Dockerfile.grafana
tag: grafana
task_definition: monew-grafana-task
ecs_service: monew-grafana-service
container: monew-grafana-app
port: 3000
target_group_arn: arn:aws:elasticloadbalancing:ap-northeast-2:381437600029:targetgroup/monew-grafana-target/aece83e522ce15b5

steps:
- name: Checkout code
uses: actions/checkout@v4

# ===== Public ECR (us-east-1) =====
- name: Configure AWS credentials for Public ECR
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ env.ECR_PUBLIC_REGION }}

- name: Debug AWS caller identity (ECR creds)
run: aws sts get-caller-identity --region ${{ env.ECR_PUBLIC_REGION }}

- name: Login to Public ECR
run: |
aws ecr-public get-login-password --region ${{ env.ECR_PUBLIC_REGION }} | \
docker login --username AWS --password-stdin public.ecr.aws

- name: Build and push ${{ matrix.service.name }} image
run: |
set -euo pipefail
docker build -f ${{ matrix.service.dockerfile }} \
-t ${{ env.ECR_REPOSITORY }}:${{ matrix.service.tag }}-${{ github.sha }} \
-t ${{ env.ECR_REPOSITORY }}:${{ matrix.service.tag }}-latest .
docker push ${{ env.ECR_REPOSITORY }}:${{ matrix.service.tag }}-${{ github.sha }}
docker push ${{ env.ECR_REPOSITORY }}:${{ matrix.service.tag }}-latest

# ===== ECS (ap-northeast-2) =====
- name: Configure AWS credentials for ECS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ env.ECS_REGION }}

- name: Debug AWS caller identity (ECS creds)
run: aws sts get-caller-identity --region ${{ env.ECS_REGION }}

- name: Dry-run DescribeTaskDefinition (debug)
run: |
set -euo pipefail
echo "Family: ${{ matrix.service.task_definition }} Region: ${{ env.ECS_REGION }}"
aws ecs describe-task-definition \
--task-definition ${{ matrix.service.task_definition }} \
--region ${{ env.ECS_REGION }} \
--query 'taskDefinition.taskDefinitionArn' --output text

- name: Update task definition (swap image)
run: |
set -euo pipefail
FAMILY="${{ matrix.service.task_definition }}"
REGION="${{ env.ECS_REGION }}"
CNAME="${{ matrix.service.container }}"
IMAGE="${{ env.ECR_REPOSITORY }}:${{ matrix.service.tag }}-latest"

TD=$(aws ecs describe-task-definition \
--task-definition "$FAMILY" \
--region "$REGION" \
--query 'taskDefinition' --output json)

echo "== Before =="; echo "$TD" | jq '.containerDefinitions[] | {name,image}'

NEW_TD=$(echo "$TD" | jq \
--arg IMAGE "$IMAGE" --arg CNAME "$CNAME" '
.containerDefinitions |=
( map( if .name == $CNAME then (.image = $IMAGE) else . end ) )
| del(.taskDefinitionArn, .revision, .status, .requiresAttributes,
.compatibilities, .registeredAt, .registeredBy)')

echo "== After (preview) =="; echo "$NEW_TD" | jq '.containerDefinitions[] | {name,image}'

NEW_TASK_ARN=$(aws ecs register-task-definition \
--cli-input-json "$NEW_TD" \
--region "$REGION" \
--query 'taskDefinition.taskDefinitionArn' --output text)

echo "NEW_TASK_ARN=$NEW_TASK_ARN" >> $GITHUB_ENV
echo "Registered: $NEW_TASK_ARN"

- name: Create or Update ECS service (auto)
run: |
set -euo pipefail
CLUSTER="${{ env.ECS_CLUSTER }}"
SERVICE="${{ matrix.service.ecs_service }}"
REGION="${{ env.ECS_REGION }}"
TG_ARN="${{ matrix.service.target_group_arn }}"
CONTAINER="${{ matrix.service.container }}"
PORT="${{ matrix.service.port }}"
TASK_DEF="${{ env.NEW_TASK_ARN }}"

# --- Network (CLI shorthand: 배열 안전 전달) ---
IFS=',' read -r -a SUBNETS_ARR <<< "${{ env.PUBLIC_SUBNETS_CSV }}"
SUBNETS_SH=$(printf '%s,' "${SUBNETS_ARR[@]}"); SUBNETS_SH="[${SUBNETS_SH%,}]"

IFS=',' read -r -a SGS_ARR <<< "${{ env.ECS_SERVICE_SG }}"
SGS_SH=$(printf '%s,' "${SGS_ARR[@]}"); SGS_SH="[${SGS_SH%,}]"

NET_SH="awsvpcConfiguration={subnets=${SUBNETS_SH},securityGroups=${SGS_SH},assignPublicIp=ENABLED}"
echo "Network(shorthand): $NET_SH"

LB_JSON=$(jq -nc \
--arg tg "$TG_ARN" --arg cn "$CONTAINER" --argjson cp "$PORT" \
'[{targetGroupArn:$tg, containerName:$cn, containerPort:$cp}]')

DESC=$(aws ecs describe-services --cluster "$CLUSTER" --services "$SERVICE" --region "$REGION" --output json || true)
FAIL_LEN=$(echo "$DESC" | jq -r '.failures | length // 0')
SVC_LEN=$(echo "$DESC" | jq -r '.services | length // 0')
STATUS=$(echo "$DESC" | jq -r '.services[0].status // empty')

# 배포 정책: Prometheus만 겹침 금지(0/100), 나머지는 기본(100/200)
if [ "$SERVICE" = "monew-prometheus-service" ]; then
DEPLOY_CONF='maximumPercent=100,minimumHealthyPercent=0'
else
DEPLOY_CONF='maximumPercent=200,minimumHealthyPercent=100'
fi

if [ "$FAIL_LEN" != "0" ] || [ "$SVC_LEN" = "0" ]; then
echo "🟢 Service not found → create-service"
aws ecs create-service \
--cluster "$CLUSTER" \
--service-name "$SERVICE" \
--task-definition "$TASK_DEF" \
--desired-count 1 \
--launch-type FARGATE \
--platform-version LATEST \
--deployment-configuration "$DEPLOY_CONF" \
--deployment-controller "type=ECS" \
--enable-execute-command \
--network-configuration "$NET_SH" \
--load-balancers "$LB_JSON" \
--region "$REGION"
else
if [ "$STATUS" != "ACTIVE" ]; then
echo "🟠 Service exists but status=$STATUS → delete & recreate"
aws ecs delete-service --cluster "$CLUSTER" --service "$SERVICE" --force --region "$REGION" || true
for i in {1..30}; do
sleep 10
D=$(aws ecs describe-services --cluster "$CLUSTER" --services "$SERVICE" --region "$REGION" --output json || true)
FL=$(echo "$D" | jq -r '.failures | length // 0')
SL=$(echo "$D" | jq -r '.services | length // 0')
[ "$FL" != "0" ] || [ "$SL" = "0" ] && break
echo "Waiting deletion..."
done
aws ecs create-service \
--cluster "$CLUSTER" \
--service-name "$SERVICE" \
--task-definition "$TASK_DEF" \
--desired-count 1 \
--launch-type FARGATE \
--platform-version LATEST \
--deployment-configuration "$DEPLOY_CONF" \
--deployment-controller "type=ECS" \
--enable-execute-command \
--network-configuration "$NET_SH" \
--load-balancers "$LB_JSON" \
--region "$REGION"
else
echo "🔵 Service ACTIVE"
if [ "$SERVICE" = "monew-prometheus-service" ]; then
echo "🧯 Prometheus: scale down to 0 first (avoid TSDB lock)"
aws ecs update-service \
--cluster "$CLUSTER" \
--service "$SERVICE" \
--desired-count 0 \
--region "$REGION"

# 모든 태스크 정지 대기
for i in {1..60}; do
TASKS=$(aws ecs list-tasks --cluster "$CLUSTER" --service-name "$SERVICE" --region "$REGION" --query 'taskArns' --output json)
if [ "$TASKS" = "[]" ]; then
echo "All prometheus tasks stopped."
break
fi
echo "Waiting prometheus tasks to stop..."
sleep 5
done

echo "🔁 Update task def & scale up to 1"
aws ecs update-service \
--cluster "$CLUSTER" \
--service "$SERVICE" \
--task-definition "$TASK_DEF" \
--desired-count 1 \
--force-new-deployment \
--region "$REGION"
else
echo "🔁 Regular rolling update"
aws ecs update-service \
--cluster "$CLUSTER" \
--service "$SERVICE" \
--task-definition "$TASK_DEF" \
--desired-count 1 \
--force-new-deployment \
--region "$REGION"
fi
fi
fi

- name: Wait for service stability
run: |
set -euo pipefail
aws ecs wait services-stable \
--cluster ${{ env.ECS_CLUSTER }} \
--services ${{ matrix.service.ecs_service }} \
--region ${{ env.ECS_REGION }}
echo "🚀 ${{ matrix.service.name }} deployment completed!"
8 changes: 8 additions & 0 deletions Dockerfile.grafana
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM grafana/grafana:latest

# 환경변수 설정
ENV GF_SECURITY_ADMIN_USER=admin \
GF_SECURITY_ADMIN_PASSWORD=admin

# 포트 노출
EXPOSE 3000
18 changes: 18 additions & 0 deletions Dockerfile.prom
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM prom/prometheus:latest

# 설정 파일
COPY monew-monitor/src/main/resources/prometheus.yml /etc/prometheus/prometheus.yml

EXPOSE 9090

# 핵심: TSDB 경로를 /prometheus 로!
# (ECS 태스크에서 /prometheus를 EFS로 마운트했으므로 여기에 저장됨)
CMD [ \
"--config.file=/etc/prometheus/prometheus.yml", \
"--storage.tsdb.path=/prometheus", \
"--storage.tsdb.retention.time=30d", \
"--storage.tsdb.retention.size=10GB", \
"--web.enable-admin-api", \
"--web.console.libraries=/usr/share/prometheus/console_libraries", \
"--web.console.templates=/usr/share/prometheus/consoles" \
]
10 changes: 7 additions & 3 deletions docker-compose.prod.yml → docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,11 @@ services:

# Prometheus (Metrics Collector)
prometheus:
image: prom/prometheus:latest
build:
context: .
dockerfile: Dockerfile.prom
image: monew-prometheus:-latest
container_name: monew-prometheus
volumes:
- ./monew-monitor/src/main/resources/prometheus.yml:/etc/prometheus/prometheus.yml:ro
ports:
- "9090:9090"
networks:
Expand All @@ -148,6 +149,9 @@ services:

# Grafana (Visualization)
grafana:
build:
context: .
dockerfile: Dockerfile.grafana
image: grafana/grafana:latest
container_name: monew-grafana
environment:
Expand Down
3 changes: 3 additions & 0 deletions monew-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ dependencies {
implementation 'org.springframework.security:spring-security-crypto'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9'
implementation 'org.apache.commons:commons-text:1.10.0' // 유사도 계산용

implementation("org.springframework.boot:spring-boot-starter-actuator")
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNo
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpectedException(Exception e, HttpServletRequest request) {
public ResponseEntity<ErrorResponse> handleUnexpectedException(Exception e, HttpServletRequest request) throws Exception {

if (request.getRequestURI().startsWith("/actuator")) {
throw e;
}

log.error("[서버 내부 오류] 예외 타입: {}, 메시지: {}, URI: {}",
e.getClass().getSimpleName(),
e.getMessage(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,6 @@ public CursorPageResponseInterestDto getInterests(Long userId,

Slice<Interest> slices = interestRepository.findAll(
keyword, orderBy, direction, cursor, after, limit);
log.info("REQ userId={}, keyword={}, orderBy={}, direction={}, cursor={}, after={}, limit={}",
userId, keyword, orderBy, direction, cursor, after, limit);

List<Interest> interests = slices.getContent();

Expand All @@ -126,9 +124,6 @@ public CursorPageResponseInterestDto getInterests(Long userId,
boolean subscribedByMe = subscribedIds.contains(interest.getId());
InterestDto dto = interestMapper.toInterestDto(interest, keywords, subscribedByMe,
subscriberCount);

log.info("DBG dto id={}, name={}, subscriberCount={} subscribedByMe={}",
dto.id(), dto.name(), dto.subscriberCount(), dto.subscribedByMe());
interestDtos.add(dto);
}

Expand Down
Loading