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 that 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 Environment.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, Environment.Config} Nat
wordCountService totalCountTable text =
  size = (List.size (Text.split ?\s text))
  isDebug = Optional.exists (value -> value  === "true") (Environment.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.named "myServiceEnvironment"
  (IO.getEnv envVar) |> Environment.setValue env "debugMode"
  database = Database.named "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.serve.

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.serve 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.named "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

For more tips and caveats on local development, check out our local development FAQs.