diff --git a/.github/workflows/ci-petclinic-gke.yaml b/.github/workflows/ci-petclinic-gke.yaml new file mode 100644 index 000000000..00c01773c --- /dev/null +++ b/.github/workflows/ci-petclinic-gke.yaml @@ -0,0 +1,147 @@ +name: CI - Petclinic GKE + +permissions: + contents: read + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }} + GAR_LOCATION: ${{ vars.GAR_LOCATION || 'asia-northeast3' }} + GAR_REPOSITORY: ${{ vars.GAR_REPOSITORY || 'petclinic' }} + IMAGE_NAME: ${{ vars.IMAGE_NAME || 'petclinic' }} + +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "25" + cache: maven + + - name: Maven build & test + run: | + if [ -x "./mvnw" ]; then + ./mvnw -B clean test package + else + mvn -B clean test package + fi + + - name: Archive built JAR + uses: actions/upload-artifact@v4 + with: + name: petclinic-jar + path: target/*.jar + + build-and-push-image: + needs: build-test + runs-on: ubuntu-latest + + if: github.event_name != 'pull_request' && github.actor != 'github-actions[bot]' + + permissions: + contents: write + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "25" + cache: maven + + - name: Auth to Google Cloud (WIF) + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }} + service_account: ${{ secrets.GCP_SA_EMAIL }} + + - name: Set up gcloud + uses: google-github-actions/setup-gcloud@v2 + + - name: Configure docker for Artifact Registry + run: | + gcloud auth configure-docker ${GAR_LOCATION}-docker.pkg.dev --quiet + + - name: Set image tag + run: | + SHORT_SHA=${GITHUB_SHA::7} + echo "IMAGE_TAG=${SHORT_SHA}" >> "$GITHUB_ENV" + + - name: Read base version from pom.xml + run: | + if [ -x "./mvnw" ]; then + POM_VERSION=$(./mvnw -q -Dexpression=project.version -DforceStdout help:evaluate) + else + POM_VERSION=$(mvn -q -Dexpression=project.version -DforceStdout help:evaluate) + fi + BASE_VERSION=${POM_VERSION%-SNAPSHOT} + echo "BASE_VERSION=${BASE_VERSION}" >> "$GITHUB_ENV" + + - name: Build APP_VERSION + run: | + APP_VERSION="G-${BASE_VERSION}-${IMAGE_TAG}" + echo "APP_VERSION=${APP_VERSION}" >> "$GITHUB_ENV" + + - name: Update application version property + run: | + sed -i "s/^app.version=.*/app.version=${APP_VERSION}/" src/main/resources/application.properties + + - name: Build Docker image + env: + DOCKER_BUILDKIT: 1 + run: | + IMAGE_URI="${GAR_LOCATION}-docker.pkg.dev/${GCP_PROJECT_ID}/${GAR_REPOSITORY}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "IMAGE_URI=${IMAGE_URI}" >> "$GITHUB_ENV" + docker build -t "$IMAGE_URI" . + + - name: Push Docker image + run: | + docker push "$IMAGE_URI" + + - name: Tag image as latest + if: github.ref == 'refs/heads/main' + run: | + IMAGE_BASE="${GAR_LOCATION}-docker.pkg.dev/${GCP_PROJECT_ID}/${GAR_REPOSITORY}/${IMAGE_NAME}" + docker tag "${IMAGE_BASE}:${IMAGE_TAG}" "${IMAGE_BASE}:latest" + docker push "${IMAGE_BASE}:latest" + + - name: Update Kubernetes manifest image tag + if: github.ref == 'refs/heads/main' + env: + YAML_PATH: k8s/gcp/20-petclinic-Deployments-postgre.yaml + run: | + NEW_IMAGE="${GAR_LOCATION}-docker.pkg.dev/${GCP_PROJECT_ID}/${GAR_REPOSITORY}/${IMAGE_NAME}:${IMAGE_TAG}" + grep -n "image:" "$YAML_PATH" + sed -i "s#^\(\s*image:\s*\).*#\1${NEW_IMAGE}#" "$YAML_PATH" + + - name: Commit and push changes + if: github.ref == 'refs/heads/main' + run: | + git config user.name "github-actions" + git config user.email "github-actions@github.com" + git add src/main/resources/application.properties k8s/gcp/20-petclinic-Deployments-postgre.yaml + if git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + git commit -m "chore: update petclinic image to ${IMAGE_TAG} [skip ci]" + git push diff --git a/k8s/gcp/00-petclinic-ns.yaml b/k8s/gcp/00-petclinic-ns.yaml new file mode 100644 index 000000000..47836737a --- /dev/null +++ b/k8s/gcp/00-petclinic-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: petclinic-ns diff --git a/k8s/gcp/01-petclinic-sa.yaml b/k8s/gcp/01-petclinic-sa.yaml new file mode 100644 index 000000000..90ad00211 --- /dev/null +++ b/k8s/gcp/01-petclinic-sa.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: petclinic-sa + namespace: petclinic-ns \ No newline at end of file diff --git a/k8s/gcp/11-petclinic-secret-es.yaml b/k8s/gcp/11-petclinic-secret-es.yaml new file mode 100644 index 000000000..ce8f181b2 --- /dev/null +++ b/k8s/gcp/11-petclinic-secret-es.yaml @@ -0,0 +1,26 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: petclinic-db-secret + namespace: petclinic-ns +spec: + refreshInterval: 1h + secretStoreRef: + name: gcp-secretmanager + kind: ClusterSecretStore + target: + name: petclinic-db-secret + creationPolicy: Owner + data: + - secretKey: SPRING_DATASOURCE_URL + remoteRef: + key: finalproj-dev-petclinic-db # 예: GCP Secret 이름 + property: jdbc_url # secret value가 JSON일 때 property 사용 + - secretKey: SPRING_DATASOURCE_USERNAME + remoteRef: + key: finalproj-dev-petclinic-db + property: username + - secretKey: SPRING_DATASOURCE_PASSWORD + remoteRef: + key: finalproj-dev-petclinic-db + property: password diff --git a/k8s/gcp/12-petclinic-clustersecretstore.yaml b/k8s/gcp/12-petclinic-clustersecretstore.yaml new file mode 100644 index 000000000..61e1d5dd3 --- /dev/null +++ b/k8s/gcp/12-petclinic-clustersecretstore.yaml @@ -0,0 +1,16 @@ +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: + name: gcp-secretmanager +spec: + provider: + gcpsm: + projectID: kdt2-final-project-t2 + auth: + workloadIdentity: + clusterLocation: asia-northeast3 + clusterName: k8s-gke + serviceAccountRef: + name: external-secrets + namespace: external-secrets + diff --git a/k8s/gcp/20-petclinic-Deployments-postgre.yaml b/k8s/gcp/20-petclinic-Deployments-postgre.yaml new file mode 100644 index 000000000..4103e10ab --- /dev/null +++ b/k8s/gcp/20-petclinic-Deployments-postgre.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: petclinic + namespace: petclinic-ns + labels: + app: petclinic +spec: + replicas: 2 + selector: + matchLabels: + app: petclinic + template: + metadata: + labels: + app: petclinic + spec: + # 🔹 여기: service account 지정 + serviceAccountName: petclinic-sa + + nodeSelector: + role: "app" + + tolerations: + - key: "role" + operator: "Equal" + value: "app" + effect: "NoSchedule" + + containers: + - name: petclinic-container + image: 723926525504.dkr.ecr.ap-northeast-2.amazonaws.com/eks/petclinic:e6690f6 + + # DB 설정은 ConfigMap / Secret에서 그대로 가져오기 + envFrom: + - secretRef: + name: petclinic-db-secret + + # 🔹 여기: Spring profile을 postgres용으로 + env: + - name: SPRING_PROFILES_ACTIVE + value: "postgres" + - name: SPRING_DATASOURCE_DRIVER_CLASS_NAME + value: "org.postgresql.Driver" + + ports: + - name: http + containerPort: 8080 + + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: http + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 5 + + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: http + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 5 + + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "500m" + memory: "1Gi" diff --git a/k8s/gcp/31-petclinic-ingress.yaml b/k8s/gcp/31-petclinic-ingress.yaml new file mode 100644 index 000000000..1c8bec8f7 --- /dev/null +++ b/k8s/gcp/31-petclinic-ingress.yaml @@ -0,0 +1,46 @@ +apiVersion: cloud.google.com/v1 +kind: BackendConfig +metadata: + name: petclinic-backendconfig + namespace: petclinic-ns +spec: + healthCheck: + type: HTTP + requestPath: /actuator/health/liveness + port: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: petclinic + namespace: petclinic-ns + annotations: + beta.cloud.google.com/backend-config: '{"default":"petclinic-backendconfig"}' +spec: + type: ClusterIP + selector: + app: petclinic + ports: + - name: http + port: 80 + targetPort: http +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: petclinic + namespace: petclinic-ns + annotations: + kubernetes.io/ingress.class: "gce" +spec: + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: petclinic + port: + number: 80 +