From 3fc022cca9c35ebad6bfd2143749bcd5b3254203 Mon Sep 17 00:00:00 2001 From: vivet Date: Sat, 31 Aug 2024 16:51:31 +0200 Subject: [PATCH] Added shallow Web template --- Web/.dockerignore | 7 + Web/.github/config/slack.yml | 18 +++ Web/.github/workflows/build-and-deploy.yml | 145 +++++++++++++++++++++ Web/.gitignore | 37 ++++++ Web/.kubernetes/autoscaler.yaml | 25 ++++ Web/.kubernetes/certificate.yaml | 19 +++ Web/.kubernetes/configmap.yaml | 7 + Web/.kubernetes/deployment.yaml | 82 ++++++++++++ Web/.kubernetes/ingress.yaml | 55 ++++++++ Web/.kubernetes/service.yaml | 14 ++ Web/Dockerfile | 19 +++ Web/README.md | 2 + 12 files changed, 430 insertions(+) create mode 100644 Web/.dockerignore create mode 100644 Web/.github/config/slack.yml create mode 100644 Web/.github/workflows/build-and-deploy.yml create mode 100644 Web/.gitignore create mode 100644 Web/.kubernetes/autoscaler.yaml create mode 100644 Web/.kubernetes/certificate.yaml create mode 100644 Web/.kubernetes/configmap.yaml create mode 100644 Web/.kubernetes/deployment.yaml create mode 100644 Web/.kubernetes/ingress.yaml create mode 100644 Web/.kubernetes/service.yaml create mode 100644 Web/Dockerfile create mode 100644 Web/README.md diff --git a/Web/.dockerignore b/Web/.dockerignore new file mode 100644 index 0000000..72e9aa4 --- /dev/null +++ b/Web/.dockerignore @@ -0,0 +1,7 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.next +.git \ No newline at end of file diff --git a/Web/.github/config/slack.yml b/Web/.github/config/slack.yml new file mode 100644 index 0000000..3592aff --- /dev/null +++ b/Web/.github/config/slack.yml @@ -0,0 +1,18 @@ +username: GitHub Actions +icon_url: https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png + +pretext: "{{icon jobStatus}} *<{{repositoryUrl}}|{{repositoryName}}>* <{{workflowRunUrl}}|`#{{runNumber}}`> triggered via {{eventName}} by ** for branch <{{refUrl}}|`{{ref}}`>." + +text: | + {{#if payload.commits}} + *Commits* + {{#each payload.commits}} + <{{this.url}}|`{{truncate this.id 8}}`> - {{this.message}} + {{/each}} + {{/if}} + +footer: >- + <{{repositoryUrl}}|{{repositoryName}}> #{{runNumber}} + +fallback: |- + [GitHub] {{workflow}} #{{runNumber}} {{jobName}} is {{jobStatus}} diff --git a/Web/.github/workflows/build-and-deploy.yml b/Web/.github/workflows/build-and-deploy.yml new file mode 100644 index 0000000..16683b1 --- /dev/null +++ b/Web/.github/workflows/build-and-deploy.yml @@ -0,0 +1,145 @@ +name: Build and Deploy +on: + push +env: + NODE_JS_VERSION: 20.16.0 + APP_NAME: nano.template.web + IMAGE_NAME: nano.template.web + SERVICE_NAME: nano-template-web + VERSION: '${{ vars.VERSION }}.${{ github.run_number }}.${{ github.run_attempt }}' + AZURE_GROUP: Kubernetes + AZURE_LOCATION: North Europe + KUBERNETES_NAMESPACE: default + KUBERNETES_REPLICA_COUNT: ${{ github.ref == 'refs/heads/master' && 2 || 1 }} + KUBERNETES_REPLICA_COUNT_MAX: ${{ github.ref == 'refs/heads/master' && 2 || 1 }} + KUBERNETES_REPLICA_HISTORY_COUNT: 0 + KUBERNETES_MEMORY_REQUEST: 372Mi + KUBERNETES_MEMORY_LIMIT: 1152Mi + KUBERNETES_MEMORY_SCALING: 180 + KUBERNETES_CPU_REQUEST: 150m + KUBERNETES_CPU_LIMIT: 450m + KUBERNETES_CPU_SCALING: 180 + CERTIFICATE_ISSUER: letsencrypt-prod + ENVIRONMENT: ${{ github.ref == 'refs/heads/master' && 'production' || 'staging' }} + ENVIRONMENT_RUNTIME_VAR_FILE: ${{ github.ref == 'refs/heads/master' && '.env.live' || '.env.staging' }} +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + concurrency: + group: ${{ github.repository }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + + - name: Set Environmental Variables + id: set-variables + shell: pwsh + run: | + echo "CERTIFICATE_ORGANIZATION=${{ vars.CERTIFICATE_ORGANIZATION }}" >> $env:GITHUB_ENV; + + if ('${{ github.ref }}' -eq 'refs/heads/master') { + echo "AZURE_SUBSCRIPTION_ID=${{ secrets.LIVE_AZURE_SUBSCRIPTION_ID }}" >> $env:GITHUB_ENV; + echo "AZURE_ACR_HOST=${{ secrets.LIVE_AZURE_ACR_HOST }}" >> $env:GITHUB_ENV; + echo "AZURE_ACR_USERNAME=${{ secrets.LIVE_AZURE_ACR_USERNAME }}" >> $env:GITHUB_ENV; + echo "AZURE_ACR_PASSWORD=${{ secrets.LIVE_AZURE_ACR_PASSWORD }}" >> $env:GITHUB_ENV; + echo "KUBERNETES_CLUSTER=${{ vars.PRODUCTION_KUBERNETES_CLUSTER }}" >> $env:GITHUB_ENV; + echo "CERTIFICATE_HOST=${{ vars.HOST_WEB_SUBDOMAIN }}.${{ vars.PRODUCTION_HOST }}" >> $env:GITHUB_ENV; + echo "API_URL=${{ vars.HOST_API_SUBDOMAIN }}.${{ vars.PRODUCTION_HOST }}" >> $env:GITHUB_ENV; + echo "NONCE_TOKEN=${{ vars.PRODUCTION_WEB_NONCE_TOKEN }}" >> $env:GITHUB_ENV; + } + elseif ('${{ github.ref }}' -eq 'refs/heads/staging') { + echo "AZURE_SUBSCRIPTION_ID=${{ secrets.STAGING_AZURE_SUBSCRIPTION_ID }}" >> $env:GITHUB_ENV; + echo "AZURE_ACR_HOST=${{ secrets.STAGING_AZURE_ACR_HOST }}" >> $env:GITHUB_ENV; + echo "AZURE_ACR_USERNAME=${{ secrets.STAGING_AZURE_ACR_USERNAME }}" >> $env:GITHUB_ENV; + echo "AZURE_ACR_PASSWORD=${{ secrets.STAGING_AZURE_ACR_PASSWORD }}" >> $env:GITHUB_ENV; + echo "KUBERNETES_CLUSTER=${{ vars.STAGING_KUBERNETES_CLUSTER }}" >> $env:GITHUB_ENV; + echo "CERTIFICATE_HOST=${{ vars.HOST_WEB_SUBDOMAIN }}.${{ vars.STAGING_HOST }}" >> $env:GITHUB_ENV; + echo "API_URL=${{ vars.HOST_API_SUBDOMAIN }}.${{ vars.STAGING_HOST }}" >> $env:GITHUB_ENV; + echo "NONCE_TOKEN=${{ vars.STAGING_WEB_NONCE_TOKEN }}" >> $env:GITHUB_ENV; + } + + - name: Build and Deploy + shell: pwsh + run: | + sudo az login --service-principal -u "${{ secrets.AZURE_CLIENT_ID }}" -p "${{ secrets.AZURE_CLIENT_SECRET }}" --tenant "${{ secrets.AZURE_TENANT_ID }}" -o none; + sudo az account set -s $env:AZURE_SUBSCRIPTION_ID -o none; + sudo az aks get-credentials -g "${{ vars.AZURE_KUBERNETES_RESOURCE_GROUP }}" -n $env:KUBERNETES_CLUSTER --overwrite -o none; + + Add-Content -Path $env:ENVIRONMENT_RUNTIME_VAR_FILE -Value `n; + Add-Content -Path $env:ENVIRONMENT_RUNTIME_VAR_FILE -Value NEXT_PUBLIC_VERSION=$env:VERSION; + Add-Content -Path $env:ENVIRONMENT_RUNTIME_VAR_FILE -Value NEXT_PUBLIC_API_URL=$env:API_URL; + Add-Content -Path $env:ENVIRONMENT_RUNTIME_VAR_FILE -Value NEXT_PUBLIC_NONCE_TOKEN=$env:NONCE_TOKEN; + + sudo docker build ` + -t $env:AZURE_ACR_HOST"/"$env:IMAGE_NAME":latest" ` + -t $env:AZURE_ACR_HOST"/"$env:IMAGE_NAME":"$env:VERSION ` + --build-arg ENVIRONMENT=$env:ENVIRONMENT ` + --build-arg NODE_JS_VERSION=$env:NODE_JS_VERSION ` + ./; + + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + sudo docker login -u="$env:AZURE_ACR_USERNAME" -p="$env:AZURE_ACR_PASSWORD" $env:AZURE_ACR_HOST; + sudo docker push $env:AZURE_ACR_HOST"/"$env:IMAGE_NAME":latest"; + sudo docker push $env:AZURE_ACR_HOST"/"$env:IMAGE_NAME":"$env:VERSION; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + Get-Content .kubernetes/certificate.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/certificate.tmp.yaml; + sudo kubectl apply -f .kubernetes/certificate.tmp.yaml; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + Get-Content .kubernetes/service.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/service.tmp.yaml; + sudo kubectl apply -f .kubernetes/service.tmp.yaml; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + Get-Content .kubernetes/configmap.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/configmap.tmp.yaml; + sudo kubectl apply -f .kubernetes/configmap.tmp.yaml; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + Get-Content .kubernetes/deployment.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/deployment.tmp.yaml; + sudo kubectl apply -f .kubernetes/deployment.tmp.yaml; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + Get-Content .kubernetes/autoscaler.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/autoscaler.tmp.yaml; + sudo kubectl apply -f .kubernetes/autoscaler.tmp.yaml; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + Get-Content .kubernetes/ingress.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/ingress.tmp.yaml; + sudo kubectl apply -f .kubernetes/ingress.tmp.yaml; + if ($LastExitCode -ne 0) + { + throw "error"; + }; + + - name: Slack Notification + if: always() + uses: act10ns/slack@v2 + with: + webhook-url: ${{ secrets.SLACK_WEBHOOK }} + config: .github/config/slack.yml + status: ${{ job.status }} + channel: ${{ vars.SLACK_CHANNEL }} diff --git a/Web/.gitignore b/Web/.gitignore new file mode 100644 index 0000000..8585166 --- /dev/null +++ b/Web/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts +.env diff --git a/Web/.kubernetes/autoscaler.yaml b/Web/.kubernetes/autoscaler.yaml new file mode 100644 index 0000000..95add8a --- /dev/null +++ b/Web/.kubernetes/autoscaler.yaml @@ -0,0 +1,25 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: %SERVICE_NAME%-hpa + namespace: %KUBERNETES_NAMESPACE% +spec: + minReplicas: %KUBERNETES_REPLICA_COUNT% + maxReplicas: %KUBERNETES_REPLICA_COUNT_MAX% + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: %SERVICE_NAME% + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: %KUBERNETES_CPU_SCALING% + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: %KUBERNETES_MEMORY_SCALING% diff --git a/Web/.kubernetes/certificate.yaml b/Web/.kubernetes/certificate.yaml new file mode 100644 index 0000000..a37bdad --- /dev/null +++ b/Web/.kubernetes/certificate.yaml @@ -0,0 +1,19 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: %SERVICE_NAME%-nginx-tls + namespace: %KUBERNETES_NAMESPACE% +spec: + secretName: %CERTIFICATE_HOST%-tls + duration: 2160h + renewBefore: 720h + subject: + organizations: + - %CERTIFICATE_ORGANIZATION% + dnsNames: + - %CERTIFICATE_HOST% + privateKey: + rotationPolicy: Always + issuerRef: + name: %CERTIFICATE_ISSUER% + kind: ClusterIssuer diff --git a/Web/.kubernetes/configmap.yaml b/Web/.kubernetes/configmap.yaml new file mode 100644 index 0000000..7a9a2c9 --- /dev/null +++ b/Web/.kubernetes/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: %SERVICE_NAME%-config + namespace: %KUBERNETES_NAMESPACE% +data: + App__Version: %VERSION% diff --git a/Web/.kubernetes/deployment.yaml b/Web/.kubernetes/deployment.yaml new file mode 100644 index 0000000..10a986e --- /dev/null +++ b/Web/.kubernetes/deployment.yaml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: %SERVICE_NAME% + namespace: %KUBERNETES_NAMESPACE% + labels: + app: %SERVICE_NAME% +spec: + replicas: %KUBERNETES_REPLICA_COUNT% + revisionHistoryLimit: %KUBERNETES_REPLICA_HISTORY_COUNT% + selector: + matchLabels: + app: %SERVICE_NAME% + template: + metadata: + labels: + app: %SERVICE_NAME% + spec: + automountServiceAccountToken: false + securityContext: + runAsUser: 1000 + runAsGroup: 2000 + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: %SERVICE_NAME% + automountServiceAccountToken: false + containers: + - name: %SERVICE_NAME% + image: %AZURE_ACR_HOST%/%IMAGE_NAME%:%VERSION% + ports: + - containerPort: 8080 + - containerPort: 4443 + imagePullPolicy: Always + envFrom: + - configMapRef: + name: %SERVICE_NAME%-config + resources: + requests: + memory: %KUBERNETES_MEMORY_REQUEST% + cpu: %KUBERNETES_CPU_REQUEST% + limits: + memory: %KUBERNETES_MEMORY_LIMIT% + cpu: %KUBERNETES_CPU_LIMIT% + securityContext: + privileged: false + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 2000 + capabilities: + drop: + - ALL + livenessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + periodSeconds: 20 + initialDelaySeconds: 25 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + periodSeconds: 5 + initialDelaySeconds: 20 + volumeMounts: + - name: tmp + mountPath: /tmp + - name: npm-logs + mountPath: /home/node/.npm/_logs + volumes: + - name: tmp + emptyDir: {} + - name: npm-logs + emptyDir: {} + diff --git a/Web/.kubernetes/ingress.yaml b/Web/.kubernetes/ingress.yaml new file mode 100644 index 0000000..f131be4 --- /dev/null +++ b/Web/.kubernetes/ingress.yaml @@ -0,0 +1,55 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-%SERVICE_NAME% + namespace: %KUBERNETES_NAMESPACE% + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/ingress.allow-http: "false" + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/use-forwarded-headers: "true" + nginx.ingress.kubernetes.io/proxy-send-timeout: "300" + nginx.ingress.kubernetes.io/proxy-read-timeout: "300" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "300" + nginx.ingress.kubernetes.io/proxy-body-size: "128m" + nginx.ingress.kubernetes.io/proxy-max-temp-file-size: "128m" + nginx.ingress.kubernetes.io/session-cookie-samesite: "Strict" + nginx.ingress.kubernetes.io/enable-substitution: "true" + nginx.ingress.kubernetes.io/sub-filter-recursive: "on" + nginx.ingress.kubernetes.io/sub-filter-order: last + nginx.ingress.kubernetes.io/sub-filter-exclude-content-type: "text/event-stream" + nginx.ingress.kubernetes.io/sub-filter-ignore-content-type: "text/html" + nginx.ingress.kubernetes.io/custom-http-errors: "400" + nginx.ingress.kubernetes.io/configuration-snippet: | + more_set_headers "X-Frame-Options: Deny" + more_set_headers "Referrer-Policy: same-origin" + more_set_headers "X-XSS-Protection: 1; mode=block" + more_set_headers "X-Content-Type-Options: nosniff" + more_set_headers "X-Robots-Tag: noindex, nofollow" + more_set_headers "Cross-Origin-Embedder-Policy: require-corp" + more_set_headers "Cross-Origin-Opener-Policy: same-origin" + more_set_headers "Cross-Origin-Resource-Policy: same-origin" + more_set_headers "Content-Security-Policy: default-src 'none'; manifest-src 'self'; img-src 'self' blob: data:; object-src 'self'; script-src 'self' 'nonce-${request_id}'; style-src 'self' https://*.%CERTIFICATE_HOST% 'nonce-${request_id}'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; font-src 'self' data:; connect-src https://*.%CERTIFICATE_HOST% blob: https://*.%CERTIFICATE_HOST%"; + more_set_headers "Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), gamepad=(), geolocation=(), gyroscope=(), layout-animations=(), legacy-image-formats=(), magnetometer=(), microphone=(), midi=(), navigation-override=(self), oversized-images=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), speaker-selection=(), sync-xhr=(), unoptimized-images=(), unsized-media=(), usb=(), screen-wake-lock=(), web-share=(), xr-spatial-tracking=()"; + proxy_set_header Accept-Encoding ""; + sub_filter_once off; + sub_filter '%NONCE_TOKEN%' $request_id; + sub_filter '(]*>)(.*?)%NONCE_TOKEN%(.*?<\/body>)' '$1$2"$request_id"$3'; + nginx.org/client-max-body-size: "128m" +spec: + tls: + - hosts: + - %CERTIFICATE_HOST% + secretName: %CERTIFICATE_HOST%-tls + rules: + - host: %CERTIFICATE_HOST% + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: %SERVICE_NAME% + port: + number: 8080 diff --git a/Web/.kubernetes/service.yaml b/Web/.kubernetes/service.yaml new file mode 100644 index 0000000..18e348d --- /dev/null +++ b/Web/.kubernetes/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: %SERVICE_NAME% + namespace: %KUBERNETES_NAMESPACE% +spec: + ports: + - name: http + port: 8080 + - name: https + port: 4443 + selector: + app: %SERVICE_NAME% + type: ClusterIP diff --git a/Web/Dockerfile b/Web/Dockerfile new file mode 100644 index 0000000..4b9e876 --- /dev/null +++ b/Web/Dockerfile @@ -0,0 +1,19 @@ +# Todo - Update and use NODE_JS_VERSION +ARG NODE_JS_VERSION + +FROM node:$NODE_JS_VERSION +EXPOSE 8080 + +WORKDIR /app +COPY package*.json ./ + +RUN npm install + +COPY . . + +RUN npm run build + +CMD ["npm", "run", "start"] + + + diff --git a/Web/README.md b/Web/README.md new file mode 100644 index 0000000..196491d --- /dev/null +++ b/Web/README.md @@ -0,0 +1,2 @@ +# Nano.Templates.Web +Coming... \ No newline at end of file