Initial commit

This commit is contained in:
llm-bot
2026-03-26 17:50:50 +09:00
commit 3d8f55e8a2
9 changed files with 471 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
venv/
__pycache__/
*.pyc
.env

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM python:3.11-slim
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -U pip
RUN pip install --no-cache-dir uvicorn fastapi
RUN pip install --no-cache-dir -r requirements.txt
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "80"]

327
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,327 @@
pipeline {
agent { label 'py-kaniko' } // JCasC 파드 템플릿: jnlp / ci-python / tooling / kaniko
options { timestamps() }
parameters {
string(name: 'BRANCH_NAME', defaultValue: '', description: '빌드할 브랜치명 (비우면 main)')
string(name: 'GIT_COMMIT_HASH', defaultValue: '', description: '빌드할 커밋 해시 (비우면 최신 커밋)')
}
environment {
NODE_ID = '25082bef-b7a9-41d3-aeeb-30472b03ecd5'
NODE_TYPE = 'agent'
NAMESPACE = 'abc'
REGISTRY = 'maxis.azurecr.io'
PLATFORM_NAMESPACE = 'abc'
IMAGE_PULL_SECRET = 'cr-pull'
}
stages {
// ──────────────────────────────────────────────
// Stage 1. Checkout
// - 브랜치 / 커밋 해시 기준으로 소스코드 체크아웃
// - 이미지 경로 / release 이름 등 이후 stage 에서 사용할 환경변수 세팅
// ──────────────────────────────────────────────
stage('Checkout') {
steps {
container('jnlp') {
script {
sh 'git fetch --all || true'
def checkoutBranch = params.BRANCH_NAME?.trim() ?: 'main'
def commitHash = params.GIT_COMMIT_HASH?.trim()
if (commitHash) {
sh "git fetch origin ${checkoutBranch}"
sh "git checkout ${checkoutBranch}"
sh "git checkout ${commitHash}"
} else {
sh "git fetch origin ${checkoutBranch}"
sh "git checkout ${checkoutBranch}"
commitHash = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
}
env.BRANCH_NAME = checkoutBranch
env.GIT_COMMIT_HASH = commitHash
env.RELEASE_NAME = "node-${NODE_ID}-${checkoutBranch}"
env.IMAGE_REPO = "${REGISTRY}/node.${NODE_ID}.${checkoutBranch}"
env.IMAGE_TAG = commitHash
env.FULL_IMAGE = "${env.IMAGE_REPO}:${env.IMAGE_TAG}"
echo "NODE_ID=${NODE_ID}"
echo "BRANCH_NAME=${env.BRANCH_NAME}"
echo "GIT_COMMIT_HASH=${env.GIT_COMMIT_HASH}"
echo "RELEASE_NAME=${env.RELEASE_NAME}"
echo "FULL_IMAGE=${env.FULL_IMAGE}"
echo "IMAGE_PULL_SECRET=${IMAGE_PULL_SECRET}"
}
}
}
}
// ──────────────────────────────────────────────
// Stage 2. Build
// - Kaniko 로 Docker 이미지 빌드 후 레지스트리에 푸시
// - 실패 시 로그에 pip install / SyntaxError 등 원인 출력
// - retry(3): 레지스트리 일시적 네트워크 오류 대비
// ──────────────────────────────────────────────
stage('Build') {
steps {
container('kaniko') {
retry(3) {
sh '''
set -e
/kaniko/executor \
--context=$WORKSPACE \
--dockerfile=Dockerfile \
--destination=$FULL_IMAGE \
--cache=true \
--snapshot-mode=time
'''
}
}
}
post {
failure {
echo '[Build] ★ BUILD FAILED ★'
echo '[Build] Kaniko 빌드 실패. 위 로그에서 원인을 확인하세요.'
echo '[Build] 주요 원인: Dockerfile 오류 / pip install 실패 / SyntaxError / ModuleNotFoundError'
}
}
}
// ──────────────────────────────────────────────
// Stage 3. Deploy
// - K8s manifest 렌더링 후 kubectl apply
// - imagePullSecret 없으면 platform 네임스페이스에서 복제
// - rollout status 로 배포 완료 대기 (최대 10분)
// - spec 변경 없으면 rollout restart 실행
// ──────────────────────────────────────────────
stage('Deploy') {
steps {
container('tooling') {
sh '''
set -euo pipefail
# imagePullSecret 복제 (platform → NAMESPACE)
if [ -n "${IMAGE_PULL_SECRET}" ] && ! kubectl -n "${NAMESPACE}" get secret "${IMAGE_PULL_SECRET}" >/dev/null 2>&1; then
echo "[Deploy] imagePullSecret '${IMAGE_PULL_SECRET}' 복제 중 (platform → ${NAMESPACE})"
kubectl -n "${PLATFORM_NAMESPACE}" get secret "${IMAGE_PULL_SECRET}" -o go-template='{{ index .data ".dockerconfigjson" }}' > .dockercfg.b64
base64 -d .dockercfg.b64 > .dockerconfigjson
kubectl -n "${NAMESPACE}" create secret generic "${IMAGE_PULL_SECRET}" \
--type=kubernetes.io/dockerconfigjson \
--from-file=.dockerconfigjson
rm -f .dockercfg.b64 .dockerconfigjson
fi
# 배포 전 현재 generation 저장 (spec 변경 감지용)
PREV_GEN=""
if kubectl -n "${NAMESPACE}" get deploy "${RELEASE_NAME}" >/dev/null 2>&1; then
PREV_GEN="$(kubectl -n "${NAMESPACE}" get deploy "${RELEASE_NAME}" -o jsonpath='{.metadata.generation}' 2>/dev/null || true)"
fi
echo "[Deploy] PREV_GEN=${PREV_GEN:-<none>}"
# manifest 렌더링 (envsubst 로 배포마다 바뀌는 값 주입)
rm -rf .rendered && mkdir -p .rendered
export RELEASE_NAME FULL_IMAGE BRANCH_NAME
for f in k8s/*.yaml; do
echo "[Deploy] rendering $f"
envsubst '${RELEASE_NAME} ${FULL_IMAGE} ${BRANCH_NAME}' < "$f" > ".rendered/$(basename "$f")"
done
echo "[Deploy] --- Rendered files ---"
ls -al .rendered
kubectl -n "${NAMESPACE}" apply -f .rendered/
# rollout 완료 대기 (최대 10분)
echo "[Deploy] rollout status 대기 중..."
kubectl -n "${NAMESPACE}" rollout status deploy "${RELEASE_NAME}" --timeout=10m
# spec 변경 없으면 rollout restart
POST_GEN="$(kubectl -n "${NAMESPACE}" get deploy "${RELEASE_NAME}" -o jsonpath='{.metadata.generation}' 2>/dev/null || true)"
echo "[Deploy] POST_GEN=${POST_GEN:-<none>}"
if [ -n "${PREV_GEN}" ] && [ -n "${POST_GEN}" ] && [ "${PREV_GEN}" = "${POST_GEN}" ]; then
echo "[Deploy] Spec 변경 없음 (generation=${POST_GEN}). rollout restart 실행"
kubectl -n "${NAMESPACE}" rollout restart deploy "${RELEASE_NAME}"
kubectl -n "${NAMESPACE}" rollout status deploy "${RELEASE_NAME}" --timeout=10m
else
echo "[Deploy] Spec 변경 감지 또는 최초 배포. rollout restart skip"
fi
echo "[Deploy] --- 배포 후 리소스 상태 ---"
kubectl -n "${NAMESPACE}" get deploy,po,svc \
-l watcher.nodeId="${NODE_ID}",watcher.role="${NODE_TYPE}" || true
'''
}
}
post {
failure {
container('tooling') {
echo '[Deploy] ★ DEPLOY FAILED ★'
sh '''
echo "[Deploy] --- Deployment 상태 ---"
kubectl -n "${NAMESPACE}" describe deploy "${RELEASE_NAME}" || true
echo "[Deploy] --- Pod 이벤트 ---"
kubectl -n "${NAMESPACE}" get events \
--field-selector involvedObject.name="${RELEASE_NAME}" \
--sort-by='.lastTimestamp' || true
'''
}
}
}
}
// ──────────────────────────────────────────────
// Stage 4. Verify
// - rollout 완료 후 Pod 실제 정상 여부 최종 검증
// - Ready condition + restartCount 확인
// - CrashLoopBackOff 즉시 감지
// - 성공 시: Pod 기동 로그 첫 30줄 출력
// - 실패 시: Pod 로그 마지막 50줄 + describe 출력 → Poller 가 수집
// ──────────────────────────────────────────────
stage('Verify') {
steps {
container('tooling') {
sh '''
set -euo pipefail
MAX_WAIT=120
INTERVAL=5
ELAPSED=0
echo "[Verify] Pod 상태 확인 시작 (최대 ${MAX_WAIT}s)"
while [ $ELAPSED -lt $MAX_WAIT ]; do
POD_NAME="$(kubectl -n "${NAMESPACE}" get po \
-l app.kubernetes.io/name="${RELEASE_NAME}" \
--sort-by=.metadata.creationTimestamp \
-o jsonpath='{.items[-1].metadata.name}' 2>/dev/null || true)"
if [ -z "$POD_NAME" ]; then
echo "[Verify] elapsed=${ELAPSED}s | Pod 아직 없음, 대기 중..."
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
continue
fi
PHASE="$(kubectl -n "${NAMESPACE}" get po "${POD_NAME}" \
-o jsonpath='{.status.phase}' 2>/dev/null || true)"
READY="$(kubectl -n "${NAMESPACE}" get po "${POD_NAME}" \
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true)"
RESTART_COUNT="$(kubectl -n "${NAMESPACE}" get po "${POD_NAME}" \
-o jsonpath='{.status.containerStatuses[0].restartCount}' 2>/dev/null || echo '0')"
WAITING_REASON="$(kubectl -n "${NAMESPACE}" get po "${POD_NAME}" \
-o jsonpath='{.status.containerStatuses[0].state.waiting.reason}' 2>/dev/null || true)"
echo "[Verify] elapsed=${ELAPSED}s | Pod=${POD_NAME} | Phase=${PHASE} | Ready=${READY} | Restarts=${RESTART_COUNT} | WaitReason=${WAITING_REASON}"
# CrashLoopBackOff 즉시 실패 처리
if [ "${WAITING_REASON}" = "CrashLoopBackOff" ]; then
echo "[Verify] ★ VERIFY FAILED ★ CrashLoopBackOff 감지"
echo "[Verify] --- Pod 로그 (마지막 50줄) ---"
kubectl -n "${NAMESPACE}" logs "${POD_NAME}" --tail=50 || true
echo "[Verify] --- 이전 컨테이너 로그 ---"
kubectl -n "${NAMESPACE}" logs "${POD_NAME}" --previous --tail=50 || true
echo "[Verify] --- Pod describe ---"
kubectl -n "${NAMESPACE}" describe po "${POD_NAME}" || true
exit 1
fi
# Phase Failed 즉시 실패 처리
if [ "${PHASE}" = "Failed" ]; then
echo "[Verify] ★ VERIFY FAILED ★ Pod Phase=Failed"
echo "[Verify] --- Pod 로그 (마지막 50줄) ---"
kubectl -n "${NAMESPACE}" logs "${POD_NAME}" --tail=50 || true
echo "[Verify] --- Pod describe ---"
kubectl -n "${NAMESPACE}" describe po "${POD_NAME}" || true
exit 1
fi
# restartCount > 0 이면 불안정 판정
if [ "${RESTART_COUNT}" -gt 0 ]; then
echo "[Verify] ★ VERIFY FAILED ★ 재시작 감지 (restartCount=${RESTART_COUNT})"
echo "[Verify] --- Pod 로그 (마지막 50줄) ---"
kubectl -n "${NAMESPACE}" logs "${POD_NAME}" --tail=50 || true
echo "[Verify] --- 이전 컨테이너 로그 ---"
kubectl -n "${NAMESPACE}" logs "${POD_NAME}" --previous --tail=50 || true
echo "[Verify] --- Pod describe ---"
kubectl -n "${NAMESPACE}" describe po "${POD_NAME}" || true
exit 1
fi
# Ready=True 확인 → 최종 성공
if [ "${READY}" = "True" ]; then
echo "[Verify] ✅ VERIFY SUCCESS"
echo "[Verify] Pod=${POD_NAME} | Phase=${PHASE} | Ready=${READY} | Restarts=${RESTART_COUNT}"
echo "[Verify] --- Pod 기동 로그 (첫 30줄) ---"
kubectl -n "${NAMESPACE}" logs "${POD_NAME}" | head -30 || true
echo "[Verify] --- 최종 리소스 상태 ---"
kubectl -n "${NAMESPACE}" get deploy,po,svc \
-l watcher.nodeId="${NODE_ID}",watcher.role="${NODE_TYPE}" || true
exit 0
fi
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done
# 타임아웃
echo "[Verify] ★ VERIFY FAILED ★ 타임아웃 (${MAX_WAIT}s 초과)"
echo "[Verify] --- Pod 로그 (마지막 50줄) ---"
kubectl -n "${NAMESPACE}" logs "${POD_NAME}" --tail=50 || true
echo "[Verify] --- Pod describe ---"
kubectl -n "${NAMESPACE}" describe po "${POD_NAME}" || true
exit 1
'''
}
}
post {
failure {
container('tooling') {
echo '[Verify] ★ VERIFY FAILED - post ★'
sh '''
echo "[Verify] --- Deployment 최종 상태 ---"
kubectl -n "${NAMESPACE}" get deploy "${RELEASE_NAME}" -o wide || true
echo "[Verify] --- Pod 최종 상태 ---"
kubectl -n "${NAMESPACE}" get po \
-l app.kubernetes.io/name="${RELEASE_NAME}" -o wide || true
'''
}
}
}
}
}
post {
success {
echo '✅ DEPLOY SUCCESS'
echo "NODE_ID=${NODE_ID} | BRANCH=${env.BRANCH_NAME} | COMMIT=${env.GIT_COMMIT_HASH}"
}
failure {
echo '★ DEPLOY FAILED ★'
echo "NODE_ID=${NODE_ID} | BRANCH=${env.BRANCH_NAME} | COMMIT=${env.GIT_COMMIT_HASH}"
}
always {
container('tooling') {
sh '''
echo "--- FINAL SUMMARY ---"
kubectl -n "${NAMESPACE}" get deploy "${RELEASE_NAME}" -o wide || true
kubectl -n "${NAMESPACE}" get po \
-l app.kubernetes.io/name="${RELEASE_NAME}" -o wide || true
kubectl -n "${NAMESPACE}" get svc "${RELEASE_NAME}" -o wide || true
'''
}
cleanWs()
}
}
}

38
README.md Normal file
View File

@@ -0,0 +1,38 @@
# ㅋㅌㅊ
파이썬 프로젝트 템플릿
---
## requirements.txt 작성 안내
배포 시 필요한 라이브러리 설치를 위해 **`requirements.txt`를 반드시 작성**해 주세요.
> ⚠️ 주의
> 이 프로젝트는 **import 이름과 pip 설치 이름이 다른 라이브러리**가 포함될 수 있어
> `pipreqs` 같은 자동 생성 도구 결과는 누락/오탐이 날 수 있습니다.
> (예: `import fitz` → `PyMuPDF`)
### 권장 작성 방법
1. 프로젝트가 정상 실행되는 가상환경(venv)에서 설치를 완료한 뒤
2. 아래 명령으로 생성하세요.
```bash
pip freeze > requirements.txt
```
## MCP Tool 작성 안내
MCP(Custom Tool)로 노드를 구현하는 경우, 툴 함수에 설명(docstring)을 반드시 작성해야 합니다.
설명이 없으면 Dify/Tool Registry에서 노드가 정상적으로 노출되지 않거나 동작이 제한될 수 있습니다.
아래 형식을 지켜 주세요:
```
@mcp.tool()
def 함수명(...):
"""
설명 작성 필수
- 이 툴이 무엇을 하는지
- 입력 파라미터 의미
- 반환값 의미
"""
```

65
k8s/deployment.yaml Normal file
View File

@@ -0,0 +1,65 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${RELEASE_NAME}
labels:
app.kubernetes.io/name: ${RELEASE_NAME}
app.kubernetes.io/managed-by: "jenkins"
watcher.project: "10e41ace-9d37-4839-b6f3-e6d4987486ec"
watcher.nodeId: "25082bef-b7a9-41d3-aeeb-30472b03ecd5"
watcher.role: "agent"
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ${RELEASE_NAME}
template:
metadata:
labels:
app.kubernetes.io/name: ${RELEASE_NAME}
app.kubernetes.io/managed-by: "jenkins"
watcher.project: "10e41ace-9d37-4839-b6f3-e6d4987486ec"
watcher.nodeId: "25082bef-b7a9-41d3-aeeb-30472b03ecd5"
watcher.role: "agent"
watcher.branch: ${BRANCH_NAME}
annotations:
watcher.enabled: "true"
spec:
imagePullSecrets:
- name: cr-pull
containers:
- name: app
image: ${FULL_IMAGE}
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
env:
- name: TZ
value: "Asia/Seoul"
# 기본 probe (필요 없으면 제거/수정)
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 3
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 6
resources:
requests:
cpu: "50m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"

14
k8s/service.yaml Normal file
View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: ${RELEASE_NAME}
labels:
app.kubernetes.io/name: ${RELEASE_NAME}
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: ${RELEASE_NAME}
ports:
- name: http
port: 80
targetPort: http

0
requirements.txt Normal file
View File

1
src/__init__.py Normal file
View File

@@ -0,0 +1 @@
# ㅋㅌㅊ agent 패키지 초기화 파일

10
src/main.py Normal file
View File

@@ -0,0 +1,10 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
"""
This is an api that says, "Hello!"
"""
return {"response": "Hello ! ㅋㅌㅊ !"}