From 3d8f55e8a2ba6103465ea5db41d33940a7d079d9 Mon Sep 17 00:00:00 2001 From: llm-bot Date: Thu, 26 Mar 2026 17:50:50 +0900 Subject: [PATCH] Initial commit --- .gitignore | 4 + Dockerfile | 12 ++ Jenkinsfile | 327 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 38 +++++ k8s/deployment.yaml | 65 +++++++++ k8s/service.yaml | 14 ++ requirements.txt | 0 src/__init__.py | 1 + src/main.py | 10 ++ 9 files changed, 471 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Jenkinsfile create mode 100644 README.md create mode 100644 k8s/deployment.yaml create mode 100644 k8s/service.yaml create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b91c72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +venv/ +__pycache__/ +*.pyc +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dd069b8 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..8bce395 --- /dev/null +++ b/Jenkinsfile @@ -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:-}" + + # 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:-}" + + 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() + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2603624 --- /dev/null +++ b/README.md @@ -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 함수명(...): + """ + 설명 작성 필수 + - 이 툴이 무엇을 하는지 + - 입력 파라미터 의미 + - 반환값 의미 + """ +``` diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..0c1f61f --- /dev/null +++ b/k8s/deployment.yaml @@ -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" diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..857e459 --- /dev/null +++ b/k8s/service.yaml @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..30ea60e --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +# ㅋㅌㅊ agent 패키지 초기화 파일 \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..6e1fdd5 --- /dev/null +++ b/src/main.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +def root(): + """ + This is an api that says, "Hello!" + """ + return {"response": "Hello ! ㅋㅌㅊ !"} \ No newline at end of file