Copy env files to remote hosts
Setting env variables in the docker arguments requires having them on the deploy host. Instead we'll add two new commands `kamal env push` and `kamal env delete` which will manage copying the environment as .env files to the remote host. Docker will pick up the file with `--env-file <path-to-file>`. Env files will be stored under `<kamal run directory>/env`. Running `kamal env push` will create env files for each role and accessory, and traefik if required. `kamal envify` has been updated to also push the env files. By avoiding using `kamal envify` and creating the local and remote secrets manually, you can now avoid accessing secrets needed for the docker runtime environment locally. You will still need build secrets. One thing to note - the Docker doesn't parse the environment variables in the env file, one result of this is that you can't specify multi-line values - see https://github.com/moby/moby/issues/12997. We maybe need to look docker config or docker secrets longer term to get around this. Hattip to @kevinmcconnell - this was all his idea.
This commit is contained in:
52
lib/kamal/cli/env.rb
Normal file
52
lib/kamal/cli/env.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
require "tempfile"
|
||||
|
||||
class Kamal::Cli::Env < Kamal::Cli::Base
|
||||
desc "push", "Push the env file to the remote hosts"
|
||||
def push
|
||||
mutating do
|
||||
on(KAMAL.hosts) do
|
||||
KAMAL.roles_on(host).each do |role|
|
||||
role_config = KAMAL.config.role(role)
|
||||
execute *KAMAL.app(role: role).make_env_directory
|
||||
upload! StringIO.new(role_config.env_file), role_config.host_env_file_path, mode: 400
|
||||
end
|
||||
end
|
||||
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.traefik.make_env_directory
|
||||
upload! StringIO.new(KAMAL.traefik.env_file), KAMAL.traefik.host_env_file_path, mode: 400
|
||||
end
|
||||
|
||||
on(KAMAL.accessory_hosts) do
|
||||
KAMAL.accessories_on(host).each do |accessory|
|
||||
accessory_config = KAMAL.config.accessory(accessory)
|
||||
execute *KAMAL.accessory(accessory).make_env_directory
|
||||
upload! StringIO.new(accessory_config.env_file), accessory_config.host_env_file_path, mode: 400
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "delete", "Delete the env file from the remote hosts"
|
||||
def delete
|
||||
mutating do
|
||||
on(KAMAL.hosts) do
|
||||
KAMAL.roles_on(host).each do |role|
|
||||
role_config = KAMAL.config.role(role)
|
||||
execute *KAMAL.app(role: role).remove_env_file
|
||||
end
|
||||
end
|
||||
|
||||
on(KAMAL.traefik_hosts) do
|
||||
execute *KAMAL.traefik.remove_env_file
|
||||
end
|
||||
|
||||
on(KAMAL.accessory_hosts) do
|
||||
KAMAL.accessories_on(host).each do |accessory|
|
||||
accessory_config = KAMAL.config.accessory(accessory)
|
||||
execute *KAMAL.accessory(accessory).remove_env_file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -175,6 +175,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
end
|
||||
|
||||
File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
|
||||
|
||||
load_envs # reload new file
|
||||
invoke "kamal:cli:env:push", options
|
||||
end
|
||||
|
||||
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
||||
@@ -204,6 +207,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
desc "build", "Build application image"
|
||||
subcommand "build", Kamal::Cli::Build
|
||||
|
||||
desc "env", "Manage environment files"
|
||||
subcommand "env", Kamal::Cli::Env
|
||||
|
||||
desc "healthcheck", "Healthcheck application"
|
||||
subcommand "healthcheck", Kamal::Cli::Healthcheck
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ class Kamal::Commander
|
||||
config.accessories&.collect(&:name) || []
|
||||
end
|
||||
|
||||
def accessories_on(host)
|
||||
config.accessories.select { |accessory| accessory.hosts.include?(host.to_s) }.map(&:name)
|
||||
end
|
||||
|
||||
|
||||
def app(role: nil)
|
||||
Kamal::Commands::App.new(config, role: role)
|
||||
|
||||
@@ -86,14 +86,6 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||
end
|
||||
end
|
||||
|
||||
def make_directory_for(remote_file)
|
||||
make_directory Pathname.new(remote_file).dirname.to_s
|
||||
end
|
||||
|
||||
def make_directory(path)
|
||||
[ :mkdir, "-p", path ]
|
||||
end
|
||||
|
||||
def remove_service_directory
|
||||
[ :rm, "-rf", service_name ]
|
||||
end
|
||||
@@ -106,6 +98,14 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||
docker :image, :rm, "--force", image
|
||||
end
|
||||
|
||||
def make_env_directory
|
||||
make_directory accessory_config.host_env_directory
|
||||
end
|
||||
|
||||
def remove_env_file
|
||||
[:rm, "-f", accessory_config.host_env_file_path]
|
||||
end
|
||||
|
||||
private
|
||||
def service_filter
|
||||
[ "--filter", "label=service=#{service_name}" ]
|
||||
|
||||
@@ -81,7 +81,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
docker :run,
|
||||
("-it" if interactive),
|
||||
"--rm",
|
||||
*config.env_args,
|
||||
*role&.env_args,
|
||||
*config.volume_args,
|
||||
*role&.option_args,
|
||||
config.absolute_image,
|
||||
@@ -149,6 +149,13 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
docker :tag, config.absolute_image, config.latest_image
|
||||
end
|
||||
|
||||
def make_env_directory
|
||||
make_directory config.role(role).host_env_directory
|
||||
end
|
||||
|
||||
def remove_env_file
|
||||
[:rm, "-f", config.role(role).host_env_file_path]
|
||||
end
|
||||
|
||||
private
|
||||
def container_name(version = nil)
|
||||
|
||||
@@ -26,6 +26,14 @@ module Kamal::Commands
|
||||
docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet"
|
||||
end
|
||||
|
||||
def make_directory_for(remote_file)
|
||||
make_directory Pathname.new(remote_file).dirname.to_s
|
||||
end
|
||||
|
||||
def make_directory(path)
|
||||
[ :mkdir, "-p", path ]
|
||||
end
|
||||
|
||||
private
|
||||
def combine(*commands, by: "&&")
|
||||
commands
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Kamal::Commands::Traefik < Kamal::Commands::Base
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
||||
delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils
|
||||
|
||||
DEFAULT_IMAGE = "traefik:v2.9"
|
||||
CONTAINER_PORT = 80
|
||||
@@ -63,6 +63,22 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
|
||||
"#{host_port}:#{CONTAINER_PORT}"
|
||||
end
|
||||
|
||||
def env_file
|
||||
env_file_with_secrets config.traefik.fetch("env", {})
|
||||
end
|
||||
|
||||
def host_env_file_path
|
||||
File.join host_env_directory, "traefik.env"
|
||||
end
|
||||
|
||||
def make_env_directory
|
||||
make_directory(host_env_directory)
|
||||
end
|
||||
|
||||
def remove_env_file
|
||||
[:rm, "-f", host_env_file_path]
|
||||
end
|
||||
|
||||
private
|
||||
def publish_args
|
||||
argumentize "--publish", port unless config.traefik["publish"] == false
|
||||
@@ -73,13 +89,11 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
def env_args
|
||||
env_config = config.traefik["env"] || {}
|
||||
argumentize "--env-file", host_env_file_path
|
||||
end
|
||||
|
||||
if env_config.present?
|
||||
argumentize_env_with_secrets(env_config)
|
||||
else
|
||||
[]
|
||||
end
|
||||
def host_env_directory
|
||||
File.join config.host_env_directory, "traefik"
|
||||
end
|
||||
|
||||
def labels
|
||||
|
||||
@@ -7,7 +7,7 @@ require "net/ssh/proxy/jump"
|
||||
|
||||
class Kamal::Configuration
|
||||
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_accessor :destination
|
||||
attr_accessor :raw_config
|
||||
@@ -113,14 +113,6 @@ class Kamal::Configuration
|
||||
end
|
||||
|
||||
|
||||
def env_args
|
||||
if raw_config.env.present?
|
||||
argumentize_env_with_secrets(raw_config.env)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def volume_args
|
||||
if raw_config.volumes.present?
|
||||
argumentize "--volume", raw_config.volumes
|
||||
@@ -174,7 +166,6 @@ class Kamal::Configuration
|
||||
repository: repository,
|
||||
absolute_image: absolute_image,
|
||||
service_with_version: service_with_version,
|
||||
env_args: env_args,
|
||||
volume_args: volume_args,
|
||||
ssh_options: ssh.to_h,
|
||||
sshkit: sshkit.to_h,
|
||||
@@ -199,12 +190,15 @@ class Kamal::Configuration
|
||||
|
||||
# Will raise KeyError if any secret ENVs are missing
|
||||
def ensure_env_available
|
||||
env_args
|
||||
roles.each(&:env_args)
|
||||
roles.each(&:env_file)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def host_env_directory
|
||||
"#{run_directory}/env"
|
||||
end
|
||||
|
||||
private
|
||||
# Will raise ArgumentError if any required config keys are missing
|
||||
def ensure_required_keys_present
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Kamal::Configuration::Accessory
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
||||
delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_accessor :name, :specifics
|
||||
|
||||
@@ -45,8 +45,20 @@ class Kamal::Configuration::Accessory
|
||||
specifics["env"] || {}
|
||||
end
|
||||
|
||||
def env_file
|
||||
env_file_with_secrets env
|
||||
end
|
||||
|
||||
def host_env_directory
|
||||
File.join config.host_env_directory, "accessories"
|
||||
end
|
||||
|
||||
def host_env_file_path
|
||||
File.join host_env_directory, "#{service_name}.env"
|
||||
end
|
||||
|
||||
def env_args
|
||||
argumentize_env_with_secrets env
|
||||
argumentize "--env-file", host_env_file_path
|
||||
end
|
||||
|
||||
def files
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Kamal::Configuration::Role
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
||||
delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_accessor :name
|
||||
|
||||
@@ -31,8 +31,20 @@ class Kamal::Configuration::Role
|
||||
end
|
||||
end
|
||||
|
||||
def env_file
|
||||
env_file_with_secrets env
|
||||
end
|
||||
|
||||
def host_env_directory
|
||||
File.join config.host_env_directory, "roles"
|
||||
end
|
||||
|
||||
def host_env_file_path
|
||||
File.join host_env_directory, "#{[config.service, name, config.destination].compact.join("-")}.env"
|
||||
end
|
||||
|
||||
def env_args
|
||||
argumentize_env_with_secrets env
|
||||
argumentize "--env-file", host_env_file_path
|
||||
end
|
||||
|
||||
def health_check_args
|
||||
|
||||
@@ -16,14 +16,24 @@ module Kamal::Utils
|
||||
end
|
||||
end
|
||||
|
||||
# Return a list of shell arguments using the same named argument against the passed attributes,
|
||||
# but redacts and expands secrets.
|
||||
def argumentize_env_with_secrets(env)
|
||||
if (secrets = env["secret"]).present?
|
||||
argumentize("-e", secrets.to_h { |key| [ key, ENV.fetch(key) ] }, sensitive: true) + argumentize("-e", env["clear"])
|
||||
else
|
||||
argumentize "-e", env.fetch("clear", env)
|
||||
end
|
||||
def env_file_with_secrets(env)
|
||||
env_file = StringIO.new.tap do |contents|
|
||||
if (secrets = env["secret"]).present?
|
||||
env.fetch("secret", env)&.each do |key|
|
||||
contents << docker_env_file_line(key, ENV.fetch(key))
|
||||
end
|
||||
env["clear"]&.each do |key, value|
|
||||
contents << docker_env_file_line(key, value)
|
||||
end
|
||||
else
|
||||
env.fetch("clear", env)&.each do |key, value|
|
||||
contents << docker_env_file_line(key, value)
|
||||
end
|
||||
end
|
||||
end.string
|
||||
|
||||
# Ensure the file has some contents to avoid the SSHKIT empty file warning
|
||||
env_file || "\n"
|
||||
end
|
||||
|
||||
# Returns a list of shell-dashed option arguments. If the value is true, it's treated like a value-less option.
|
||||
@@ -97,4 +107,12 @@ module Kamal::Utils
|
||||
def uncommitted_changes
|
||||
`git status --porcelain`.strip
|
||||
end
|
||||
|
||||
def docker_env_file_line(key, value)
|
||||
if key.include?("\n") || value.to_s.include?("\n")
|
||||
raise ArgumentError, "docker env file format does not support newlines in keys or values, key: #{key}"
|
||||
end
|
||||
|
||||
"#{key.to_s}=#{value.to_s}\n"
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user