Core Concepts

The Cloud deploys Unison code without the need for configuring infrastructure, build pipelines, or physical hardware. Instead, you'll interact with the Cloud via Unison code, guided by some conventions and core concepts.

If you prefer a hands-on approach first, learn as you go with this http service deployment tutorial.

Working with the Cloud

With the Unison Cloud, infrastructure management is unified with regular Unison application code and benefits from all the language features and tooling improvements that Unison offers.

Most Unison Cloud programs will start with a top-level function that calls Cloud.main or Cloud.main.local.serve, depending upon whether your service should be run on Cloud infrastructure or locally on your own machine. Within the Cloud.main block, you'll specify resources like databases and deployments or even orchestrate the interaction of multiple jobs and services at once.

main =
  Cloud.main do
    serviceHash1 = deploy !Environment.default service1
    serviceHash2 = deploy !Environment.default service2
    Cloud.submit !Environment.default do
      resultService1 = Services.call serviceHash1 myServiceInput
      Services.call serviceHash2 resultService1

Say you want to deploy a service which uses the Cloud's secure configuration store; that functionality, called Environment.Config, is included in a specific set of abilities which are supported by the Cloud. You can see them in functions that send your code to the Cloud for execution, like submit, or deploy:

deploy : Environment
  -> (a ->{Environment.Config, Exception, Storage, Http, Services, Remote, Random, Log, Scratch} b)
  ->{Exception, Cloud} ServiceHash a b

When working with the Cloud, remember that many of the features your service requires, like caching or logging or durable storage, might have a natively supported Unison solution represented by these abilities.

🏁 Experience the Cloud workflow with a quickstart deployment

Remote: the I/O of the Cloud

Think of the Remote ability as the “I/O of the Cloud.” It’s an ability which describes the runtime for a distributed system.

Remote handles forking arbitrary computations to different locations in a cluster and also handles some of the functionality that you might expect from I/O, like getting the current time or a random value.

Put simply, Remote describes a program being executed on other machines and the Cloud provides the compute pool for it. All computations that run in the Cloud use the Remote ability as their runtime, and the Cloud ability describes the infrastructure needed to deploy or submit such computations as services or batch jobs.

There are a number of additional abilities we provide to computations running in the Cloud besides those provided by the Remote interface. For example:

  • Http - issues http requests
  • Storage - provides persistent storage
  • Scratch - provides ephemeral storage
  • Services - allows you to make calls to other cloud services

Since we expect these abilities to be available anytime Remote is available, we provide a helper function, toRemote, that will run all of the other abilities provided by the Cloud in the Remote runtime.

toRemote : '{Environment.Config, Exception, Storage, Http, Services, Remote, websockets.HttpWebSocket, WebSockets, Random, Log, Scratch} a ->{Remote} a

📚 Learn more about the Remote ability

Deploying cloud services

You can think of Unison services as typed, versioned, Cloud-hosted functions.

These functions can be called remotely, from one cloud node to another, reaping all of the benefits of the type-system and composability without having to worry about serialization. If a service's type signature is one that matches one of a Http (or WebSockets) service, that service can be trivially exposed to the web.

Just like Unison code is identified by a hash of its contents, every deployed service is known by its ServiceHash. If you deploy the same code, you'll get the same hash—update your code and the Cloud will return a different hash representing the new version. Should you need a static name, you can assign a ServiceName to your service and always refer to the latest service deployment that way.

Unison Cloud's model means that calling hosted services is as easy as a local function call. Here's what it looks like to deploy and call a native service:

main =
  Cloud.main do
    aService = Nat.toText
    myService : ServiceHash Nat Text
    myService = deploy !Environment.default aService
    Cloud.submit !Environment.default do
      Services.call myService 2

🤖 Deploy a Slackbot as an Http service

Storing data on the Cloud

The most common storage types you'll see are: Database, Cell, and OrderedTable.

Cell and OrderedTable are different kinds of tables that can be used to store data in the Cloud. Cell is way of storing a single value and OrderedTable is a typed key-value store which provides a wider set of querying operations. A Database contains potentially many Cells, OrderedTables or other tables.

The creation of these Cloud storage resources is lightweight (you don't need to worry about a complex provisioning process) and idempotent (so you can rerun your deployments and not accidentally recreate your databases).

You'll be interacting with Cloud databases via the Storage and Transaction abilities. Storage is the general ability for interacting with durable storage—all updates or reads to data in the Cloud will use it. Transaction represents the reads and/or updates to the database that should be performed atomically. They're how you can keep two OrderedTables or Tables in sync.

🌳 Learn more about Cloud storage with our OrderedTable deep dive video

Cloud access management

When you deploy a service, you'll need to specify the Environment in which it will run. Environments provide a way to group Cloud resources together for basic access management. For example, only a service running in the same Environment as a Database can write or read from its data.

The Environment type is closely related to Environment.Config, which provides access to encrypted secrets at runtime. Load your secrets and configuration values into an Environment with Environment.setValue and then use the Environment.Config.lookup or Environment.Config.expect functions to use them in your service or job.

serviceLogic : Nat -> {Environment.Config, Exception} Text
serviceLogic input =
    key = Environment.Config.expect "apiKey"
    signRequest k = "signed_" ++ k
    (Nat.toText input) ++ (signRequest key)

main : '{IO, Exception} ServiceHash Nat Text
main =
  Cloud.main do
    env = Environment.named "myServiceEnvironment"
    (IO.getEnv "API_KEY") |> Environment.setValue env "apiKey"
    deploy env serviceLogic

📑 Read the API docs for the Environment type