This commit is contained in:
changmin hyeon
2026-02-10 11:33:59 +09:00
commit 0d808202f6
17 changed files with 575 additions and 0 deletions

4
common/.gitignore.j2 Normal file
View File

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

View File

@@ -0,0 +1,53 @@
pipeline {
agent any
parameters {
string(name: 'BRANCH_NAME', defaultValue: '', description: '빌드할 브랜치명(비우면 main)')
string(name: 'GIT_COMMIT_HASH', defaultValue: '', description: '빌드할 커밋 해시(비우면 최신 커밋 기준)')
}
stages {
stage('Checkout') {
steps {
script {
sh 'git fetch --all'
def checkoutBranch = params.BRANCH_NAME?.trim() ? params.BRANCH_NAME.trim() : 'main'
def commitHash = params.GIT_COMMIT_HASH?.trim()
if (commitHash) {
echo "브랜치(${checkoutBranch})의 커밋 해시(${commitHash})로 체크아웃합니다."
sh "git fetch origin ${checkoutBranch}"
sh "git checkout ${checkoutBranch}"
sh "git checkout ${commitHash}"
} else {
echo "브랜치(${checkoutBranch})의 최신 커밋으로 체크아웃합니다."
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
}
}
}
stage('Build & Deploy') {
steps {
script {
def composeProject = "{{ node_id }}-" + (env.BRANCH_NAME ?: "") + "{{ node_type }}"
writeFile file: '.env', text: """
BRANCH_NAME=${env.BRANCH_NAME}
GIT_COMMIT_HASH=${env.GIT_COMMIT_HASH}
"""
sh "cat .env"
sh "cat docker-compose.yml"
sh "docker compose -p ${composeProject} --env-file .env down --rmi all"
sh "docker compose -p ${composeProject} build --no-cache"
sh "docker compose -p ${composeProject} --env-file .env up --force-recreate --remove-orphans -d"
}
}
}
}
post {
always {
cleanWs()
}
}
}

168
common/Jenkinsfile.k8s.j2 Normal file
View File

@@ -0,0 +1,168 @@
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 {
// 고정 값(노드 생성 시점에 Jinja로 박힘)
NODE_ID = '{{ node_id }}'
NODE_TYPE = '{{ node_type }}'
NAMESPACE = '{{ namespace }}'
REGISTRY = '{{ regi }}'
platform_namespace = '{{ platform_namespace }}'
// ✅ imagePullSecret 이름도 "메소드에서 전달"되도록 Jinja로 박힘
// (create_node_project_files(extra_context={"image_pull_secret":"..."}) 로 전달)
IMAGE_PULL_SECRET = '{{ image_pull_secret | default("acr-pull") }}'
}
stages {
stage('Checkout') {
steps {
container('jnlp') {
sh '''
set -e
git fetch --all || true
'''
script {
def checkoutBranch = params.BRANCH_NAME?.trim() ? params.BRANCH_NAME.trim() : 'main'
def commitHash = params.GIT_COMMIT_HASH?.trim()
if (commitHash) {
sh """
git fetch origin ${checkoutBranch}
git checkout ${checkoutBranch}
git checkout ${commitHash}
"""
} else {
sh """
git fetch origin ${checkoutBranch}
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}-${env.BRANCH_NAME}"
env.IMAGE_REPO = "${REGISTRY}/node.${NODE_ID}.${env.BRANCH_NAME}"
env.IMAGE_TAG = env.GIT_COMMIT_HASH
env.FULL_IMAGE = "${env.IMAGE_REPO}:${env.IMAGE_TAG}"
echo "RELEASE_NAME=${env.RELEASE_NAME}"
echo "FULL_IMAGE=${env.FULL_IMAGE}"
echo "IMAGE_PULL_SECRET=${env.IMAGE_PULL_SECRET}"
}
}
}
}
stage('Build Image (Kaniko)') {
steps {
container('kaniko') {
retry(3) {
sh '''
set -e
/kaniko/executor \
--context=$WORKSPACE \
--dockerfile=Dockerfile \
--destination=$FULL_IMAGE \
--cache=true \
--snapshot-mode=time
'''
}
}
}
}
stage('Deploy (Manifests)') {
steps {
container('tooling') {
sh '''
set -euo pipefail
# ✅ B) imagePullSecret 복제 (platform -> ${NAMESPACE})
# secret 이름도 IMAGE_PULL_SECRET로 통일
if [ -n "${IMAGE_PULL_SECRET}" ] && ! kubectl -n "${NAMESPACE}" get secret "${IMAGE_PULL_SECRET}" >/dev/null 2>&1; then
echo "[INFO] Copying imagePullSecret '${IMAGE_PULL_SECRET}' platform -> ${NAMESPACE} ..."
{% raw %}
kubectl -n "${platform_namespace}" get secret "${IMAGE_PULL_SECRET}" -o go-template='{{ index .data ".dockerconfigjson" }}' > .dockercfg.b64
{% endraw %}
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
# ✅ C) 배포 전 Deployment generation 저장
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 "[INFO] PREV_GEN=${PREV_GEN:-<none>} deploy=${RELEASE_NAME}"
# ✅ D) repo의 manifests 템플릿을 렌더링 후 apply
# - manifests/*.yaml 은 Jinja로 이미 한번 렌더된 상태(노드 생성 시점)
# - 여기서는 배포마다 바뀌는 값(RELASE_NAME, FULL_IMAGE, BRANCH_NAME)만 envsubst로 주입
rm -rf .rendered && mkdir -p .rendered
# envsubst 치환 대상 화이트리스트
export RELEASE_NAME FULL_IMAGE BRANCH_NAME
for f in manifests/*.yaml; do
echo "[INFO] render $f"
envsubst '${RELEASE_NAME} ${FULL_IMAGE} ${BRANCH_NAME}' < "$f" > ".rendered/$(basename "$f")"
done
echo "---- Rendered files ----"
ls -al .rendered
kubectl -n "${NAMESPACE}" apply -f .rendered/
# ✅ E) 롤아웃 대기(Deployment가 이 이름으로 생성된다는 전제)
kubectl -n "${NAMESPACE}" rollout status deploy "${RELEASE_NAME}" --timeout=10m
# ✅ F) generation 비교 → 안 바뀌면 rollout restart
POST_GEN="$(kubectl -n "${NAMESPACE}" get deploy "${RELEASE_NAME}" -o jsonpath='{.metadata.generation}' 2>/dev/null || true)"
echo "[INFO] POST_GEN=${POST_GEN:-<none>} deploy=${RELEASE_NAME}"
if [ -n "${PREV_GEN}" ] && [ -n "${POST_GEN}" ] && [ "${PREV_GEN}" = "${POST_GEN}" ]; then
echo "[INFO] Deployment spec unchanged (generation=${POST_GEN}). Running rollout restart..."
kubectl -n "${NAMESPACE}" rollout restart deploy "${RELEASE_NAME}"
kubectl -n "${NAMESPACE}" rollout status deploy "${RELEASE_NAME}" --timeout=10m
else
echo "[INFO] Deployment spec changed (or first install). Skip rollout restart."
fi
echo '--- STATUS ---'
kubectl -n "${NAMESPACE}" get deploy,po,svc -l watcher.nodeId="${NODE_ID}",watcher.role="${NODE_TYPE}" || true
'''
}
}
}
}
post {
always {
container('tooling') {
sh '''
set +e
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
common/README.md.j2 Normal file
View File

@@ -0,0 +1,38 @@
# {{ node_name }}
파이썬 프로젝트 템플릿
---
## 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 함수명(...):
"""
설명 작성 필수
- 이 툴이 무엇을 하는지
- 입력 파라미터 의미
- 반환값 의미
"""
```

View File

@@ -0,0 +1,17 @@
version: "3.8"
services:
{{ node_id }}:
build: .
container_name: {{ node_id }}-${BRANCH_NAME}-{{ node_type }}
image: {{ node_id }}.${BRANCH_NAME}:${GIT_COMMIT_HASH}
environment:
- PYTHONUNBUFFERED=1
- BRANCH_NAME=${BRANCH_NAME}
- GIT_COMMIT_HASH=${GIT_COMMIT_HASH}
restart: unless-stopped
networks:
- ai
networks:
ai:
external: true

View File

1
common/src__init__.py.j2 Normal file
View File

@@ -0,0 +1 @@
# {{ node_name }} {{ node_type }} 패키지 초기화 파일