From e100f4ab0dc4cb2292a9442347a351ec1acb9c0d Mon Sep 17 00:00:00 2001 From: rheiga19 Date: Wed, 26 Mar 2025 22:06:38 +0700 Subject: [PATCH] deploy to kubernetes --- Jenkinsfile | 120 ++++++++++++++++++++++++++++++ docker/Dockerfile | 37 ++++++++++ kubernetes/dev.yaml | 175 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 332 insertions(+) create mode 100644 Jenkinsfile create mode 100644 docker/Dockerfile create mode 100644 kubernetes/dev.yaml diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..617eb17 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,120 @@ +pipeline { + agent { + kubernetes { + yaml ''' +apiVersion: v1 +kind: Pod +metadata: + name: my-pod +spec: + containers: + - name: kubectl + image: bitnami/kubectl:latest + command: + - cat + tty: true + securityContext: + runAsUser: 1000 + + - name: docker + image: docker:latest + command: + - cat + tty: true + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + +''' + } + } + + triggers { + pollSCM('H/5 * * * *') + } + + environment { + REGISTRY_URL = 'git.winteraccess.id' + IMAGE_NAME = 'winter-access/frontend-nam.git' + KUBE_CONFIG_ID = '1c8bd8d9-1590-468c-afc7-24495d4330dc' + CREDENTIALS_ID = '45c25ade-b1f8-455b-bdd6-e11ff141b70c' + } + + stages { + stage('Checkout Code') { + steps { + cleanWs() + git branch: 'master', url: 'https://git.winteraccess.id/winter-access/frontend-nam.git', credentialsId: "${CREDENTIALS_ID}" + } + } + + stage('Get Short SHA') { + steps { + script { + env.SHORT_SHA = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + } + } + } + + stage('Login to Docker Registry') { + steps { + container('docker'){ + withCredentials([usernamePassword(credentialsId: "08063d26-0005-4942-9b87-9d819a13b973", + usernameVariable: 'DOCKER_USER', + passwordVariable: 'DOCKER_PASS')]) { + sh "docker login -u ${DOCKER_USER} -p ${DOCKER_PASS} ${REGISTRY_URL}" + } + } + } + } + + + stage('Build and Push Docker Image') { + steps { + container('docker') { // Runs in the Docker container inside Kubernetes + script { + def imageTag = "dev-${env.SHORT_SHA}" + sh """ + docker build -t ${REGISTRY_URL}/${IMAGE_NAME}:${imageTag} \ + -t ${REGISTRY_URL}/${IMAGE_NAME}:master \ + -t ${REGISTRY_URL}/${IMAGE_NAME}:latest \ + -f deploy/docker/Dockerfile . + + docker push ${REGISTRY_URL}/${IMAGE_NAME}:${imageTag} + docker push ${REGISTRY_URL}/${IMAGE_NAME}:master + docker push ${REGISTRY_URL}/${IMAGE_NAME}:latest + """ + } + } + } + } + + stage('Deploy and Restart to Kubernetes') { + steps { + container('kubectl') { // ✅ Runs kubectl inside the correct container + sh 'ls -la deploy/kubernetes/' + withKubeConfig([credentialsId: '1c8bd8d9-1590-468c-afc7-24495d4330dc']) { // ✅ Uses the Jenkins credential + sh ''' + set -e + kubectl apply -f deploy/kubernetes/dev.yaml --validate=true + kubectl rollout restart deployment nam-frontend-dev -n nam-frontend-dev + ''' + } + } + } + } + + + } + + post { + always { + cleanWs() + } + } +} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..d8b0d54 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,37 @@ +# Build stage +FROM node:20-alpine as build + +WORKDIR /app + +# Copy package files and install dependencies +COPY package.json package-lock.json* ./ +RUN npm ci + +# Copy the rest of the application code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy the built files from the build stage to nginx +COPY --from=build /app/dist /usr/share/nginx/html + +# Create a custom nginx configuration that handles SPA routing +RUN echo 'server { \ + listen 80; \ + server_name _; \ + root /usr/share/nginx/html; \ + index index.html; \ + location / { \ + try_files $uri $uri/ /index.html; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/kubernetes/dev.yaml b/kubernetes/dev.yaml new file mode 100644 index 0000000..08df3f4 --- /dev/null +++ b/kubernetes/dev.yaml @@ -0,0 +1,175 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: frontend-nam-dev-config + namespace: nam-frontend-dev + labels: + app.kubernetes.io/name: frontend-nam-dev + app.kubernetes.io/instance: frontend-nam-dev +data: + ".env": | + VITE_API_URL=https://api.example.com + VITE_APP_ENV=development +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend-nam-dev + namespace: nam-frontend-dev + labels: + app.kubernetes.io/instance: frontend-nam-dev + app.kubernetes.io/name: frontend-nam-dev +spec: + progressDeadlineSeconds: 600 + replicas: 2 + revisionHistoryLimit: 10 + selector: + matchLabels: + app.kubernetes.io/instance: frontend-nam-dev + app.kubernetes.io/name: frontend-nam-dev + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app.kubernetes.io/instance: frontend-nam-dev + app.kubernetes.io/name: frontend-nam-dev + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/instance: frontend-nam-dev + app.kubernetes.io/name: frontend-nam-dev + topologyKey: "kubernetes.io/hostname" + containers: + - name: web + image: git.winteraccess.id/winter-access/frontend-nam:dev + imagePullPolicy: Always + resources: + limits: + cpu: "250m" + memory: 512M + requests: + cpu: "100m" + memory: 256M + ports: + - containerPort: 80 + name: http + protocol: TCP + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: false + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + envFrom: + - configMapRef: + name: frontend-nam-dev-config + imagePullSecrets: + - name: winter-registry + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + runAsNonRoot: true + terminationGracePeriodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend-nam-dev + namespace: nam-frontend-dev + labels: + app.kubernetes.io/name: frontend-nam-dev + app.kubernetes.io/instance: frontend-nam-dev +spec: + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 80 + selector: + app.kubernetes.io/instance: frontend-nam-dev + app.kubernetes.io/name: frontend-nam-dev + sessionAffinity: None + type: ClusterIP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.entrypoints: web + traefik.ingress.kubernetes.io/router.middlewares: default-https-redirect@kubernetescrd + labels: + app.kubernetes.io/instance: frontend-nam-dev + app.kubernetes.io/name: frontend-nam-dev + name: frontend-nam-dev-http + namespace: nam-frontend-dev +spec: + ingressClassName: traefik + rules: + - host: dev-nam-frontend.winteraccess.id + http: + paths: + - backend: + service: + name: frontend-nam-dev + port: + number: 80 + path: / + pathType: Prefix +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production + kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.entrypoints: websecure + labels: + app.kubernetes.io/instance: frontend-nam-dev + app.kubernetes.io/name: frontend-nam-dev + name: frontend-nam-dev-https + namespace: nam-frontend-dev +spec: + ingressClassName: traefik + rules: + - host: dev-nam-frontend.winteraccess.id + http: + paths: + - backend: + service: + name: frontend-nam-dev + port: + number: 80 + path: / + pathType: Prefix + tls: + - hosts: + - dev-nam-frontend.winteraccess.id + secretName: frontend-nam-dev-tls