news
Oct 12, 2023

Unison Cloud - our month in testing

Rebecca Mark

Since our earlier preview of what Cloud services might look like, we've made many of the speculative goals of cloud computing in Unison a reality. Over the past month, the Unison Cloud platform has been in testing with a small group of trailblazers.

Let's take a look at what you can expect when Unison Cloud enters general availability later this year:

Long-running http service support

With other platforms, the typical experience of deploying an HTTP service generally involves some out-of-band steps to get your code and its dependencies running "somewhere in the cloud". In Unison, you just call a function:

httpMain : '{IO, Exception} ServiceHash HttpRequest HttpResponse httpMain = Cloud.main do helloService : HttpRequest -> HttpResponse helloService request = HttpResponse.ok (Body (Text.toUtf8 "Hello world")) deployHttp Environment.default() helloService

Notice the deployHttp call. This uploads the code and any missing dependencies to Unison Cloud and starts the service running on our managed infrastructure. Services by default get assigned a unique content-addressed hash, but you can have service names which are assigned to these hashes, and this can be used for easy rollback or conditional "promote to production".

Unlike other platforms, there's no separate packaging step, no building containers or uploading them somewhere or other, no YAML files or weird configuration languages to specify how deployment should happen. Deployment of services and other management tasks are done with ordinary typed Unison code which you can factor however you like.

When using Unison Cloud, you get to focus on the actual business logic of your services, not a mess of plumbing and cloud infra management.

How is it so simple? In Unison, arbitrary values (including functions and code) may be serialized and sent over the network, and Unison has an approach to dynamic code loading that avoids the possibility of dependency conflicts. We build on these features to make Unison Cloud work.

Durable transactional storage

Rather than needing a tedious layer of boilerplate between your application and the storage layer, you can persist arbitrary Unison values to typed Unison-native storage:

simpleStorage :
  Database -> Table Text Nat -> '{Exception, cloud.Storage, Cloud} ()
simpleStorage db table = do
  use Nat +
  use Transaction write.tx
  key = "myKey"
  transact db do match Transaction.tryRead.tx table key with
    Optional.None -> write.tx table key 1
    Some count    -> write.tx table key (count + 1)

The storage layer has typed tables and transactions that operate on these tables. Tables can be used directly, or as building blocks for all more interesting durable data structures: queues, sorted maps, search indexes, and more.

This works via the same magic that powers the dynamic code deployment. Since arbitrary Unison values may be persisted and then unpersisted without dependency conflicts, we can eliminate the layer of manual encoding and decoding to SQL or whatever other storage layer and just store values directly.

Typed inter-service communication

Calls between services often involve boilerplate for serializing and deserializing data models, and it can be a pain to verify schemas or keep multiple service versions in sync with each other. In Unison, service-to-service communication can be expressed as regular function calls whose arguments and return values are checked at compile time. Check out the signature of the Services.call function:

The Services.call function's argument must match the input type represented in the ServiceHash. Moreover, because Unison types are identified by their hash, not just their name, we’re assured that the UserModel used by the caller and the service are the exact same version.

Say you don't have or care about the unique ServiceHash of the service you're calling, but like most clients, you know the overall service name. callName lets you perform typed service calls by their name because the Unison Cloud handles resolving the service name to the particular ServiceHash which is currently registered to it.

Logging and log viewing

While this isn't the most earth-shattering feature, service logs are easily viewable in the Unison Cloud UI. You can also you can tail logs to your local terminal with a single function call.

Log messages are arbitrary JSON and we include a number of convenience functions for logging messages for instance:

warn : Text -> [(Text, Text)] ->{Log} ()
warn.json : Text -> Json ->{Log} ()

The use of abilities here also makes it easy to intercept and send log messages to whatever external log aggregator you like.

Secrets and config management

Services and jobs have access to config environments via the Config ability. Config values are encrypted and your program can only access them if it has the appropriate permissions.

setConfig : Text ->{IO, Exception} Text
setConfig apiKeyValue = Cloud.run do
  cloudEnv = Environment.default()
  setValue cloudEnv "api-key" apiKeyValue
  Cloud.submit cloudEnv do Config.expect "api-key"

... and we're just getting started

In the coming months, we have a series of additions planned, including:

  • A entire suite of durable data structures for use in your applications
  • WebSocket-based services
  • Scheduled jobs
  • Asset management support for full-stack web applications
  • ... and more

Curious? Connect with us!

If you like the sound of this, head over to the newly revamped Unison Cloud website and sign up to request early access or be notified of launch.

In the meantime you can also brush up on your Unison programming skills or ask us your questions in our welcoming Slack community.

We'll also be at Scale by the Bay showing a live demo of all this.

The Cloud is coming soon and we can't wait to share it with you!

🙏
Thank you to our Cloud Trailblazers who have helped test out the platform, reported bugs, given feedback, and discovered new practices and usage patters while writing their applications. We really appreciate it!