Draft

I’ve been playing with a lot of development work flow tools over the last few weeks. I was an early adopter of Docker and have always been passionate about reducing friction in the development process, especially locally. Quick performance wins for individual developers add up to substantial productivity improvements across a whole team. I’ve been interested in how these tools are evolving, as we expand beyond the basic tools Docker provided with containers. As an outcome of this exploration I’m going to write a few posts on these tools.

note
These posts are explorations of each solution, not comparisons or a pros and cons analysis. I think everyone’s development platform and experience is sufficiently different that they should explore solutions and identify which works for them. Additionally, in many cases, these solutions might not fit at all and something like Docker Compose might address your needs.

The posts:

Draft 🔗︎

Draft is developed by Microsoft and:

makes it easy to build applications that run on Kubernetes. Draft targets the “inner loop” of a developer’s workflow: as they hack on code, but before code is committed to version control.

Draft is designed to handle local development, allowing a developer to make changes, test, and resolve issues, before committing your code and letting your CI/CD pipeline take over for deployment. To help reduce the friction of this deployment, Draft produces Helm-compatible artifacts.

Draft has a couple of neat features, the first is automatically detecting and bootstrapping your application. IT can peek at your code and help you generate the right configuration and Helm charts files to run your application in Kubernetes. This includes generating Dockerfile’s and Kubernetes configuration files.

The second feature is at the heart of several of these products, the ability to do quickly re-deploy your applications when they change. Draft doesn’t quite automatically deploy changes. Rather, the approach is to rely on your IDE, there’s an integration for VS Code and KUbernetes that provides this capability.

You can read more about Draft’s architecture and what it is, and is not, in their design documentation.

warning
It’s unclear to me how well Draft is maintained. I found a lot of weirdness in the documentation and what seemed like a lot of undocumented change. There’s also a lot of open issues and pull requests. It’s a neat tool but I’d check the status of the project before committing to it.

So let’s get started by installing Draft locally and running it.

Installing Draft 🔗︎

We’re going to install on macOS but the documentation has installation instructions for Linux and Windows too. There are several prerequisites for Draft:

  1. Docker and a local Kubernetes cluster (you can also point Draft at an external cluster).
  2. A working Helm installation on the cluster.
  3. A local kubectl binary.

Let’s get these sorted first.

Installing Kubernetes 🔗︎

First, we will need to install Docker and a local Kubernetes environment. One of the easiest ways to do this locally is to use Docker for Mac, which installs both. You could also install something like Minikube. Install one or the other and initiate a local Kubernetes cluster, either enabling it in Docker for Mac, or initiating and building a Minikube cluster.

Installing Helm 🔗︎

We then need to install Helm, a package manager for Kubernetes, that is used to install packages, called charts, on Kubernetes clusters. We first install a local helm binary. We’re going to use Homebrew for this but installation is also available for other platforms.

$ brew install kubernetes-helm

Then we need to initiate Helm on our cluster.

$ helm init

This will install Tiller, Helm’s server component, onto our Kubernetes cluster.

Installing kubectl 🔗︎

We’ll also need the kubectl binary, which is a CLI for Kubernetes. There’s documentation to install and configure it on several platforms but we’ll use the Homebrew recipe.

$ brew install kubernetes-cli

We can then test kubectl is installed.

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.1", GitCommit:"4485c6f18cee9a5d3c3b4e523bd27972b1b53892", GitTreeState:"clean", BuildDate:"2019-07-18T14:25:20Z", GoVersion:"go1.12.7", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.3", GitCommit:"5e53fd6bc17c0dec8434817e69b04a25d8ae0ff0", GitTreeState:"clean", BuildDate:"2019-06-06T01:36:19Z", GoVersion:"go1.12.5", Compiler:"gc", Platform:"linux/amd64"}

If you’ve set up Minikube or Docker for Mac already then it’ll have configured kubectl to connect to your local cluster, otherwise you’ll need to configure it.

Installing Draft 🔗︎

Next, we need to install Draft itself. We can either download a binary for our preferred environment, or use a tool like Homebrew or Chocolatey. As we’re on MacOS, we’re going to again use Homebrew.

$ brew install azure/draft/draft

We can then test it is working like so:

$ draft version
&version.Version{SemVer:"v0.16.0", GitCommit:"5433afea1421810ae9d828631d8651de913b347a", GitTreeState:"dirty"}

Configure Draft 🔗︎

After we’ve installed Draft, we need to configure it. We do this with the draft init command.

$ draft init
Installing default plugins...
Downloading https://azuredraft.blob.core.windows.net/draft/pack-repo-v0.4.2-darwin-amd64.tar.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 3270k  100 3270k    0     0  5973k      0 --:--:-- --:--:-- --:--:-- 5967k
Preparing to install into /Users/james/.draft/plugins/draft-pack-repo
draft-pack-repo installed into /Users/james/.draft/plugins/draft-pack-repo/draft-pack-repo
Installed plugin: pack-repo
Installation of default plugins complete
Installing default pack repositories...
Installing pack repo from https://github.com/Azure/draft
Installed pack repository github.com/Azure/draft
Installation of default pack repositories complete
$DRAFT_HOME has been configured at /Users/james/.draft.
Happy Sailing!

We can see we’ve downloaded some packs. Packs are what informs Draft about the configuration of applications written in various programming languages. Using packs, Draft knows how to build Dockerfile’s, Helm charts, including Kubernetes manifests, to build and deploy applications written in languages like Ruby, Python, Java, and Go, etc. Packs are extensible too, so if you have a language that isn’t supported, or a tweak or adjustment specific to your environment or implementation you can modify packs to suit.

We can also see Draft has configured its home directory in ~/.draft.

We’re now ready to deploy an application with Draft.

Deploying applications with Draft 🔗︎

Let’s test how well Draft can recognize an application. We’re going to use a Draft example we’ve pre-built to take you through the process. You can find it on Github.

Let’s checkout our project.

$ git clone git@github.com:jamtur01/draft-example.git
$ cd draft-example
$ ls -la
README.md  mate

Here we’ve checked out the project, changed into the resulting directory, and listed its contents. We have a README for the project, and a directory called mate, that contains a service we’re going to test with Draft. The mate service is a Sinatra-based Ruby application.

Configuring the mate service 🔗︎

Let’s see if Draft can recognize and deploy the mate service. We change into the mate directory which contains our Ruby application.

$ cd mate
$ ls
Gemfile Gemfile.lock mate.rb

And then run draft create to see if Draft can identify and build a scaffold for our application.

$ draft create
--> Draft detected Ruby (78.378378%)
--> Ready to sail

Woot! Looks like it correctly identified our application as Ruby-based. Let’s see what it did.

$ ls
Dockerfile charts draft.toml Gemfile mate.rb

We can see it created a Dockerfile, a directory called charts, and a file called draft.toml. The Dockerfile will help us build our Ruby-based image, the charts directory contains a Helm chart that Draft has created for our application, and the draft.toml contains Draft’s defintion of our application. Let’s start by looking at the Dockerfile.

FROM ruby
ENV PORT 3000
EXPOSE 3000
RUN bundle config --global frozen 1

WORKDIR /usr/src/app

COPY Gemfile Gemfile.lock ./
RUN bundle install

COPY . .
CMD ["ruby", "app.rb"]

Hmmmm… Looks pretty good. Almost correct, our application is called mate.rb and not app.rb but we can edit that to reflect our environment. Let’s look into the draft.toml file.

[environments]
  [environments.development]
    name = "mate"
    namespace = "default"
    wait = true
    watch = false
    watch-delay = 2
    auto-connect = false
    dockerfile = "Dockerfile"
    chart = ""

The drafts.toml file contains our application’s definition. It uses TOML and contains blocks of environments. Environments allow you to specify different running conditions for different purposes, for example how you configure your development environment might differ from how you create a test or staging environment. You can specify what environment Draft should deploy at runtime.

We can see we’ve only got one environment defined: environments.development. It defines the name of our application, mate, and the Kubernetes namespace into which it should be deployed, in our case the ever-popular default.

We have also set the wait option to true, which ensures Draft will wait for all resources to be ready before it installs your application. The watch option controls whether Draft should monitor for local file changes and automatically deploy them when they occur. The default is false, and as mentioned above this functionality doesn’t currently work. Instead we’d enable monitor for changes in our IDE and re-deploy based on that.

The auto-connect option controls whether Draft should automatically connect to the application after the deployment is successful.

The dockerfile option controls the name of the Dockerfile that will be used to build the image. The chart option controls the path of the chart, relative to the location of the draft.toml file, which will deploy the application. If nothing is specified then it’ll use the first directory it finds in the charts/ directory.

You can find the full specification for the draft.toml file in the Draft documentation.

Let’s take a quick look at the charts directory tree before we launch our application. Inside the charts directory we have a directory called mate, containing the generated Helm chart for our application.

$ ls
Chart.yaml  charts      templates   values.yaml

Helm charts are templated. The easiest way to see what Helm will do when installing and configuring the mate application is to look at the template values in the values.yaml file.

replicaCount: 1
image:
  pullPolicy: IfNotPresent
service:
  name: ruby
  type: ClusterIP
  externalPort: 80
  internalPort: 3000
resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 100m
    memory: 128Mi
ingress:
  enabled: false

We can see some basic network configuration, mapping port 3000 inside the application, to port 80 externally via a ClusterIP. And we’ve got some basic memory and CPU resources controls.

Now let’s see if we can deploy the mate service.

Deploying the mate service 🔗︎

Draft deploys applications using the draft up command.

$ draft up
WARNING: no registry has been set, therefore Draft will not push to a container registry. This can be fixed by running `draft config set registry docker.io/myusername`
Hint: this warning can be disabled by running `draft config set disable-push-warning 1`
Draft Up Started: 'mate': 01DJBX8D54VNF9C07TQHHVZBTV
mate: Building Docker Image: SUCCESS ⚓  (1.0005s)
mate: Releasing Application: SUCCESS ⚓  (2.5179s)
Inspect the logs with `draft logs 01DJBX8D54VNF9C07TQHHVZBTV`

tip
You can ignore the warning, it just means a container registry is not set and your images won’t automatically be pushed to a registry.

Draft has used the Dockerfile it generated to build an image and then deployed it to our local Kubernetes cluster via Helm. The log output will show the output of that build.

We can see our running application in the cluster using kubectl.

$ kubectl get pods
NAME                        READY   STATUS    RESTARTS   AGE
mate-mate-c85b5ff84-lkltn   1/1     Running   0          65m

tip
As our deployment is a Helm chart, when we want to take the application to production, we package up the Helm chart we’ve created with helm package and deploy it.

Currently we have no way of reaching this service and testing it but Draft allows us to create local proxy tunnels that map ports and stream the services logs using the draft connect command. Let’s do that now.

$ draft connect
Connect to mate:3000 on localhost:58037
[mate]: [2019-08-16 15:08:33] INFO  WEBrick 1.4.2
[mate]: [2019-08-16 15:08:33] INFO  ruby 2.6.3 (2019-04-16) [x86_64-linux]
[mate]: == Sinatra (v2.0.5) has taken the stage on 3000 for development with backup from WEBrick
[mate]: [2019-08-16 15:08:33] INFO  WEBrick::HTTPServer#start: pid=1 port=3000

tip
You can run draft up with the --auto-connect flag to have Draft automatically connect to the application when the deployment completes. Or you can also set the auto-connect option in draft.toml to true.

We can see Draft has mapped port 3000 on our service to port 58037 on localhost. If we curl the API endpoint on this service we can see it in action.

$ curl localhost:58037/mate
Mate!

We can also indicate a specific port for Draft to use.

$ draft connect -p 3001:3000

Would set remote port 3000 to local port 3001. If you don’t want to do this every time you run draft connect, or you want to automatically connect, then you can use the override-ports option in the draft.toml file.

override-ports = ["3001:3000"]

tip
There’s more about this in the draft connect documentation, including options for mapping multiple ports.

Developing with Draft 🔗︎

So we’ve got our service running, now let’s make a change to it. We’re going to add a new endpoint to our service.

get '/folks' do
  "Folks!"
end

We then run draft up again to deploy our new application.

$ draft up
Draft Up Started: 'mate': 01DJES911H79M41TPXS2JCCWC4
mate: Building Docker Image: SUCCESS ⚓  (1.0011s)
mate: Releasing Application: SUCCESS ⚓  (2.1385s)
Inspect the logs with `draft logs 01DJES911H79M41TPXS2JCCWC4`

If we now connect to the application we should be able to curl our new endpoint.

$ draft connect -p 3001:3000
Connect to mate:3000 on localhost:3001
[mate]: [2019-08-17 03:17:36] INFO  WEBrick 1.4.2
[mate]: [2019-08-17 03:17:36] INFO  ruby 2.6.3 (2019-04-16) [x86_64-linux]
[mate]: == Sinatra (v2.0.5) has taken the stage on 3000 for development with backup from WEBrick
[mate]: [2019-08-17 03:17:36] INFO  WEBrick::HTTPServer#start: pid=1 port=3000

And then check the new endpoint:

$ curl localhost:3001/folks
Folks!

Woot! Now we can continue our development cycle by making changes and re-deploying.

Finally, when we’re done, we can shut down our Draft applications using:

$ draft delete

Summary 🔗︎

I hope this was useful as a walk through. This just scratches the surface of Draft’s capabilities and you can explore features and other uses cases in the Draft documentation.

comments powered by Disqus