Local development with Unison Cloud

You can develop and test your application locally before ever deploying it on Cloud infrastructure. Changing the deployment environment is often as simple as swapping Cloud.main in place of one of its Cloud.main.local handler variants, while the code which describes your infrastructure remains exactly the same.

A service with environment-dependent resources

Here's a Unison native service which splits the given text argument into a list and records the total words seen over time in a table. It involves a few resources that are frequently different per environment:

  • Secure Config values like API keys or feature flags may vary between environments
  • Log outputs might be more verbose for debugging
  • Cell table data would only be persistently stored in the Cloud
wordCountService : Cell Nat -> Text -> {Remote, Log, Config} Nat
wordCountService totalCountTable text =
  size = (List.size (Text.split ?\s text))
  isDebug = Optional.exists (value -> value  === "true") (Config.lookup "debugMode")
  logger = if isDebug then Log.debug else Log.info
  logger "adding-message" [("size", (Nat.toText size))]
  Cell.modify totalCountTable (cases current ->
      total = current + size
      (total, total)

Describe your service resources

You can defer the decision for which infrastructure to run your service on, even as you describe the kinds of resources that your service requires. This service needs an Environment to group together the service, database, and secure config variables; a Database to house the table for saving the total value; and a call to the deploy function to host the service.

cloud.deploy.impl : Text -> {Exception, Cloud, IO} ServiceHash Text Nat
cloud.deploy.impl envVar =
  env = Environment.create "myServiceEnvironment"
  (IO.getEnv envVar) |> Environment.setValue env "debugMode"
  database = Database.create "myServiceDatabase"
  Database.assign  database env
  totalCountTable = Cell.named database "totalCount" 0
  deploy env (wordCountService totalCountTable)

The function keeps the {Cloud} ability around in the signature because the choice of whether to run the service locally or on the Cloud is still up in the air.

Local and prod deployments

We've pushed the environment-dependent elements of the deployment process to the edge of our runnable programs. The Unison Cloud infrastructure deployment of this service wraps the implementation with Cloud.main, while a local deployment will Cloud.main.local or Cloud.main.local.interactive.

cloud.deploy.prod : '{IO, Exception} ServiceHash Text Nat
cloud.deploy.prod = Cloud.main do (deploy.impl "CONFIG_KEY")

Interactive local testing of Http or WebSockets services is best handled by the Cloud.main.local.interactive handler, since it will wait for a user to enter a newline to stop the service. Our local service test can just use Cloud.main.local since we don't want to interactively call our service once it's hosted. Instead, inside the same Cloud.main.local scope, let's issue a few test requests using the ServiceHash returned from the deployment function.

cloud.deploy.local : '{IO, Exception} Nat
cloud.deploy.local =
  Cloud.main.local do
    serviceHash = deploy.impl "LOCAL_CONFIG_KEY"
    testEnv = Environment.create "testEnvironment"
    Cloud.submit testEnv do
      _ = Services.call serviceHash "Hello, world!"
      Services.call serviceHash "Wow, another request."

Note that the Environment in which you run your requests to the service does not need to be the same as the one you created for running the service.

Deploy with run in the UCM

There's no executable files to upload or lengthy build pipelines in the way of deploying to prod; just issue the UCM run command for the deployments to kick things off in both environments.

myProject/main> run cloud.deploy.prod
myProject/main> run cloud.deploy.local

Tips and Caveats

Arbitrary IO and Debug.trace statements are not supported in production on the Cloud

For security reasons, functions which run arbitrary IO (like printLine) and some debugging functions like Debug.trace are not available in the Cloud, so you should avoid using them in your production code. Instead, use the Log ability to log messages to the Cloud's logging system. There are some handy functions in the Log ability for logging messages at different levels, like Log.info, Log.warn, and Log.error.

👉 Read more about the Log ability here.

Test web-services locally with the Cloud.main.local.interactive handler

If you would like to run a local service and keep it around for testing with CURL or the browser, you should use the Cloud.main.local.interactive version of the local cloud handler. The Cloud.main.local handler will exit immediately after starting the service, so it is primarily useful for Unison-native services, one-shot unit tests of services, or compute jobs.

👉 Write an HTTP service and interact with it locally

The Environment type does not just mean "prod" vs "local"

It's easy to think of the Environment type as a flag for determining "Prod" vs "Staging" vs "Local". Instead, think of an Environment as describing a "configuration environment". While your Config values might indeed be different between different environments, the Environment type is a broad way of scoping configuration values and resources like databases and services to a particular context.

👉 Read more about using Environments in the Cloud

You cannot call a deployed Unison native service in production from a local test

Local testing of deployed Http services is easily supported via regular Http requests, but services that are dependent upon the Cloud's own serialization layer cannot be called across deployment boundaries. You can always run and call your native service locally or plan on deploying your native service to the Cloud and write a separate Cloud job that issues a test request against it.

👉 The Services ability describes native service interactions