From 05c1129fb89d60d321d8ab7788b8e19292d373eb Mon Sep 17 00:00:00 2001 From: Ilya Kreymer Date: Fri, 3 Dec 2021 10:17:22 -0800 Subject: [PATCH] Frontend + Backend Integrated Deployment (K8s only) (#45) * support running backend + frontend together on k8s * split nginx container into separate frontend service, which uses nignx-base image and the static frontend files * add nginx-based frontend image to docker-compose build (for building only, docker-based combined deployment not yet supported) * backend: - fix paths for email templates - chart: support '--set backend_only=1' and '--set frontend_only=1' to only force deploy one or the other - run backend from root /api in uvicorn --- backend/Dockerfile | 2 +- backend/emailsender.py | 4 +- chart/.helmignore | 1 + chart/nginx.conf | 6 +- chart/templates/{main.yaml => backend.yaml} | 66 ++++---------- chart/templates/configmap.yaml | 3 + chart/templates/frontend.yaml | 99 +++++++++++++++++++++ chart/templates/ingress.yaml | 10 ++- docker-compose.yml | 6 ++ frontend/Dockerfile | 6 ++ frontend/nginx.conf | 78 ++++++++++++++++ frontend/webpack.config.js | 4 +- 12 files changed, 227 insertions(+), 58 deletions(-) rename chart/templates/{main.yaml => backend.yaml} (61%) create mode 100644 chart/templates/frontend.yaml create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf diff --git a/backend/Dockerfile b/backend/Dockerfile index edf9a442..d3dd0d0f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -8,5 +8,5 @@ RUN pip install -r requirements.txt ADD . /app -CMD uvicorn main:app --host 0.0.0.0 --reload --access-log --log-level debug +CMD uvicorn main:app --host 0.0.0.0 --root-path /api --reload --access-log --log-level debug diff --git a/backend/emailsender.py b/backend/emailsender.py index b55afc82..fb3418a5 100644 --- a/backend/emailsender.py +++ b/backend/emailsender.py @@ -37,7 +37,7 @@ class EmailSender: message = f""" Please verify your registration for Browsertrix Cloud for {receiver_email} -You can verify by clicking here: {self.host}/app/verify/{token} +You can verify by clicking here: {self.host}/verify?token={token} The verification token is: {token}""" @@ -49,7 +49,7 @@ The verification token is: {token}""" message = f""" You are invited by {sender} to join their archive, {archive_name} on Browsertrix Cloud! -You can join by clicking here: {self.host}/app/join/{token} +You can join by clicking here: {self.host}/join/{token}?email={receiver_email} The invite token is: {token}""" diff --git a/chart/.helmignore b/chart/.helmignore index 0e8a0eb3..92b0fd14 100644 --- a/chart/.helmignore +++ b/chart/.helmignore @@ -21,3 +21,4 @@ .idea/ *.tmproj .vscode/ +frontend/ diff --git a/chart/nginx.conf b/chart/nginx.conf index 06d2a1b9..5c745700 100644 --- a/chart/nginx.conf +++ b/chart/nginx.conf @@ -63,8 +63,10 @@ http { } location / { - proxy_pass http://localhost:8000/; - proxy_set_header Host $host; + #proxy_pass http://localhost:8000/; + #proxy_set_header Host $host; + root /usr/share/nginx/html; + index index.html index.htm; } } } diff --git a/chart/templates/main.yaml b/chart/templates/backend.yaml similarity index 61% rename from chart/templates/main.yaml rename to chart/templates/backend.yaml index f1fd70e0..97ed127b 100644 --- a/chart/templates/main.yaml +++ b/chart/templates/backend.yaml @@ -2,35 +2,28 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Values.name }} + name: {{ .Values.name }}-backend namespace: {{ .Release.Namespace }} spec: selector: matchLabels: app: {{ .Values.name }} + role: backend replicas: {{ .Values.api_num_replicas }} template: metadata: labels: app: {{ .Values.name }} + role: backend annotations: # force helm to update the deployment each time + {{- if not .Values.frontend_only }} "helm.update": {{ randAlphaNum 5 | quote }} + {{- end }} spec: - volumes: - - name: nginx-config - configMap: - name: nginx-config - items: - - key: nginx.conf - path: nginx.conf - - - name: nginx-resolver - emptyDir: {} - initContainers: {{- if .Values.minio_local }} - name: init-bucket @@ -47,42 +40,8 @@ spec: args: ['-c', 'mc mb --ignore-existing local/{{ .Values.minio_local_bucket_name }}' ] {{- end }} - - name: init-nginx - image: {{ .Values.nginx_image }} - command: ["/bin/sh"] - args: ["-c", "echo resolver $(awk 'BEGIN{ORS=\" \"} $1==\"nameserver\" {print $2}' /etc/resolv.conf) valid=30s \";\" > /etc/nginx/resolvers/resolvers.conf"] - volumeMounts: - - name: nginx-resolver - mountPath: /etc/nginx/resolvers/ - containers: - - name: nginx - image: {{ .Values.nginx_image }} - imagePullPolicy: {{ .Values.nginx_pull_policy }} - volumeMounts: - - name: nginx-config - mountPath: /etc/nginx/nginx.conf - subPath: nginx.conf - readOnly: true - - - name: nginx-resolver - mountPath: /etc/nginx/resolvers/ - readOnly: true - - resources: - limits: - cpu: {{ .Values.nginx_limit_cpu }} - - requests: - cpu: {{ .Values.nginx_requests_cpu }} - - readinessProbe: - httpGet: - path: /healthz - port: 80 - - - name: api image: {{ .Values.api_image }} imagePullPolicy: {{ .Values.api_pull_policy }} @@ -101,6 +60,13 @@ spec: cpu: {{ .Values.api_requests_cpu }} memory: {{ .Values.api_requests_memory }} + startupProbe: + httpGet: + path: /healthz + port: 8000 + failureThreshold: 30 + periodSeconds: 5 + readinessProbe: httpGet: path: /healthz @@ -112,9 +78,10 @@ kind: Service metadata: namespace: {{ .Release.Namespace }} - name: {{ .Values.name }} + name: {{ .Values.name }}-backend labels: app: {{ .Values.name }} + role: backend {{- if .Values.service }} {{- if .Values.service.annotations }} @@ -128,6 +95,7 @@ metadata: spec: selector: app: {{ .Values.name }} + role: backend {{- if .Values.service }} {{- if .Values.service.type }} @@ -137,7 +105,5 @@ spec: ports: - protocol: TCP - port: 80 + port: 8000 name: api - #externalIPs: - # - 127.0.0.1 diff --git a/chart/templates/configmap.yaml b/chart/templates/configmap.yaml index d48556af..aecb5b7e 100644 --- a/chart/templates/configmap.yaml +++ b/chart/templates/configmap.yaml @@ -45,3 +45,6 @@ metadata: data: {{ (.Files.Glob "*.conf").AsConfig | indent 2 }} + +#{{ (.Files.Glob "frontend/*.*").AsConfig | indent 2 }} + diff --git a/chart/templates/frontend.yaml b/chart/templates/frontend.yaml new file mode 100644 index 00000000..b02ff28f --- /dev/null +++ b/chart/templates/frontend.yaml @@ -0,0 +1,99 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.name }}-frontend + namespace: {{ .Release.Namespace }} + +spec: + selector: + matchLabels: + app: {{ .Values.name }} + role: frontend + replicas: 1 + template: + metadata: + labels: + app: {{ .Values.name }} + role: frontend + + annotations: + # force helm to update the deployment each time + {{- if not .Values.backend_only }} + "helm.update": {{ randAlphaNum 5 | quote }} + {{- end }} + + + spec: + volumes: + - name: nginx-resolver + emptyDir: {} + + initContainers: + - name: init-nginx + image: {{ .Values.nginx_image }} + command: ["/bin/sh"] + args: ["-c", "echo resolver $(awk 'BEGIN{ORS=\" \"} $1==\"nameserver\" {print $2}' /etc/resolv.conf) valid=30s \";\" > /etc/nginx/resolvers/resolvers.conf"] + volumeMounts: + - name: nginx-resolver + mountPath: /etc/nginx/resolvers/ + + + containers: + - name: nginx + image: {{ .Values.nginx_image }} + imagePullPolicy: {{ .Values.nginx_pull_policy }} + volumeMounts: + - name: nginx-resolver + mountPath: /etc/nginx/resolvers/ + readOnly: true + + resources: + limits: + cpu: {{ .Values.nginx_limit_cpu }} + + requests: + cpu: {{ .Values.nginx_requests_cpu }} + + readinessProbe: + httpGet: + path: /healthz + port: 80 + +--- + +apiVersion: v1 +kind: Service + +metadata: + namespace: {{ .Release.Namespace }} + name: {{ .Values.name }}-frontend + labels: + app: {{ .Values.name }} + role: frontend + +{{- if .Values.service }} + {{- if .Values.service.annotations }} + annotations: + {{- range $key, $val := .Values.service.annotations }} + {{ $key }}: {{ $val | quote }} + {{- end }} + {{- end }} +{{- end }} + +spec: + selector: + app: {{ .Values.name }} + role: frontend + +{{- if .Values.service }} + {{- if .Values.service.type }} + type: {{ .Values.service.type | quote }} + {{- end }} +{{- end }} + + ports: + - protocol: TCP + port: 80 + name: frontend + diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml index ee51c30f..5fb0d9db 100644 --- a/chart/templates/ingress.yaml +++ b/chart/templates/ingress.yaml @@ -40,11 +40,19 @@ spec: number: 9000 {{- end }} + - path: /api/(.*) + pathType: Prefix + backend: + service: + name: browsertrix-cloud-backend + port: + number: 8000 + - path: /(.*) pathType: Prefix backend: service: - name: browsertrix-cloud + name: browsertrix-cloud-frontend port: number: 80 diff --git a/docker-compose.yml b/docker-compose.yml index b90a122b..eddf729c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,12 @@ services: - minio - mongo + frontend: + build: ./frontend + image: registry.digitalocean.com/btrix/webrecorder/browsertrix-frontend + ports: + - 8010:80 + redis: image: redis command: redis-server --appendonly yes diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..a5a89a5b --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,6 @@ +FROM nginx + +COPY ./index.html /usr/share/nginx/html +COPY ./dist/main.js /usr/share/nginx/html +COPY ./nginx.conf /etc/nginx/nginx.conf + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 00000000..5df7220f --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,78 @@ +worker_processes 1; +error_log stderr; +pid /var/run/nginx.pid; +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /dev/stdout; + sendfile on; + keepalive_timeout 65; + include ./resolvers/resolvers.conf; + server { + listen 80 default_server; + server_name _; + proxy_buffering off; + proxy_buffers 16 64k; + proxy_buffer_size 64k; + root /usr/share/nginx/html; + index index.html index.htm; + + error_page 500 501 502 503 504 /50x.html; + + merge_slashes off; + location = /50x.html { + root /usr/share/nginx/html; + } + + # fallback to index for any page + error_page 404 /index.html; + + location ~* /watch/([^/]+)/([^/]+)/ws { + set $archive $1; + set $crawl $2; + #auth_request /authcheck; + + proxy_pass http://$2.crawlers.svc.cluster.local:9037/ws; + proxy_set_header Host "localhost"; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + } + + location ~* /watch/([^/]+)/([^/]+)/ { + set $archive $1; + set $crawl $2; + #auth_request /authcheck; + + proxy_pass http://$2.crawlers.svc.cluster.local:9037/; + proxy_set_header Host "localhost"; + } + + location = /authcheck { + internal; + proxy_pass http://localhost:8000/archives/$archive/crawls/$crawl; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + } + + location /healthz { + return 200; + } + + location / { + #proxy_pass http://localhost:8000/; + #proxy_set_header Host $host; + root /usr/share/nginx/html; + index index.html index.htm; + } + } +} + diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index ccae3be0..dc65b9f7 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -20,7 +20,7 @@ const shoelaceAssetsSrcPath = path.resolve( __dirname, "node_modules/@shoelace-style/shoelace/dist/assets" ); -const shoelaceAssetsPublicPath = "/shoelace/assets"; +const shoelaceAssetsPublicPath = "shoelace/assets"; module.exports = { entry: "./src/index.ts", @@ -63,7 +63,7 @@ module.exports = { static: [ { directory: shoelaceAssetsSrcPath, - publicPath: shoelaceAssetsPublicPath, + publicPath: "/" + shoelaceAssetsPublicPath, }, {