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.
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.
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:
- Docker and a local Kubernetes cluster (you can also point Draft at an external cluster).
- A working Helm installation on the cluster.
- A local
kubectlbinary.
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-helmThen we need to initiate Helm on our cluster.
$ helm initThis 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-cliWe can then test kubectl is installed.
$ kubectl versionClient 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/draftWe 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 initInstalling 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 Speed100 3270k 100 3270k 0 0 5973k 0 --:--:-- --:--:-- --:--:-- 5967kPreparing to install into /Users/james/.draft/plugins/draft-pack-repodraft-pack-repo installed into /Users/james/.draft/plugins/draft-pack-repo/draft-pack-repoInstalled plugin: pack-repoInstallation of default plugins completeInstalling default pack repositories...Installing pack repo from https://github.com/Azure/draftInstalled pack repository github.com/Azure/draftInstallation 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 -laREADME.md mateHere 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$ lsGemfile Gemfile.lock mate.rbAnd 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 sailWoot! Looks like it correctly identified our application as Ruby-based. Let’s see what it did.
$ lsDockerfile charts draft.toml Gemfile mate.rbWe 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 rubyENV PORT 3000EXPOSE 3000RUN 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.
$ lsChart.yaml charts templates values.yamlHelm 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: 1image: pullPolicy: IfNotPresentservice: name: ruby type: ClusterIP externalPort: 80 internalPort: 3000resources: limits: cpu: 100m memory: 128Mi requests: cpu: 100m memory: 128Miingress: enabled: falseWe 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 upWARNING: 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': 01DJBX8D54VNF9C07TQHHVZBTVmate: Building Docker Image: SUCCESS ⚓ (1.0005s)mate: Releasing Application: SUCCESS ⚓ (2.5179s)Inspect the logs with `draft logs 01DJBX8D54VNF9C07TQHHVZBTV`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 podsNAME READY STATUS RESTARTS AGEmate-mate-c85b5ff84-lkltn 1/1 Running 0 65mCurrently 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 connectConnect 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=3000We 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/mateMate!We can also indicate a specific port for Draft to use.
$ draft connect -p 3001:3000Would 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"]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!"endWe then run draft up again to deploy our new application.
$ draft upDraft Up Started: 'mate': 01DJES911H79M41TPXS2JCCWC4mate: 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:3000Connect 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=3000And then check the new endpoint:
$ curl localhost:3001/folksFolks!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 deleteSummary
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.