diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 589e59649db..47987805d89 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,4 +1,4 @@ -name: Deploy production +name: Deploy production on Kube on: workflow_dispatch: ~ @@ -14,8 +14,8 @@ concurrency: jobs: - deploy-production: - name: '🚧 Build & deploy 🚀' + build-production: + name: '🚧 Build 🚀' runs-on: ubuntu-latest timeout-minutes: 10 @@ -23,14 +23,16 @@ jobs: name: production url: ${{ vars.WEBSITE_URL }} + permissions: + contents: read + packages: write + steps: - name: 'Checkout' uses: actions/checkout@v4 - - name: 'Configure deployer SSH key' - uses: webfactory/ssh-agent@v0.8.0 - with: - ssh-private-key: ${{ secrets.SSH_DEPLOY_KEY_PRODUCTION }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 # https://github.com/actions/setup-node - name: 'Setup node' @@ -86,8 +88,6 @@ jobs: - name: 'Build static site' run: | bin/console stenope:build --no-interaction -vv --ansi --ignore-content-not-found - bin/console app:generate-redirections --target=site --no-interaction -vv --ansi > build/redirections-site.conf - bin/console app:generate-redirections --target=blog --no-interaction -vv --ansi > build/redirections-blog.conf env: APP_ENV: prod ROUTER_DEFAULT_URI: ${{ vars.WEBSITE_URL }} @@ -95,16 +95,76 @@ jobs: SHOW_UNPUBLISHED_ARTICLES: 0 MATOMO_ID: ${{ vars.MATOMO_ID }} - - name: '🚀 Deploy' + - name: 'Login to GitHub Container Registry' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: elao-tools + password: ${{ secrets.GITHUB_TOKEN }} + + - name: 'Build and push the Docker image for website prod' + uses: docker/build-push-action@v5 + with: + context: . + file: kubernetes/Dockerfile + push: true + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ghcr.io/elao/website:latest + + deploy-helm: + runs-on: ubuntu-22.04 + timeout-minutes: 5 + needs: [build-production] + + steps: + - name: 'Set the Kubernetes context' + uses: azure/k8s-set-context@v4 + with: + method: kubeconfig + kubeconfig: $${{ secrets.KUBERNETES_KUBECONFIG }} + context: kubernetes-admin@elao_argon-website-prod + + - name: Checkout source code + uses: actions/checkout@v4 + + - name: 'Render charts website' + uses: azure/k8s-bake@v2.4 + id: bake_website + with: + renderEngine: helm + helmChart: kubernetes/charts/website + helm-version: latest + silent: false + + - name: 'Deploy to the Kubernetes cluster' + uses: azure/k8s-deploy@v5 + with: + action: deploy + force: true + namespace: website-prod + manifests: ${{ steps.bake_website.outputs.manifestsBundle }} + pull-images: false + + deploy-image: + runs-on: ubuntu-22.04 + timeout-minutes: 5 + needs: [deploy-helm] + + steps: + - name: 'Set the Kubernetes context' + uses: azure/k8s-set-context@v4 + with: + method: kubeconfig + kubeconfig: $${{ secrets.KUBERNETES_KUBECONFIG }} + context: kubernetes-admin@elao_argon-website-prod + + - uses: azure/setup-kubectl@v4 + name: Setup kubectl + with: + version: v1.28.2 + + - name: 'Rollout deployment website' run: | - rsync build/ ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ vars.DEPLOY_PATH }} \ - --human-readable \ - --compress \ - --archive \ - --delete \ - --rsh "ssh -o StrictHostKeyChecking=no" \ - --itemize-changes \ - ; - - - name: 'Reload Nginx config' - run: ssh ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} 'sudo /bin/systemctl reload nginx' + kubectl rollout restart deployment/website --namespace website-prod diff --git a/kubernetes/Dockerfile b/kubernetes/Dockerfile new file mode 100644 index 00000000000..67acf16ce81 --- /dev/null +++ b/kubernetes/Dockerfile @@ -0,0 +1,54 @@ +ARG DEBIAN=bookworm + +FROM debian:${DEBIAN}-slim as website + +ARG DEBIAN +ARG NGINX_VERSION=1.26 +ARG USER_ID=1000 +ARG GROUP_ID=1000 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg2 \ + sudo + +# User +RUN addgroup --gid ${GROUP_ID} app \ + && adduser --home /home/app --shell /bin/bash --uid ${USER_ID} --gecos app --ingroup app --disabled-password app \ + && echo "app ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/app \ + # App dir + && mkdir --parents /srv && chown app:app /srv + +######### +# Nginx # +######### +RUN \ + echo "deb http://nginx.org/packages/debian/ ${DEBIAN} nginx" > /etc/apt/sources.list.d/nginx.list \ + && curl -sSL http://nginx.org/keys/nginx_signing.key \ + | apt-key add - \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + nginx=${NGINX_VERSION}.* \ + && chown -R $USER_ID:0 /etc/nginx /var/log/nginx \ + && chmod -R g+w /etc/nginx + +################# +# Configuration # +################# +COPY --chown=app:app kubernetes/app.conf /etc/nginx/app.conf +COPY --chown=app:app build/ /srv/app/website/ + +######### +# Clean # +######### +RUN \ + rm -rf \ + /var/lib/apt/lists/* \ + /var/cache/debconf/*-old \ + /var/lib/dpkg/*-old \ + && truncate -s 0 /var/log/*.log + +CMD ["nginx", "-c", "/etc/nginx/app.conf"] diff --git a/kubernetes/app.conf b/kubernetes/app.conf new file mode 100644 index 00000000000..78d3eff68b5 --- /dev/null +++ b/kubernetes/app.conf @@ -0,0 +1,83 @@ +include /etc/nginx/modules-enabled/*.conf; +worker_processes 1; + +error_log stderr "warn"; +pid /etc/nginx/nginx.pid; + +events { + worker_connections 1024; + multi_accept on; + use epoll; +} + +daemon off; + +http { + include /etc/nginx/mime.types; + + proxy_temp_path /tmp/proxy_temp; + client_body_temp_path /tmp/client_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + log_format json escape=json '{' + '"remote_addr": "$remote_addr",' + '"remote_user": "$remote_user",' + '"time_local": "$time_local",' + '"request": "$request",' + '"request_length": $request_length,' + '"status": $status,' + '"body_bytes_sent": $body_bytes_sent,' + '"http_host": "$http_host",' + '"http_referer": "$http_referer",' + '"http_user_agent": "$http_user_agent",' + '"http_x_forwarded_for": "$http_x_forwarded_for",' + '"request_time": $request_time,' + '"upstream_connect_time": "$upstream_connect_time",' + '"upstream_header_time": "$upstream_header_time",' + '"upstream_response_time": "$upstream_response_time"' + '}'; + + access_log /dev/stdout json; + + server { + listen 8080; + server_name www.elao.com; + root /srv/app/website; + absolute_redirect off; + gzip on; + gzip_disable msie6; + gzip_vary on; + gzip_proxied expired no-cache no-store private auth; + gzip_comp_level 6; + gzip_min_length 1000; + gzip_types text/css text/javascript text/xml text/plain application/javascript application/x-javascript application/json application/xml application/rss+xml font/truetype application/x-font-ttf font/opentype application/vnd.ms-fontobject image/svg+xml; + # https://scotthelme.co.uk/hardening-your-http-response-headers/ + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy strict-origin-when-cross-origin; + add_header Feature-Policy 'geolocation \'self\';fullscreen \'self\';microphone \'none\';camera \'none\';autoplay \'none\';payment \'none\';speaker \'none\''; + location ~* ^.+\.(?:css|cur|js|jpe?g|gif|htc|ico|png|xml|otf|ttf|eot|woff|woff2|svg|webp)$ { + expires 60d; + add_header Cache-Control public; + } + location / { + try_files $uri $uri/index.html =404; + } + location ~ \.html { + internal; + } + error_page 404 /404.html; + } + + server { + listen 8080; + server_name blog.elao.com; + } +} diff --git a/kubernetes/charts/website/Chart.yaml b/kubernetes/charts/website/Chart.yaml new file mode 100644 index 00000000000..e610d4277e5 --- /dev/null +++ b/kubernetes/charts/website/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: website +version: 1.0.0 diff --git a/kubernetes/charts/website/templates/_helpers.tpl b/kubernetes/charts/website/templates/_helpers.tpl new file mode 100644 index 00000000000..db7a8008d7c --- /dev/null +++ b/kubernetes/charts/website/templates/_helpers.tpl @@ -0,0 +1,47 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "name" -}} +{{- .Chart.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "fullname" -}} +{{- $name := .Chart.Name -}} +{{- if contains $name .Chart.Name -}} +{{- .Chart.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Chart.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "labels" -}} +helm.sh/chart: {{ include "chart" . }} +{{ include "selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "selectorLabels" -}} +app.kubernetes.io/name: {{ include "name" . }} +app.kubernetes.io/instance: {{ .Chart.Name }} +{{- end }} diff --git a/kubernetes/charts/website/templates/deployment.yaml b/kubernetes/charts/website/templates/deployment.yaml new file mode 100644 index 00000000000..0d09a569d7f --- /dev/null +++ b/kubernetes/charts/website/templates/deployment.yaml @@ -0,0 +1,38 @@ +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "fullname" . }} + labels: + {{- include "labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + {{- include "selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "labels" . | nindent 8 }} + annotations: {} + spec: + imagePullSecrets: + - name: dockerconfig + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + containers: + - name: website + image: ghcr.io/elao/website:latest + resources: + requests: + cpu: 100m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + imagePullPolicy: Always + ports: + - containerPort: 8080 + protocol: TCP diff --git a/kubernetes/charts/website/templates/ingress.yaml b/kubernetes/charts/website/templates/ingress.yaml new file mode 100644 index 00000000000..61d55261415 --- /dev/null +++ b/kubernetes/charts/website/templates/ingress.yaml @@ -0,0 +1,42 @@ +--- + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "fullname" . }}-ingress + labels: + {{- include "labels" . | nindent 4 }} + annotations: + cert-manager.io/cluster-issuer: letsencrypt +spec: + ingressClassName: nginx + tls: + - hosts: + - www.elao.com + secretName: www.elao.com.tls + - hosts: + - blog.elao.com + secretName: blog.elao.com.tls + rules: + # Proxied to redirectionio service + - host: www.elao.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: redirectionio-service + port: + number: 8080 + # Proxied to redirectionio service + - host: blog.elao.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: redirectionio-service + port: + number: 8081 diff --git a/kubernetes/charts/website/templates/service.yaml b/kubernetes/charts/website/templates/service.yaml new file mode 100644 index 00000000000..8a6b038fea8 --- /dev/null +++ b/kubernetes/charts/website/templates/service.yaml @@ -0,0 +1,15 @@ +--- + +kind: Service +apiVersion: v1 +metadata: + name: {{ include "fullname" . }}-service + labels: + {{- include "labels" . | nindent 4 }} +spec: + selector: + {{- include "selectorLabels" . | nindent 4 }} + type: ClusterIP + ports: + - name: web + port: 8080