From 91659421936a201d529da966e01749b2bca0268f Mon Sep 17 00:00:00 2001 From: bjarneo Date: Sat, 8 Jun 2024 18:02:36 +0200 Subject: [PATCH] feat: initial deployment --- .dockerignore | 3 +- .env | 2 +- .github/workflows/deploy.yaml | 27 ++++++ .gitignore | 2 +- .kamal/hooks/docker-setup.sample | 7 ++ .kamal/hooks/post-deploy.sample | 14 +++ .kamal/hooks/post-traefik-reboot.sample | 3 + .kamal/hooks/pre-build.sample | 51 +++++++++++ .kamal/hooks/pre-connect.sample | 47 ++++++++++ .kamal/hooks/pre-deploy.sample | 109 ++++++++++++++++++++++++ .kamal/hooks/pre-traefik-reboot.sample | 3 + config/deploy.yml | 49 +++++++++++ 12 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/deploy.yaml create mode 100755 .kamal/hooks/docker-setup.sample create mode 100755 .kamal/hooks/post-deploy.sample create mode 100755 .kamal/hooks/post-traefik-reboot.sample create mode 100755 .kamal/hooks/pre-build.sample create mode 100755 .kamal/hooks/pre-connect.sample create mode 100755 .kamal/hooks/pre-deploy.sample create mode 100755 .kamal/hooks/pre-traefik-reboot.sample create mode 100644 config/deploy.yml diff --git a/.dockerignore b/.dockerignore index 2c11c91d..a3306567 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,4 +3,5 @@ node_modules example_request_do.log bin/ .git -hemmelig.backup.db \ No newline at end of file +hemmelig.backup.db +.env.private diff --git a/.env b/.env index 16a8d97f..577ddd94 100644 --- a/.env +++ b/.env @@ -5,4 +5,4 @@ # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. # See the documentation for all the connection string options: https://pris.ly/d/connection-strings -SECRET_DATABASE_URL="file:../database/hemmelig.db" \ No newline at end of file +SECRET_DATABASE_URL="file:../database/hemmelig.db" diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 00000000..a4d79698 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,27 @@ +name: Deploy instance +on: + push: + branches: + - kamal +jobs: + deploy: + runs-on: ubuntu-latest + env: + SECRET_MASTER_KEY: ${{ secrets.SECRET_MASTER_KEY }} + SECRET_JWT_SECRET: ${{ secrets.SECRET_JWT_SECRET }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3.0 + bundler-cache: true + - run: gem install kamal + - uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - uses: docker/setup-buildx-action@v3 + - run: sed -i 's/{REPLACE_ME_IP}/\${{ secrets.IP }}/g' config/deploy.yml + - run: kamal lock release + - run: kamal deploy diff --git a/.gitignore b/.gitignore index 87d40303..2281aa14 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* - +.env.private # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 00000000..fe68b937 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +# A sample docker-setup hook +# +# Sets up a Docker network which can then be used by the application’s containers + +ssh user@example.com docker network create kamal diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 00000000..75efafc1 --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-traefik-reboot.sample b/.kamal/hooks/post-traefik-reboot.sample new file mode 100755 index 00000000..e3d9e3cc --- /dev/null +++ b/.kamal/hooks/post-traefik-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted Traefik on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 00000000..f87d8113 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 00000000..18e61d7e --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 00000000..1b280c71 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end +end + + +$stdout.sync = true + +puts "Checking build status..." +attempts = 0 +checks = GithubStatusChecks.new + +begin + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-traefik-reboot.sample b/.kamal/hooks/pre-traefik-reboot.sample new file mode 100755 index 00000000..8cfda6d9 --- /dev/null +++ b/.kamal/hooks/pre-traefik-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting Traefik on $KAMAL_HOSTS..." diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 00000000..25defed3 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,49 @@ +# Name of your application. Used to uniquely configure containers. +service: hemmelig_app + +# Name of the container image. +image: hemmeligapp/hemmelig + +registry: + username: + - DOCKER_USERNAME + password: + - DOCKER_PASSWORD + +# Deploy to these servers. +servers: + - '{REPLACE_ME_IP}' + +volumes: + - '/root/hemmelig/userdata/:/var/tmp/hemmelig/upload/files' + - '/root/hemmelig/database/:/home/node/hemmelig/database/' + +# Inject ENV variables into containers (secrets come from .env). +# Remember to run `kamal env push` after making changes! +env: + clear: + SECRET_ROOT_USER: 'bjarneo' + SECRET_ROOT_PASSWORD: 'lolyouthinkthisisit' + SECRET_ROOT_EMAIL: 'hemmeligapp@gmail.com' + SECRET_MAX_TEXT_SIZE: '20048' + SECRET_HOST: 'hemmelig.app' + secret: + - SECRET_MASTER_KEY + - SECRET_JWT_SECRET + +# TODO: Implement non root user +# Use a different ssh user than root +# ssh: +# user: app + +# Configure custom arguments for Traefik. Be sure to reboot traefik when you modify it. +traefik: + args: + accesslog: true + accesslog.format: json + +# Configure a health check +healthcheck: + path: /api/healthz + port: 3000 + interval: 10s