Deno Scripts for Server Provisioning

Ever since Deno was announced at JSConf EU 2018, I’ve been following its progress closely. Normally I advocate writing software in compiled languages with static typing. But there are valid use cases for interpreted languages.

One of those use cases is provisioning servers. I’m talking about installing packages, making directories, changing permissions, adding users, etc. There’s a reason that every successful configuration management framework is written in Python or Ruby. And there’s a reason that ops personnel still write boatloads of bash scripts. It’s simply not reasonable to require people to compile binaries on target systems and then run those binaries to provision those systems. The toolchains are too hard to set up. And even when the toolchain is fairly easy (like Go’s), a lot of extra questions that arise when you’re compiling code. Where do the artifacts live? Where do my depedencies come from? Am I binary compatible with the target platform? Do I have native libraries all sorted out? Something about it just feels wrong.

So we write gnarly bash instead, if we want maximum compatibility. Or, we target an older version of Python that we can reasonably expect to be there (Ansible). Or, we distribute a half dozen packages for the operator to install on the target machine to “bring it under management” (Chef, SaltStack). It seems like we have to choose between bash, a language that’s hard to write (and has no nice features or true library support), or Ruby and Python.

What’s wrong with Ruby and Python? They’re still too hard to install. The language runtimes themselves are widely available, but packaging and distributing your own code is painful. Especially when compared to the experience of building and distributing Go and Rust code.

The Deno project offers the promise of an interpreted language runtime that solves these problems. First, it’s distributable as a single binary, so installation is pretty easy (as long as Deno was compiled for your target). Furthermore, a Deno program can fetch and compile its own dependencies from the network. An import in Deno can be a URL, which is a great feature that makes it easy to distribute code. And if you’re paranoid about fetching code from the internet and executing it directly on target machines, it’s possible to precompile your scripts, and they will run on your target environment just fine.

A note about that compilation process. Deno natively supports compiling TypeScript, an interpreted language that has an advanced type system. I’ve always liked TypeScript, but setting up the build process (there’s that toolchain problem again!) has always been a bit too much for me. Deno promises to fix that. The Deno binary has the TypeScript compiler built in. Just point deno run at a TypeScript file, and it will follow all imports, fetch dependencies, and compile the code to executable JavaScript. Not to mention TypeScript is a nice language. The compiler is fast, and features like generics and interfaces allow a level of robust library authorship that’s been missing in dynamic language that executes on the server.

So how can we use TypeScript (via Deno) to do the server setup tasks that we use bash for? We have two prerequisites:

  1. Ship the Deno binary executable to the target environment, and
  2. Ship our TypeScript code to the target environment

To that end, I’ve hacked out a Packer plugin to do these two steps for you. There are still some limitations, but if you’re interested in running TypeScript with the godlike power of root, check it out. If you have a cloud account that Packer has a builder for, you can play with provisioning scripts on some throwaway VMs with minimal effort.

Over time, I hope that libraries for provisioning will emerge that will make it as easy to provision servers with Deno/TypeScript as it is with Chef and SaltStack. And the programming paradigms that TypeScript enables will make those libraries more composable.