diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d6852a3..f468a5a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -38,7 +38,7 @@ jobs: docker: needs: [ build, run-tests ] - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' + if: github.ref_name == 'main' || github.ref_name == 'develop' runs-on: ubuntu-latest permissions: contents: read @@ -90,3 +90,76 @@ jobs: ${{ env.IMAGE_SHA }} cache-from: type=gha cache-to: type=gha,mode=max + + deploy: + needs: [docker] + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref_name == 'main') + environment: + name: ${{ github.ref_name == 'main' && 'production' || 'dev' }} + url: https://${{ vars.KUBE_DOMAIN }} + env: + # Kubernetes Specific + KUBE_NAMESPACE: ${{ vars.KUBE_NAMESPACE }} + KUBE_API_DOMAIN: ${{ vars.KUBE_API_DOMAIN }} + WORKER_NODE_LABEL: ${{ vars.WORKER_NODE_LABEL }} + # Application Specific + FRONTEND_PATH: ${{ vars.FRONTEND_PATH }} + + steps: + - uses: actions/checkout@v4 + - uses: azure/setup-kubectl@v4 + + - name: Set Up Kubeconfig + uses: azure/k8s-set-context@v4 + with: + kubeconfig: ${{ secrets.PORTFOLIO_KUBECONFIG }} + + - name: Prepare Image Tag + run: | + OWNER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]') + REPO=$(echo "${GITHUB_REPOSITORY#*/}" | tr '[:upper:]' '[:lower:]') + SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) + + IMAGE_BASE="ghcr.io/${OWNER}/${REPO}" + IMAGE_TAG="sha-${SHORT_SHA}" + + echo "IMAGE_BASE=${IMAGE_BASE}" >> $GITHUB_ENV + echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_ENV + + - name: Import SOPS GPG Key + run: | + echo "${{ secrets.PORTFOLIO_GPG_PRIVATE_KEY }}" | gpg --import + + - name: Install SOPS + run: | + curl -L https://github.com/mozilla/sops/releases/download/v3.9.1/sops-v3.9.1.linux.amd64 -o /usr/local/bin/sops + chmod +x /usr/local/bin/sops + + - name: Decrypt SOPS Secrets Test + run: | + cd .k8s + sops -d secrets.enc.yml secrets.yml + + - name: Apply Kubernetes Manifests - Configuration + run: cat .k8s/config.template.yml | envsubst | kubectl apply -f - + + - name: Apply Kubernetes Manifests - Secrets + run: cat .k8s/secrets.yml | envsubst | kubectl apply -f - + + - name: Apply Kubernetes Manifests - Postgres Cluster + run: cat .k8s/postgres-cluster.template.yml | envsubst | kubectl apply -f - + + - name: Apply Kubernetes Manifests - Redis Cluster + run: cat .k8s/redis.template.yml | envsubst | kubectl apply -f - + + - name: Apply Kubernetes Manifests - Deployment + run: | + cat .k8s/deployment.template.yml | envsubst | kubectl apply -f - + cat .k8s/deployment.yaml | envsubst | kubectl rollout status deployment/frontend-deployment -n ${KUBE_NAMESPACE} --timeout=120s + + - name: Apply Kubernetes Manifests - Service + run: cat .k8s/service.template.yml | envsubst | kubectl apply -f - + + - name: Apply Kubernetes Manifests - Ingress + run: cat .k8s/ingress.template.yml | envsubst | kubectl apply -f - diff --git a/.gitignore b/.gitignore index 8efdfcd..953f756 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,15 @@ dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties # https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar \ No newline at end of file +.mvn/wrapper/maven-wrapper.jar + +### Certs ### +*.pem + +### Secrets and Envs ### +.env* +*.secret + +.k8s/*.yml +!.k8s/*.template.yml +!.k8s/*.enc.yml \ No newline at end of file diff --git a/.k8s/.sops.yaml b/.k8s/.sops.yaml new file mode 100644 index 0000000..44705ed --- /dev/null +++ b/.k8s/.sops.yaml @@ -0,0 +1,5 @@ +creation_rules: + - path_regex: ^secrets(\.enc)?\.yml$ + encrypted_regex: '^(data|stringData)$' + pgp: >- + 8C8D94A7639C87559B0F2F64B7E1F62F69798EB6 \ No newline at end of file diff --git a/.k8s/config.template.yml b/.k8s/config.template.yml new file mode 100644 index 0000000..1dec8f3 --- /dev/null +++ b/.k8s/config.template.yml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: backend-config + namespace: ${KUBE_NAMESPACE} +data: + FRONTEND_PATH: ${FRONTEND_PATH} + STORAGE_SERVICE_INTERNAL_URL: storage-service + STORAGE_SERVICE_PORT: "8000" \ No newline at end of file diff --git a/.k8s/deployment.template.yml b/.k8s/deployment.template.yml new file mode 100644 index 0000000..fccaba7 --- /dev/null +++ b/.k8s/deployment.template.yml @@ -0,0 +1,99 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend-deployment + namespace: ${KUBE_NAMESPACE} +spec: + replicas: 1 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + spec: + nodeSelector: + ${WORKER_NODE_LABEL} + imagePullSecrets: + - name: ghcr-secret + containers: + - name: backend + image: ${IMAGE_BASE}:${IMAGE_TAG} + imagePullPolicy: Always + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "256Mi" + cpu: "1000m" + ports: + - containerPort: 8070 + readinessProbe: + httpGet: + path: /health + port: 8070 + initialDelaySeconds: 60 + livenessProbe: + httpGet: + path: /health + port: 8070 + initialDelaySeconds: 60 + envFrom: + - configMapRef: + name: backend-config + - secretRef: + name: backend-secret + env: + - name: PORT + value: "8070" + + - name: REDIS_URL + value: backend-redis-service + + - name: REDIS_PORT + value: "6379" + + - name: POSTGRES_URL + valueFrom: + secretKeyRef: + name: backend-postgres-cluster-app + key: host + + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: backend-postgres-cluster-app + key: dbname + + - name: DATABASE_URL + value: "postgresql://$(POSTGRES_URL):5432/$(POSTGRES_DB)" + + - name: DATABASE_USERNAME + valueFrom: + secretKeyRef: + name: backend-postgres-cluster-app + key: user + + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: backend-postgres-cluster-app + key: password + + - name: STORAGE_SERVICE_URL + valueFrom: + configMapKeyRef: + name: backend-config + key: STORAGE_SERVICE_INTERNAL_URL + + - name: STORAGE_SERVICE_PORT + valueFrom: + configMapKeyRef: + name: backend-config + key: STORAGE_SERVICE_PORT + + - name: STORAGE_SERVICE_PATH + value: "http://$(STORAGE_SERVICE_URL):$(STORAGE_SERVICE_PORT)" + diff --git a/.k8s/ingress.template.yml b/.k8s/ingress.template.yml new file mode 100644 index 0000000..517ee9e --- /dev/null +++ b/.k8s/ingress.template.yml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: backend-ingress + namespace: ${KUBE_NAMESPACE} + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + tls: + - hosts: + - ${KUBE_API_DOMAIN} + secretName: letsencrypt-cluster-certificate-tls + rules: + - host: ${KUBE_API_DOMAIN} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: backend-service + port: + number: 8070 \ No newline at end of file diff --git a/.k8s/postgres-cluster.template.yml b/.k8s/postgres-cluster.template.yml new file mode 100644 index 0000000..86366ac --- /dev/null +++ b/.k8s/postgres-cluster.template.yml @@ -0,0 +1,22 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: backend-postgres-cluster + namespace: ${KUBE_NAMESPACE} +spec: + instances: 3 + primaryUpdateStrategy: unsupervised + imageName: ghcr.io/cloudnative-pg/postgresql:14.10-18 + + storage: + size: 5Gi + + resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "512Mi" + cpu: "500m" + + affinity: {} diff --git a/.k8s/redis.template.yml b/.k8s/redis.template.yml new file mode 100644 index 0000000..f4ce8ff --- /dev/null +++ b/.k8s/redis.template.yml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: ${KUBE_NAMESPACE} + name: backend-redis-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: backend-redis + template: + metadata: + labels: + app: backend-redis + spec: + containers: + - name: redis + image: valkey/valkey:8.0.6-alpine + imagePullPolicy: "IfNotPresent" + resources: + requests: + memory: "256Mi" + cpu: "75m" + limits: + memory: "256Mi" + cpu: "256m" + ports: + - containerPort: 6379 + env: + - name: VALKEY_PASSWORD + valueFrom: + secretKeyRef: + name: backend-secret + key: REDIS_PASSWORD + +--- +apiVersion: v1 +kind: Service +metadata: + namespace: ${KUBE_NAMESPACE} + name: backend-redis-service +spec: + selector: + app: backend-redis + ports: + - port: 6379 + type: ClusterIP \ No newline at end of file diff --git a/.k8s/secrets.enc.yml b/.k8s/secrets.enc.yml new file mode 100644 index 0000000..fa34e89 --- /dev/null +++ b/.k8s/secrets.enc.yml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: Secret +metadata: + name: backend-secret + namespace: ${KUBE_NAMESPACE} +data: + ACCESS_TOKEN_DURATION: ENC[AES256_GCM,data:UID13ASjuH9hrFfx,iv:D9cTYN60Q8KL1ZdEMPAQ/RP+uqsMbA95cOqngMtxyF8=,tag:Q4cCzqzzRiuf2wyTW86kXg==,type:str] + DEFAULT_USER_EMAIL: ENC[AES256_GCM,data:DJ/3gHT47OidWhevnCHBdzwrFhmCDQeYzjL3siAi+e/Op3Ge,iv:AqUg21UnGl5tJP6TIewcL9wdpAJBGS7Af6olHRm+auU=,tag:+PX5bjMslR0FVqd9fSXnBg==,type:str] + DEFAULT_USER_FULLNAME: ENC[AES256_GCM,data:WGQEQ1/5NhDYudrW8nBnQUXmcC0=,iv:BvysE7IGiXzFUqse0AtzrW1eO4Y52TgD1RX1hxXjqnQ=,tag:nqHCPt7Aa53OTBOTkQGSDA==,type:str] + DEFAULT_USER_PASSWORD: ENC[AES256_GCM,data:N+q5z+cYXPPf7peJdGKx+A6ou5A=,iv:kmOdcNStwIJOnO1RQ+KOcpW3wxrzR13xtdBwSTvC834=,tag:esM/CE+Y4oCFKs7w573qLw==,type:str] + DEFAULT_USER_USERNAME: ENC[AES256_GCM,data:IXWTfhIKBgs+/zUCRBN5SHcn3Ig=,iv:0RegOxvd7WzoSW8u6sjJLdn6kPoSJzstj+OyStS5zNA=,tag:Lv5XR//V9FD0Aa1kLEmOUA==,type:str] + GITHUB_CLIENT_ID: ENC[AES256_GCM,data:Wg8YoSkLie3HgAMUi2Y25GV/0Di+gs0i6KznVg==,iv:4tsj0GvTa21O/CUZ/54B0VnX9Ebi0pX8Y0cBlwQ9uGQ=,tag:uS9WRW9SEeP+PXERUAUDqg==,type:str] + GITHUB_CLIENT_SECRET: ENC[AES256_GCM,data:xA1jb1YcQzLYOtWQWnY1t6DP701Q4P7JXKVCA9m2t9npettwy4FR2Gt1WqrYkyuMrp8CYYdVnlY=,iv:ThCZid05MBY/6W4/H9Xi85bsT6AzOpdhGVWBEc4zNYE=,tag:hP3CO+ElBug0n3f4P708BA==,type:str] + GITHUB_REDIRECT_URL: ENC[AES256_GCM,data:YI1CWaPAtTilqq7ZAmKKGtn9fQs4/urH874s2Nc51Rnqc5/xjYbTpbd/2MMg0vmyrFK8Twcg2RtnfLFa,iv:viH3P9FPCUsd9KoEwVdNTYNl+v24gcGCnVMJzB4AvxM=,tag:b7oSX1Vn5SiFiMhWBsLEDQ==,type:str] + GOOGLE_CLIENT_ID: ENC[AES256_GCM,data:GaKQG4al/kl1PyVGLyQ3gKI0y93IvyPWoxlWd1Iu8YUkcP6vQ80S4fQ1LCGGS8BA0f83SnwTZT37vrFz7h6V3+po6a7CjXE/gSRsu/HIuOG+O4gyAw07KWHorLpcD8pL,iv:RGskspn8CEq3rwinaOe4T/KajAUMHVBJLnGOgBT8L78=,tag:YuSQgDuowd8LLhf5Rdcj2w==,type:str] + GOOGLE_CLIENT_SECRET: ENC[AES256_GCM,data:6sQAluZFxc6mEOZffPJV5Al64APVHJyHHZzneofu17nflE6eslzH0SfL1Uo/ngu6,iv:Za9MHjCSWsGCik9OgJXYmLfFLmcVPQ7V4bLeeflVVOw=,tag:cZfo/pdl42n+eiKPrwFGMg==,type:str] + GOOGLE_REDIRECT_URL: ENC[AES256_GCM,data:3smhd+Hp3/q2uRVHoASTnm7j1F90TAaauCVuWsCUG1aKb++eXkdLEqP5j77szzFmWmBpgeW3hjP7dipL,iv:CPUxKja1YLSnKQpVeEHGkxxmQg1qJnQxRJ48Mw/k75Q=,tag:YJ/c5N1QLLRbCXsi2D2X4g==,type:str] + REDIS_PASSWORD: ENC[AES256_GCM,data:9kexm1c0M7A=,iv:MUSnfOdUbbsJReQtuzyVrJcsc3NqptE96w3Kh1jbqjo=,tag:SvSHTW8Sm3UMIKismYubng==,type:str] + REFRESH_TOKEN_DURATION: ENC[AES256_GCM,data:MCz6j6RI9hKcD8Zzqvt1Iw==,iv:nsw3cTtVJo+/1foTp/M78ByF5p8K6uw720GY8sAJypU=,tag:Usq/7VvLn7MGpoYbZgXAtg==,type:str] + TOKEN_SECRET: ENC[AES256_GCM,data:ESHASgGJZYspUVua,iv:52aZ1Ds984u8rZR48lNjPhBM07vnWGTpEOE6c7cItUU=,tag:Xc/6w/PqDDspl3r/krgdTg==,type:str] +sops: + lastmodified: "2025-11-07T23:43:53Z" + mac: ENC[AES256_GCM,data:38dHNBEQuExxNeouXp7LotuV5aYTUcrhovYEB0v9SPK8q5ViwXSiU730BTyqF3iya6AuugT3xuFZZG0BFxAOv57FXpiX32pVOb9OQEf/vo1I6+lKjYCg0NiP6qvtpH59z4m35SG6zUXICf5zJucOr+n+UeRMsLO4tbjg9s5A+DU=,iv:vlXW79iMy1qBY+hzqkX2McB3746oJZI/6vqeSi9HNNw=,tag:kGhNHSh/aAXDv3Qz3k3gOg==,type:str] + pgp: + - created_at: "2025-11-07T23:43:53Z" + enc: |- + -----BEGIN PGP MESSAGE----- + + hQIMAwzdivR1H/BQAQ//ffQ2BHDvlwBU7Ck4BfM3sN5XYFCMVY1Nd1WxNB+Mso7+ + Rx4WD1UfNufJhzXIDwGICyNghrfy16UEZZxq2uM/vE/PeQjOwTmkBAB+izzOP8cM + 219UV0RG+qh5/n8v5szcaOvpwI2rU5OvRJs/M7N2563rTy+GtXxM3F8zdMFX6ilQ + PNwo0Ah2ag45PEuu/RH9BQ7egdPx4niKESbBX8Ixp95BndcIUqqDmg7mq7oIeg5K + 5nu6D0AAf41D5vqTDNpT5P+KiY5adStW22vwTehfBklVXK65scDbEp7BQjMlfWMX + a2DVv0axxOvqPuXaABoVtbHFH9UMnUCe3rcq1An4id/DHU7WrRt0SkHcvHPYxbsO + IWxtl8Au9NH7hjpYO5uTP1HfUYw/MkZ0kC27ZoIg/QxCM1HkYQJU85J9VrK+GhJT + /HbXGb9PE6XIH/Nh0PNJ7cHdpM0tXSE/AyTjdyN5DSnub6sT28vORbsHQkV4oykq + k03gDtT50qT5t0x9oYydJPBCcTd8vhNfNqxYA0ppuGJo3iv+81LDPRSTvGRSU8UJ + bbX56ryJMO/942oBp4u5dsL2q7u0/5cPBJ7UN8v9dJkSAuSHTVNYhhFzKnLsGbr1 + ZpBzfg1Mp0zySv2CZpY5xDu1SFs+kaDVPRrOlPz8jFsyt1WNjRNun0521Gfc/CbS + XgEpMev76yzSjkbNSwRS/2U13w7fh76F2MO6ftOwcWS89Do+drdyWJq15ou0LJZE + cqV6ojX5hhtFsH2YTS/+kGDTGf0mNEHtglSzwUT4M0bHBO2vld7p7SNkzMKrwkE= + =vkjm + -----END PGP MESSAGE----- + fp: 8C8D94A7639C87559B0F2F64B7E1F62F69798EB6 + encrypted_regex: ^(data|stringData)$ + version: 3.11.0 diff --git a/.k8s/service.template.yml b/.k8s/service.template.yml new file mode 100644 index 0000000..acd188e --- /dev/null +++ b/.k8s/service.template.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + namespace: ${KUBE_NAMESPACE} + name: backend-service +spec: + selector: + app: backend + ports: + - port: 8070 + protocol: TCP + targetPort: 8070 + type: ClusterIP \ No newline at end of file