diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..4598c5a
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,2 @@
+/helm/ @Mobyman
+/.github/ @Mobyman
\ No newline at end of file
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..bf68d00
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,130 @@
+name: Deploy to Kubernetes
+
+on:
+ push:
+ branches:
+ - main
+ - canary
+ - staging
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ env:
+ APP_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || github.ref == 'refs/heads/canary' && 'canary' || github.ref == 'refs/heads/staging' && 'staging' || 'unknown' }}
+ APP_DOMAIN: ${{ github.ref == 'refs/heads/staging' && vars.APP_DOMAIN_STAGING || vars.APP_DOMAIN }}
+
+ permissions:
+ packages: write
+ contents: read
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ vars.AWS_REGION }}
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Set sha-short
+ run: echo "GITHUB_SHA_SHORT=$(echo $GITHUB_SHA | cut -c 1-7)" >> $GITHUB_ENV
+
+ - id: lower-repo
+ name: Repository to lowercase
+ run: |
+ echo "repository=${GITHUB_REPOSITORY@L}" >> $GITHUB_OUTPUT
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ghcr.io/${{ steps.lower-repo.outputs.repository }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ tags: |
+ type=sha
+ type=sha,format=long
+ type=ref,event=branch
+
+ - name: Build and push Docker image ${{ steps.lower-repo.outputs.repository }}:${{ env.APP_ENV }}
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ tags: ghcr.io/${{ steps.lower-repo.outputs.repository }}:${{ env.GITHUB_SHA_SHORT }},ghcr.io/${{ steps.lower-repo.outputs.repository }}:${{ env.APP_ENV }}
+ build-args: |
+ sha=${{ github.sha }}
+ sha_short=${{ env.GITHUB_SHA_SHORT }}
+ app_env=${{ vars.APP_ENV }}
+
+ - name: Apply AWS k8s config
+ run: aws eks update-kubeconfig --name ${{ vars.AWS_CLUSTER }} --region ${{ vars.AWS_REGION }}
+
+ - name: Create namespace
+ run: |
+ kubectl create ns ${{ vars.APP_NAME }}-${{ env.APP_ENV }} || echo "Namespace $EKS_NAMESPACE already exists"
+
+ - name: Deploy ${{ vars.APP_NAME }} to Kubernetes
+ run: |
+ helm upgrade --install ${{ vars.APP_NAME }} ./helm/app \
+ --namespace ${{ vars.APP_NAME }}-${{ env.APP_ENV }} \
+ --values ./helm/app/values.yaml \
+ --values ./helm/app/values-${{ env.APP_ENV }}.yaml \
+ --set imageRepo="ghcr.io/${{ steps.lower-repo.outputs.repository }}" \
+ --set imageTag="${{ env.GITHUB_SHA_SHORT }}" \
+ --set host=${{ env.APP_DOMAIN }} \
+ --set appName=${{ vars.APP_NAME }} \
+ --set ghcrSecret=${{ secrets.GHCR_SECRET }} \
+ --set secrets.publicProxyKey=${{ secrets.NEXT_PUBLIC_MIXPANEL_TOKEN }} \
+ --set secrets.publicMixPanelToken=${{ secrets.NEXT_PUBLIC_ANALYTICS_ENABLED }} \
+ --set secrets.publicProxyKey=${{ secrets.NEXT_PUBLIC_ANALYTICS_ENABLED }}
+
+ - name: Verify deployment
+ run: |
+ kubectl -n ${{ vars.APP_NAME }}-${{ env.APP_ENV }} rollout status deployment/${{ vars.APP_NAME }}-${{ env.APP_ENV }}
+
+ - name: Verify TLS Certificate
+ run: |
+ kubectl describe certificate ${{ env.APP_DOMAIN }} -n ${{ vars.APP_NAME }}-${{ env.APP_ENV }}
+
+ - name: Telegram Notify
+ uses: appleboy/telegram-action@v1.0.0
+ if: success() && contains('${{ vars.ENABLE_DEPLOY_BOT }}', 1)
+ with:
+ to: ${{ secrets.TELEGRAM_DEPLOY_CHAT_ID }}
+ token: ${{ secrets.TELEGRAM_DEPLOY_TOKEN }}
+ format: markdown
+ message: |
+ π The application from repository [${{ steps.lower-repo.outputs.repository }}](https://github.com/${{ steps.lower-repo.outputs.repository }}) has been successfully deployed by [${{ github.actor }}](https://github.com/users/${{ github.actor }}) on ${{ env.APP_ENV }}.
+
+ ποΈ [GitHub Actions Build](https://github.com/${{ steps.lower-repo.outputs.repository }}/actions/runs/${{ github.run_id }})
+ π³ [Image](https://ghcr.io/${{ steps.lower-repo.outputs.repository }}:${{ env.GITHUB_SHA_SHORT }}
+ π [Link](https://${{ env.APP_DOMAIN }})
+
+ - name: Telegram Notify
+ uses: appleboy/telegram-action@v1.0.0
+ if: failure()
+ with:
+ to: ${{ secrets.TELEGRAM_DEPLOY_CHAT_ID }}
+ token: ${{ secrets.TELEGRAM_DEPLOY_TOKEN }}
+ format: markdown
+ message: |
+ π¨Deploy of the application from repository [${{ steps.lower-repo.outputs.repository }}](https://github.com/${{ steps.lower-repo.outputs.repository }}) on ${{ env.APP_ENV }} has been failed.
+
+ ποΈ [GitHub Actions Build](https://github.com/${{ steps.lower-repo.outputs.repository }}/actions/runs/${{ github.run_id }})
+ π³ [Image](https://ghcr.io/${{ steps.lower-repo.outputs.repository }}:${{ env.GITHUB_SHA_SHORT }}
+ π [Link](https://${{ env.APP_DOMAIN }})
+
+
+
+
+
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/web-ide.iml b/.idea/web-ide.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/web-ide.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..cabec61
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,10 @@
+FROM node:21-alpine AS base
+
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY . .
+RUN npm run build
+
+EXPOSE 3000
+CMD ["npm", "start"]
diff --git a/helm/app/Chart.yaml b/helm/app/Chart.yaml
new file mode 100644
index 0000000..cf9ba02
--- /dev/null
+++ b/helm/app/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v2
+name: Node.js Chart
+description: A Helm chart for deploying my Node.js application
+version: 0.1.2
diff --git a/helm/app/templates/deployment.yaml b/helm/app/templates/deployment.yaml
new file mode 100644
index 0000000..50fe9d2
--- /dev/null
+++ b/helm/app/templates/deployment.yaml
@@ -0,0 +1,54 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ namespace: {{ .Release.Namespace }}
+ labels:
+ app: {{ .Values.appName }}-{{ .Values.deployEnv }}
+ release: prometheus-stack
+spec:
+ replicas: {{ .Values.defaultReplicaCount }}
+ strategy:
+ type: RollingUpdate
+ selector:
+ matchLabels:
+ app: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ template:
+ metadata:
+ labels:
+ app: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ release: prometheus-stack
+ spec:
+ topologySpreadConstraints:
+ - maxSkew: 1
+ topologyKey: kubernetes.io/hostname
+ whenUnsatisfiable: ScheduleAnyway
+ labelSelector:
+ matchLabels:
+ app: {{ .Values.appName }}-{{ .Values.deployEnv }}
+ containers:
+ - name: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ image: "{{ .Values.imageRepo }}:{{ .Values.imageTag }}"
+ env:
+ - name: APP_ENV
+ value: {{ .Values.deployEnv }}
+ - name: APP_VERSION
+ value: {{ .Values.appVersion | quote }}
+ - name: NEXT_PUBLIC_PROXY_KEY
+ value: {{ .Values.secrets.publicProxyKey }}
+ - name: NEXT_PUBLIC_MIXPANEL_TOKEN
+ value: {{ .Values.secrets.publicMixPanelToken }}
+ - name: NEXT_PUBLIC_ANALYTICS_ENABLED
+ value:{{ .Values.secrets.publicProxyKey }}
+ ports:
+ - containerPort: {{ .Values.containerPort }}
+ resources:
+ limits:
+ cpu: {{ .Values.cpuLimit }}
+ memory: {{ .Values.memoryLimit }}
+ requests:
+ cpu: {{ .Values.cpuRequest }}
+ memory: {{ .Values.memoryRequest }}
+ imagePullPolicy: Always
+ imagePullSecrets:
+ - name: dockerconfigjson-github-com
diff --git a/helm/app/templates/ghcr-secret.yaml b/helm/app/templates/ghcr-secret.yaml
new file mode 100644
index 0000000..d126766
--- /dev/null
+++ b/helm/app/templates/ghcr-secret.yaml
@@ -0,0 +1,10 @@
+kind: Secret
+type: kubernetes.io/dockerconfigjson
+apiVersion: v1
+metadata:
+ name: dockerconfigjson-github-com
+ namespace: {{ .Release.Namespace }}
+ labels:
+ app: {{ .Values.appName }}-{{ .Values.deployEnv}}
+data:
+ .dockerconfigjson: {{ .Values.ghcrSecret }}
\ No newline at end of file
diff --git a/helm/app/templates/hpa.yaml b/helm/app/templates/hpa.yaml
new file mode 100644
index 0000000..c5c85d2
--- /dev/null
+++ b/helm/app/templates/hpa.yaml
@@ -0,0 +1,25 @@
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+ name: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ namespace: {{ .Release.Namespace }}
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ minReplicas: {{ .Values.minReplicas }}
+ maxReplicas: {{ .Values.maxReplicas }}
+ metrics:
+ - type: Resource
+ resource:
+ name: cpu
+ target:
+ type: Utilization
+ averageUtilization: 70
+ - type: Resource
+ resource:
+ name: memory
+ target:
+ type: Utilization
+ averageUtilization: 80
diff --git a/helm/app/templates/ingress.yaml b/helm/app/templates/ingress.yaml
new file mode 100644
index 0000000..d1759c8
--- /dev/null
+++ b/helm/app/templates/ingress.yaml
@@ -0,0 +1,44 @@
+{{ if .Values.publicService }}
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ namespace: {{ .Release.Namespace }}
+ annotations:
+ kubernetes.io/ingress.class: "nginx"
+ nginx.ingress.kubernetes.io/ssl-redirect: "{{ .Values.sslRedirect }}"
+ nginx.ingress.kubernetes.io/proxy-connect-timeout: "10s"
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "15s"
+ nginx.ingress.kubernetes.io/proxy-send-timeout: "15s"
+ nginx.ingress.kubernetes.io/proxy-next-upstream: "error timeout http_502 http_503 http_504"
+ nginx.ingress.kubernetes.io/proxy-next-upstream-tries: "3"
+ cert-manager.io/cluster-issuer: {{ .Values.tlsIssuer }}
+ {{- if eq .Values.deployEnv "canary" }}
+ nginx.ingress.kubernetes.io/canary: "true"
+ nginx.ingress.kubernetes.io/canary-weight: {{ .Values.canaryWeight | quote }}
+ {{- end }}
+ nginx.ingress.kubernetes.io/server-snippet: |
+ location ~ ^/(metrics|ready|health)$ {
+ return 403;
+ }
+
+ labels:
+ release: prometheus-stack
+ app: {{ .Values.appName }}-{{ .Values.deployEnv}}
+spec:
+ tls:
+ - hosts:
+ - {{ .Values.host }}
+ secretName: {{ .Values.host }}
+ rules:
+ - host: {{ .Values.host }}
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: {{ .Values.appName }}-{{ .Values.deployEnv }}
+ port:
+ name: http
+{{- end }}
\ No newline at end of file
diff --git a/helm/app/templates/ns-resource-quota.yaml b/helm/app/templates/ns-resource-quota.yaml
new file mode 100644
index 0000000..631e73d
--- /dev/null
+++ b/helm/app/templates/ns-resource-quota.yaml
@@ -0,0 +1,27 @@
+apiVersion: v1
+kind: ResourceQuota
+metadata:
+ name: resource-quota
+ namespace: {{ .Release.Namespace }}
+spec:
+ hard:
+ {{- if eq .Values.deployEnv "staging" }}
+ pods: "2"
+ services: "1"
+ requests.memory: "256Mi"
+ limits.cpu: "1"
+ limits.memory: "16Gi"
+ persistentvolumeclaims: "0"
+ {{- end }}
+ {{- if eq .Values.deployEnv "canary" }}
+ pods: "10"
+ limits.cpu: "4"
+ limits.memory: "8Gi"
+ persistentvolumeclaims: "0"
+ {{- end }}
+ {{- if eq .Values.deployEnv "production" }}
+ pods: "100"
+ limits.cpu: "8"
+ limits.memory: "16Gi"
+ persistentvolumeclaims: "0"
+ {{- end }}
diff --git a/helm/app/templates/service-monitor.yaml b/helm/app/templates/service-monitor.yaml
new file mode 100644
index 0000000..30b47f6
--- /dev/null
+++ b/helm/app/templates/service-monitor.yaml
@@ -0,0 +1,19 @@
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+ name: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ namespace: {{ .Release.Namespace }}
+ labels:
+ release: prometheus-stack
+spec:
+ selector:
+ matchLabels:
+ app: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ endpoints:
+ - port: http
+ interval: 30s
+ path: /metrics
+ - port: https
+ interval: 30s
+ path: /metrics
+
diff --git a/helm/app/templates/service.yaml b/helm/app/templates/service.yaml
new file mode 100644
index 0000000..6a6d990
--- /dev/null
+++ b/helm/app/templates/service.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ namespace: {{ .Release.Namespace }}
+ labels:
+ app: {{ .Values.appName }}-{{ .Values.deployEnv }}
+ release: prometheus-stack
+spec:
+ type: ClusterIP
+ selector:
+ app: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ ports:
+ - name: http
+ protocol: TCP
+ port: 80
+ targetPort: {{ .Values.containerPort }}
\ No newline at end of file
diff --git a/helm/app/templates/tls-cert.yaml b/helm/app/templates/tls-cert.yaml
new file mode 100644
index 0000000..b0aff17
--- /dev/null
+++ b/helm/app/templates/tls-cert.yaml
@@ -0,0 +1,15 @@
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ name: {{ .Values.appName }}-{{ .Values.deployEnv}}
+ namespace: {{ .Release.Namespace }}
+spec:
+ secretName: {{ .Values.host }}
+ issuerRef:
+ kind: ClusterIssuer
+ name: {{ .Values.tlsIssuer }}
+ commonName: {{ .Values.host }}
+ duration: 2160h
+ renewBefore: 360h
+ dnsNames:
+ - {{ .Values.host }}
diff --git a/helm/app/values-production.yaml b/helm/app/values-production.yaml
new file mode 100644
index 0000000..dd66045
--- /dev/null
+++ b/helm/app/values-production.yaml
@@ -0,0 +1,9 @@
+deployEnv: production
+defaultReplicaCount: 6
+imageTag: "production"
+
+minReplicas: 1
+maxReplicas: 1
+
+memoryLimit: 120Mi
+memoryRequest: 100Mi
diff --git a/helm/app/values-staging.yaml b/helm/app/values-staging.yaml
new file mode 100644
index 0000000..4c7c9ad
--- /dev/null
+++ b/helm/app/values-staging.yaml
@@ -0,0 +1,9 @@
+deployEnv: staging
+defaultReplicaCount: 6
+imageTag: "staging"
+
+minReplicas: 1
+maxReplicas: 1
+
+memoryLimit: 120Mi
+memoryRequest: 100Mi
diff --git a/helm/app/values.yaml b/helm/app/values.yaml
new file mode 100644
index 0000000..64cd594
--- /dev/null
+++ b/helm/app/values.yaml
@@ -0,0 +1,41 @@
+# app
+appVersion: "0.1"
+
+# limits & requests
+cpuLimit: "500m"
+memoryLimit: "128Mi"
+cpuRequest: "500m"
+memoryRequest: "64Mi"
+
+# replicas
+minReplicas: 2
+maxReplicas: 40
+
+# docker
+containerPort: 3000
+nodePort: 80
+
+# from github deploy
+imageRepo: ""
+imageTag: ""
+host: ""
+appName: ""
+ghcrSecret: ""
+
+tlsCert: ""
+tlsKey: ""
+
+
+# do not change
+tlsIssuer: "letsencrypt"
+certIssuingMode: false
+
+# http
+publicService: true
+sslRedirect: true
+
+
+secrets:
+ publicProxyKey: ""
+ publicMixPanelToken: ""
+ secrets.publicProxyKey: ""