kubernetes-deploy
is a command line tool that helps you ship changes to a Kubernetes namespace and understand the result. At Shopify, we use it within our much-beloved, open-source Shipit deployment app.
Why not just use the standard kubectl apply
mechanism to deploy? It is indeed a fantastic tool; kubernetes-deploy
uses it under the hood! However, it leaves its users with some burning questions: What just happened? Did it work?
Especially in a CI/CD environment, we need a clear, actionable pass/fail result for each deploy. Providing this was the foundational goal of kubernetes-deploy
, which has grown to support the following core features:
:eyes: Watches the changes you requested to make sure they roll out successfully.
🔢 Predeploys certain types of resources (e.g. ConfigMap, PersistentVolumeClaim) to make sure the latest version will be available when resources that might consume them (e.g. Deployment) are deployed.
🔐 Creates Kubernetes secrets from encrypted EJSON, which you can safely commit to your repository
:running: Running tasks at the beginning of a deploy using bare pods (example use case: Rails migrations)
This repo also includes related tools for running tasks and restarting deployments.
KUBERNETES-DEPLOY
KUBERNETES-RESTART
KUBERNETES-RUN
DEVELOPMENT
- Setup
- Running the test suite locally
- Releasing a new version (Shopify employees)
- CI (External contributors)
CONTRIBUTING
- Ruby 2.3+
- Your cluster must be running Kubernetes v1.7.0 or higher1
- Each app must have a deploy directory containing its Kubernetes templates (see Templates)
- You must remove the
kubectl.kubernetes.io/last-applied-configuration
annotation from any resources in the namespace that are not included in your deploy directory. This annotation is added automatically when you create resources withkubectl apply
.kubernetes-deploy
will prune any resources that have this annotation and are not in the deploy directory.2 - Each app managed by
kubernetes-deploy
must have its own exclusive Kubernetes namespace.
1 We run integration tests against these Kubernetes versions. Kubernetes v1.6 was officially supported in gem versions < 0.16. Kubernetes v1.5 was officially supported in gem versions < 0.12.
2 This requirement can be bypassed with the --no-prune
option, but it is not recommended.
- Install kubectl (requires v1.7.0 or higher) and make sure it is available in your $PATH
- Set up your kubeconfig file for access to your cluster(s).
gem install kubernetes-deploy
kubernetes-deploy <app's namespace> <kube context>
Environment variables:
$REVISION
(required): the SHA of the commit you are deploying. Will be exposed to your ERB templates ascurrent_sha
.$KUBECONFIG
(required): points to one or multiple valid kubeconfig files that include the context you want to deploy to. File names are separated by colon for Linux and Mac, and semi-colon for Windows.$ENVIRONMENT
: used to set the deploy directory toconfig/deploy/$ENVIRONMENT
. You can use the--template-dir=DIR
option instead if you prefer (one or the other is required).$GOOGLE_APPLICATION_CREDENTIALS
: points to the credentials for an authenticated service account (required if your kubeconfiguser
's auth provider is GCP)
Options:
Refer to kubernetes-deploy --help
for the authoritative set of options.
--template-dir=DIR
: Used to set the deploy directory. Set$ENVIRONMENT
instead to useconfig/deploy/$ENVIRONMENT
.--bindings=BINDINGS
: Makes additional variables available to your ERB templates. For example,kubernetes-deploy my-app cluster1 --bindings=color=blue,size=large
will exposecolor
andsize
.--no-prune
: Skips pruning of resources that are no longer in your Kubernetes template set. Not recommended, as it allows your namespace to accumulate cruft that is not reflected in your deploy directory.--max-watch-seconds=seconds
: Raise a timeout error if it takes longer than seconds for any resource to deploy.
Each app's templates are expected to be stored in a single directory. If this is not the case, you can create a directory containing symlinks to the templates. The recommended location for app's deploy directory is {app root}/config/deploy/{env}
, but this is completely configurable.
All templates must be YAML formatted. You can also use ERB. The following local variables will be available to your ERB templates by default:
current_sha
: The value of$REVISION
deployment_id
: A randomly generated identifier for the deploy. Useful for creating unique names for task-runner pods (e.g. a pod that runs rails migrations at the beginning of deploys).
You can add additional variables using the --bindings=BINDINGS
option which can be formated as comma separated string, JSON string or path to a JSON or YAML file. Complex JSON or YAML data will be converted to a Hash for use in templates. To load a file the argument should include the relative file path prefixed with an @
sign. An argument error will be raised if the string argument cannot be parsed, the referenced file does not include a valid extension (.json
, .yaml
or .yml
) or the referenced file does not exist.
# Comma separated string. Exposes, 'color' and 'size'
$ kubernetes-deploy my-app cluster1 --bindings=color=blue,size=large
# JSON string. Exposes, 'color' and 'size'
$ kubernetes-deploy my-app cluster1 --bindings='{"color":"blue","size":"large"}'
# Load JSON file from ./config
$ kubernetes-deploy my-app cluster1 --bindings='@config/production.json'
# Load YAML file from ./config (.yaml or .yml supported)
$ kubernetes-deploy my-app cluster1 --bindings='@config/production.yaml'
kubernetes-deploy
supports composing templates from so called partials in order to reduce duplication in Kubernetes YAML files. Given a template directory DIR
, partials are searched for in DIR/partials
and in 'DIR/../partials', in that order. They can be embedded in other ERB templates using the helper method partial
. For example, let's assume an application needs a number of different CronJob resources, one could place a template called cron
in one of those directories and then use it in the main deployment.yaml.erb like so:
<%= partial "cron", name: "cleanup", schedule: "0 0 * * *", args: %w(cleanup), cpu: "100m", memory: "100Mi" %>
<%= partial "cron", name: "send-mail", schedule: "0 0 * * *", args: %w(send-mails), cpu: "200m", memory: "256Mi" %>
Inside a partial, parameters can be accessed as normal variables, or via a hash called locals
. Thus, the cron
template could like this:
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: cron-<%= name %>
spec:
schedule: <%= schedule %>
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
containers:
- name: cron-<%= name %>
image: ...
args: <%= args %>
resources:
requests:
cpu: "<%= cpu %>"
memory: <%= memory %>
restartPolicy: OnFailure
Both .yaml.erb
and .yml.erb
file extensions are supported. Templates must refer to the bare filename (e.g. use partial: 'cron'
to reference cron.yaml.erb
).
Partials can be included almost everywhere in ERB templates, with one notable exception: you cannot use a partial to define a subset of fields. For example, given a partial p
defining two fields 'a' and 'b',
a: 1
b: 2
you cannot do this:
x: yz
<%= partial 'p' %>
hoping to get
x: yz
a: 1
b: 2
but you can do:
x:
<%= partial 'p' %>
or even
x: <%= partial 'p' %>
which both will result in
x:
a: 1
b: 2
This is a limitation of the current implementation.
kubernetes-deploy.shopify.io/timeout-override
: Override the tool's hard timeout for one specific resource. Both full ISO8601 durations and the time portion of ISO8601 durations are valid. Value must be between 1 second and 24 hours.- Example values: 45s / 3m / 1h / PT0.25H
- Compatibility: all resource types (Note:
Deployment
timeouts are based onspec.progressDeadlineSeconds
if present, and that field has a default value as of theapps/v1beta1
group version. Using this annotation will have no effect onDeployment
s that time out with "Timeout reason: ProgressDeadlineExceeded".)
kubernetes-deploy.shopify.io/required-rollout
: Modifies how much of the rollout needs to finish before the deployment is considered successful.- Compatibility: Deployment
full
: The deployment is successful when all pods in the newreplicaSet
are ready.none
: The deployment is successful as soon as the newreplicaSet
is created for the deployment.maxUnavailable
: The deploy is successful when minimum availability is reached in the newreplicaSet
. In other words, the number of new pods that must be ready is equal tospec.replicas
-strategy.RollingUpdate.maxUnavailable
(converted from percentages by rounding up, if applicable). This option is only valid for deployments that use theRollingUpdate
strategy.- Percent (e.g. 90%): The deploy is successful when the number of new pods that are ready is equal to
spec.replicas
* Percent.
To run a task in your cluster at the beginning of every deploy, simply include a Pod
template in your deploy directory. kubernetes-deploy
will first deploy any ConfigMap
and PersistentVolumeClaim
resources in your template set, followed by any such pods. If the command run by one of these pods fails (i.e. exits with a non-zero status), the overall deploy will fail at this step (no other resources will be deployed).
Requirements:
- The pod's name should include
<%= deployment_id %>
to ensure that a unique name will be used on every deploy (the deploy will fail if a pod with the same name already exists). - The pod's
spec.restartPolicy
must be set toNever
so that it will be run exactly once. We'll fail the deploy if that run exits with a non-zero status. - The pod's
spec.activeDeadlineSeconds
should be set to a reasonable value for the performed task (not required, but highly recommended)
A simple example can be found in the test fixtures: test/fixtures/hello-cloud/unmanaged-pod.yml.erb.
The logs of all pods run in this way will be printed inline.
Note: If you're a Shopify employee using our cloud platform, this setup has already been done for you. Please consult the CloudPlatform User Guide for usage instructions.
Since their data is only base64 encoded, Kubernetes secrets should not be committed to your repository. Instead, kubernetes-deploy
supports generating secrets from an encrypted ejson file in your template directory. Here's how to use this feature:
- Install the ejson gem:
gem install ejson
- Generate a new keypair:
ejson keygen
(prints the keypair to stdout) - Create a Kubernetes secret in your target namespace with the new keypair:
kubectl create secret generic ejson-keys --from-literal=YOUR_PUBLIC_KEY=YOUR_PRIVATE_KEY --namespace=TARGET_NAMESPACE
- (optional but highly recommended) Back up the keypair somewhere secure, such as a password manager, for disaster recovery purposes.
- In your template directory (alongside your Kubernetes templates), create
secrets.ejson
with the format shown below. The_type
key should have the value “kubernetes.io/tls” for TLS secrets and “Opaque” for all others. Thedata
key must be a json object, but its keys and values can be whatever you need.
{
"_public_key": "YOUR_PUBLIC_KEY",
"kubernetes_secrets": {
"catphotoscom": {
"_type": "kubernetes.io/tls",
"data": {
"tls.crt": "cert-data-here",
"tls.key": "key-data-here"
}
},
"monitoring-token": {
"_type": "Opaque",
"data": {
"api-token": "token-value-here"
}
}
}
}
- Encrypt the file:
ejson encrypt /PATH/TO/secrets.ejson
- Commit the encrypted file and deploy as usual. The deploy will create secrets from the data in the
kubernetes_secrets
key.
Note: Since leading underscores in ejson keys are used to skip encryption of the associated value, kubernetes-deploy
will strip these leading underscores when it creates the keys for the Kubernetes secret data. For example, given the ejson data below, the monitoring-token
secret will have keys api-token
and property
(not _property
):
{
"_public_key": "YOUR_PUBLIC_KEY",
"kubernetes_secrets": {
"monitoring-token": {
"_type": "kubernetes.io/tls",
"data": {
"api-token": "EJ[ENCRYPTED]",
"_property": "some unencrypted value"
}
}
}
kubernetes-restart
is a tool for restarting all of the pods in one or more deployments. It triggers the restart by touching the RESTARTED_AT
environment variable in the deployment's podSpec. The rollout strategy defined for each deployment will be respected by the restart.
Option 1: Specify the deployments you want to restart
The following command will restart all pods in the web
and jobs
deployments:
kubernetes-restart <kube namespace> <kube context> --deployments=web,jobs
Option 2: Annotate the deployments you want to restart
Add the annotation shipit.shopify.io/restart
to all the deployments you want to target, like this:
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: web
annotations:
shipit.shopify.io/restart: "true"
With this done, you can use the following command to restart all of them:
kubernetes-restart <kube namespace> <kube context>
kubernetes-run
is a tool for triggering a one-off job, such as a rake task, outside of a deploy.
- You've already deployed a
PodTemplate
object with fieldtemplate
containing aPod
specification that does not include theapiVersion
orkind
parameters. An example is provided in this repo intest/fixtures/hello-cloud/template-runner.yml
. - The
Pod
specification in that template has a container namedtask-runner
.
Based on this specification kubernetes-run
will create a new pod with the entrypoint of the task-runner
container overridden with the supplied arguments.
kubernetes-run <kube namespace> <kube context> <arguments> --entrypoint=/bin/bash
Options:
--template=TEMPLATE
: Specifies the name of the PodTemplate to use (default istask-runner-template
if this option is not set).--env-vars=ENV_VARS
: Accepts a comma separated list of environment variables to be added to the pod template. For example,--env-vars="ENV=VAL,ENV2=VAL2"
will makeENV
andENV2
available to the container.
If you work for Shopify, just run dev up
, but otherwise:
- Install kubectl version 1.7.0 or higher and make sure it is in your path
- Install minikube (required to run the test suite)
- Check out the repo
- Run
bin/setup
to install dependencies
To install this gem onto your local machine, run bundle exec rake install
.
- Start minikube (
minikube start [options]
) - Make sure you have a context named "minikube" in your kubeconfig. Minikube adds this context for you when you run
minikube start
; please do not rename it. You can check for it usingkubectl config get-contexts
. - Run
bundle exec rake test
To see the full-color output of a specific integration test, you can use PRINT_LOGS=1 bundle exec ruby -I test test/integration/kubernetes_deploy_test.rb -n/test_name/
.
To make StatsD log what it would have emitted, run a test with STATSD_DEV=1
.
- Make sure all merged PRs are reflected in the changelog before creating the commit for the new version.
- Update the version number in
version.rb
and commit that change with message "Version x.y.z". Don't push yet or you'll confuse Shipit. - Tag the version with
git tag vx.y.z -a -m "Version x.y.z"
- Push both your bump commit and its tag simultaneously with
git push origin master --follow-tags
(note that you can setgit config --global push.followTags true
to turn this flag on by default) - Use the Shipit Stack to build the
.gem
file and upload to rubygems.org.
If you push your commit and the tag separately, Shipit usually fails with You need to create the v0.7.9 tag first.
. To make it find your tag, go to Settings
> Resynchronize this stack
> Clear git cache
.
Please make sure you run the tests locally before submitting your PR (see Running the test suite locally). After reviewing your PR, a Shopify employee will trigger CI for you.
Go to the kubernetes-deploy-gem pipeline and click "New Build". Use branch external_contrib_ci
and the specific sha of the commit you want to build. Add BUILDKITE_REFSPEC="refs/pull/${PR_NUM}/head"
in the Environment Variables section.
Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/kubernetes-deploy.
Contributions to help us support additional resource types or increase the sophistication of our success heuristics for an existing type are especially encouraged! (See tips below)
The list of fully supported types is effectively the list of classes found in lib/kubernetes-deploy/kubernetes_resource/
.
This gem uses subclasses of KubernetesResource
to implement custom success/failure detection logic for each resource type. If no subclass exists for a type you're deploying, the gem simply assumes kubectl apply
succeeded (and prints a warning about this assumption). We're always looking to support more types! Here are the basic steps for contributing a new one:
- Create a the file for your type in
lib/kubernetes-deploy/kubernetes_resource/
- Create a new class that inherits from
KubernetesResource
. Minimally, it should implement the following methods:sync
-- Gather the data you'll need to determinedeploy_succeeded?
anddeploy_failed?
. The superclass's implementation fetches the corresponding resource, parses it and stores it in@instance_data
. You can define your own implementation if you need something else.deploy_succeeded?
deploy_failed?
- Adjust the
TIMEOUT
constant to an appropriate value for this type. - Add the a basic example of the type to the hello-cloud fixture set and appropriate assertions to
#assert_all_up
inhello_cloud.rb
. This will get you coverage in several existing tests, such astest_full_hello_cloud_set_deploy_succeeds
. - Add tests for any edge cases you foresee.
Everyone is expected to follow our Code of Conduct.
The gem is available as open source under the terms of the MIT License.