Merge branch 'main' into deploy-without-build-push

This commit is contained in:
Samuel Sieg
2023-03-13 16:10:31 +01:00
15 changed files with 75 additions and 32 deletions

View File

@@ -8,12 +8,15 @@ jobs:
- "2.7" - "2.7"
- "3.1" - "3.1"
- "3.2" - "3.2"
gemfile:
- Gemfile
- gemfiles/rails_edge.gemfile
continue-on-error: [false] continue-on-error: [false]
name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }} name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: ${{ matrix.continue-on-error }} continue-on-error: ${{ matrix.continue-on-error }}
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@@ -16,10 +16,6 @@ jobs:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
-
name: Extract Version Number
id: extract_version
run: echo "::set-output name=version::${{ github.ref | replace('refs/tags/', '') }}"
- -
name: Set up QEMU name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
@@ -42,4 +38,4 @@ jobs:
push: true push: true
tags: | tags: |
ghcr.io/mrsked/mrsk:latest ghcr.io/mrsked/mrsk:latest
ghcr.io/mrsked/mrsk:${{ steps.extract_version.outputs.version }} ghcr.io/mrsked/mrsk:${{ github.ref_name }}

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.byebug_history .byebug_history
*.gem *.gem
coverage/* coverage/*
.DS_Store .DS_Store
gemfiles/*.lock

View File

@@ -1,6 +1,9 @@
# Use the official Ruby 3.2.0 Alpine image as the base image # Use the official Ruby 3.2.0 Alpine image as the base image
FROM ruby:3.2.0-alpine FROM ruby:3.2.0-alpine
# Install docker/buildx-bin
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
# Set the working directory to /mrsk # Set the working directory to /mrsk
WORKDIR /mrsk WORKDIR /mrsk

View File

@@ -2,7 +2,3 @@ source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" } git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gemspec gemspec
gem "debug"
gem "mocha"
gem "railties"

View File

@@ -4,6 +4,8 @@ MRSK deploys web apps anywhere from bare metal to cloud VMs using Docker with ze
Watch the screencast: https://www.youtube.com/watch?v=LL1cV2FXZ5I Watch the screencast: https://www.youtube.com/watch?v=LL1cV2FXZ5I
Join us on Discord: https://discord.gg/DQETs3Pm
## Installation ## Installation
Install MRSK globally with `gem install mrsk`. Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this: Install MRSK globally with `gem install mrsk`. Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this:
@@ -23,7 +25,7 @@ env:
- RAILS_MASTER_KEY - RAILS_MASTER_KEY
``` ```
Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSWORD` (and your `RAILS_MASTER_KEY` for production with a Rails app). Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSWORD` (and your `RAILS_MASTER_KEY` for production with a Rails app).
Now you're ready to deploy to the servers: Now you're ready to deploy to the servers:
@@ -67,6 +69,16 @@ Docker Swarm is much simpler than Kubernetes, but it's still built on the same d
Ultimately, there are a myriad of ways to deploy web apps, but this is the toolkit we're using at [37signals](https://37signals.com) to bring [HEY](https://www.hey.com) [home from the cloud](https://world.hey.com/dhh/why-we-re-leaving-the-cloud-654b47e0) without losing the advantages of modern containerization tooling. Ultimately, there are a myriad of ways to deploy web apps, but this is the toolkit we're using at [37signals](https://37signals.com) to bring [HEY](https://www.hey.com) [home from the cloud](https://world.hey.com/dhh/why-we-re-leaving-the-cloud-654b47e0) without losing the advantages of modern containerization tooling.
## Running MRSK from Docker
MRSK is packaged up in a Docker container similarly to [rails/docked](https://github.com/rails/docked). This will allow you to run MRSK (from your application directory) without having to install any dependencies other than Docker. Add the following alias to your profile configuration to make working with the container more convenient:
```bash
alias mrsk="docker run -it --rm -v '${PWD}:/workdir' -v '${SSH_AUTH_SOCK}:/ssh-agent' -v /var/run/docker.sock:/var/run/docker.sock -e 'SSH_AUTH_SOCK=/ssh-agent' ghcr.io/mrsked/mrsk:latest"
```
Since MRSK uses SSH to establish a remote connection, it will need access to your SSH agent. The above command uses a volume mount to make it available inside the container and configures the SSH agent inside the container to make use of it.
## Configuration ## Configuration
### Using .env file to load required environment variables ### Using .env file to load required environment variables
@@ -99,9 +111,9 @@ If you need separate env variables for different destinations, you can set them
#### Bitwarden as a secret store #### Bitwarden as a secret store
If you are using open source secret store like bitwarden, you can create `.env.erb` as a template which looks up the secrets. If you are using open source secret store like bitwarden, you can create `.env.erb` as a template which looks up the secrets.
You can store `SOME_SECRET` in a secure note in bitwarden vault. You can store `SOME_SECRET` in a secure note in bitwarden vault.
``` ```
$ bw list items --search SOME_SECRET | jq $ bw list items --search SOME_SECRET | jq
@@ -140,7 +152,7 @@ SOME_SECRET=<%= `bw get notes 123123123-1232-4224-222f-234234234234 --session #{
<% else raise ArgumentError, "session_token token missing" end %> <% else raise ArgumentError, "session_token token missing" end %>
``` ```
Then everyone deploying the app can run `mrsk envify` and mrsk will generate `.env` Then everyone deploying the app can run `mrsk envify` and mrsk will generate `.env`
### Using another registry than Docker Hub ### Using another registry than Docker Hub
@@ -150,9 +162,9 @@ The default registry is Docker Hub, but you can change it using `registry/server
```yaml ```yaml
registry: registry:
server: registry.digitalocean.com server: registry.digitalocean.com
username: username:
- DOCKER_REGISTRY_TOKEN - DOCKER_REGISTRY_TOKEN
password: password:
- DOCKER_REGISTRY_TOKEN - DOCKER_REGISTRY_TOKEN
``` ```
@@ -222,6 +234,12 @@ volumes:
- "/local/path:/container/path" - "/local/path:/container/path"
``` ```
### MRSK env variables
The following env variables are set when your container runs:
`MRSK_CONTAINER_NAME` : this contains the current container name and version
### Using different roles for servers ### Using different roles for servers
If your application uses separate hosts for running jobs or other roles beyond the default web running, you can specify these hosts in a dedicated role with a new entrypoint command like so: If your application uses separate hosts for running jobs or other roles beyond the default web running, you can specify these hosts in a dedicated role with a new entrypoint command like so:
@@ -256,12 +274,12 @@ servers:
You can specialize the default Traefik rules by setting labels on the containers that are being started: You can specialize the default Traefik rules by setting labels on the containers that are being started:
``` ```yaml
labels: labels:
traefik.http.routers.hey.rule: Host(\`app.hey.com\`) traefik.http.routers.hey.rule: Host(`app.hey.com`)
``` ```
Note: The escaped backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash! Note: The backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
This allows you to run multiple applications on the same server sharing the same Traefik instance and port. This allows you to run multiple applications on the same server sharing the same Traefik instance and port.
See https://doc.traefik.io/traefik/routing/routers/#rule for a full list of available routing rules. See https://doc.traefik.io/traefik/routing/routers/#rule for a full list of available routing rules.

View File

@@ -0,0 +1,9 @@
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
git "https://github.com/rails/rails.git" do
gem "railties"
gem "activesupport"
end
gemspec path: "../"

View File

@@ -149,13 +149,13 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end end
end end
desc "remove [NAME]", "Remove accessory container and image from host (use NAME=all to remove all accessories)" desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
def remove(name) def remove(name)
if name == "all" if name == "all"
MRSK.accessory_names.each { |accessory_name| remove(accessory_name) } MRSK.accessory_names.each { |accessory_name| remove(accessory_name) }
else else
if options[:confirmed] || ask("This will remove all containers and images for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y" if options[:confirmed] || ask("This will remove all containers, images and data directories for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
with_accessory(name) do with_accessory(name) do
stop(name) stop(name)
remove_container(name) remove_container(name)

View File

@@ -7,6 +7,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
"--restart unless-stopped", "--restart unless-stopped",
"--log-opt", "max-size=#{MAX_LOG_SIZE}", "--log-opt", "max-size=#{MAX_LOG_SIZE}",
"--name", service_with_version_and_destination, "--name", service_with_version_and_destination,
"-e", "MRSK_CONTAINER_NAME=\"#{service_with_version_and_destination}\"",
*role.env_args, *role.env_args,
*config.volume_args, *config.volume_args,
*role.label_args, *role.label_args,

View File

@@ -9,8 +9,10 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
"--name", container_name_with_version, "--name", container_name_with_version,
"--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}", "--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
"--label", "service=#{container_name}", "--label", "service=#{container_name}",
"-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
*web.env_args, *web.env_args,
*config.volume_args, *config.volume_args,
*web.option_args,
config.absolute_image, config.absolute_image,
web.cmd web.cmd
end end

View File

@@ -55,7 +55,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
private private
def cmd_option_args def cmd_option_args
if args = config.raw_config.dig(:traefik, "args") if args = config.traefik["args"]
optionize args optionize args
else else
[] []
@@ -63,6 +63,6 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
end end
def host_port def host_port
config.raw_config.dig(:traefik, "host_port") || CONTAINER_PORT config.traefik["host_port"] || CONTAINER_PORT
end end
end end

View File

@@ -165,6 +165,9 @@ class Mrsk::Configuration
}.compact }.compact
end end
def traefik
raw_config.traefik || {}
end
private private
# Will raise ArgumentError if any required config keys are missing # Will raise ArgumentError if any required config keys are missing

View File

@@ -19,4 +19,8 @@ Gem::Specification.new do |spec|
spec.add_dependency "zeitwerk", "~> 2.5" spec.add_dependency "zeitwerk", "~> 2.5"
spec.add_dependency "ed25519", "~> 1.2" spec.add_dependency "ed25519", "~> 1.2"
spec.add_dependency "bcrypt_pbkdf", "~> 1.0" spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
spec.add_development_dependency "debug"
spec.add_development_dependency "mocha"
spec.add_development_dependency "railties"
end end

View File

@@ -14,7 +14,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ assert_equal \
"docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
@app.run.join(" ") @app.run.join(" ")
end end
@@ -22,7 +22,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:volumes] = ["/local/path:/container/path" ] @config[:volumes] = ["/local/path:/container/path" ]
assert_equal \ assert_equal \
"docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
@app.run.join(" ") @app.run.join(" ")
end end
@@ -30,7 +30,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:healthcheck] = { "path" => "/healthz" } @config[:healthcheck] = { "path" => "/healthz" }
assert_equal \ assert_equal \
"docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
@app.run.join(" ") @app.run.join(" ")
end end
@@ -39,7 +39,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config).tap { |c| c.version = "999" } @app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config).tap { |c| c.version = "999" }
assert_equal \ assert_equal \
"docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
@app.run(role: :jobs).join(" ") @app.run(role: :jobs).join(" ")
end end

View File

@@ -10,7 +10,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ assert_equal \
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app dhh/app:123", "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" dhh/app:123",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -18,7 +18,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@config[:healthcheck] = { "port" => 3001 } @config[:healthcheck] = { "port" => 3001 }
assert_equal \ assert_equal \
"docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app dhh/app:123", "docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" dhh/app:123",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -26,7 +26,14 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@destination = "staging" @destination = "staging"
assert_equal \ assert_equal \
"docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging dhh/app:123", "docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging -e MRSK_CONTAINER_NAME=\"healthcheck-app-staging\" dhh/app:123",
new_command.run.join(" ")
end
test "run with custom options" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere" } } }
assert_equal \
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app --mount \"somewhere\" dhh/app:123",
new_command.run.join(" ") new_command.run.join(" ")
end end