Tips and Tricks for Writing the Perfect Acornfile

Jan 25, 2024 by Atulpriya Sharma
Tips and Tricks for Writing the Perfect Acornfile

Building and deploying applications to the cloud is more challenging than it looks. Multiple steps are involved in deploying an application on the cloud, from configurations and resource allocations to defining workloads. While these may look simple, they can sometimes challenge some of the most adept developers.

That’s where Acorn comes to the rescue. A cloud platform that brings back the simplicity of hosting applications. Acorn makes it super easy to work with applications, configurations, and workload specifications and even share your application with others.

At the heart of this is an Acornfile that helps you define your applications and workloads. In this blog post, we will show you how to write your first Acornfile and share some of the best practices you should follow while authoring Acornfiles for an optimized and efficient workflow.

What is an Acornfile?

What a Docker compose file is to Docker, Acornfile is to Acorn. A docker compose file has all the instructions to deploy and execute a complete application - services, images, network, volumes, secrets, etc.

An Acornfile is similar. It tells the Acorn runtime what the application comprises and how to deploy it to Acorn. You can start writing your Acornfile based on a docker file or a docker compose file.

For instance, here’s what an NGINX docker compose file looks like:

version: '3' services: nginx: image: nginx:latest ports: - "80:80"

Below is the corresponding Acornfile:

containers: web: { image: "nginx" ports: publish: "80/http” }

Like a docker compose file, an Acornfile has many fields that help you define your application. Let us see all the fields that make up an Acorn file.

Acornfile format

Many fields can be defined in an Acornfile to help you describe how to deploy your applications. Below is a brief overview of each of them:

  • Args: Set of user-configurable values that can be changed at build or run time.
  • Services: Definition of services that the Acorn app will consume.
  • Acorns: List of other Acorns that an app will consume.
  • Containers: List of containers to run.
  • Jobs: List of jobs to run.
  • Routers: Definition of HTTP routes for services.
  • Volumes: Definition of volumes that the Acorn will need to run.
  • Secrets: List of secrets that the Acorn will use.
  • localData: Any other information that can be embedded to render the Acornfile.

These Acornfiles also help you build Acorn images that are OCI compliant and contain all the configurations along with Docker images required to run your application. This image can be easily pushed to any container registry and deployed to Acorn.

Read more about authoring Acorn files for a more in-depth understanding.

How To Create An Acorn File?

It’s said the best way to learn something is by doing it, so why not learn more about Acornfiles by creating one?

Before you start with creating an Acornfile, make sure to sign up on Acorn as well as install the Acorn CLI on your system.

Building Your App

The first and foremost step is to build your application. It can be as simple as a “Hello World” app or as complex as a shopping cart.

Architecture diagram

We’ll create a simple student entry application allowing you to enter and store student details. The front end has a basic form to enter the details built using express.js, while the database is an instance of MariaDB.

Student entry application

You can check out the complete application code in this Git repo. Clone this repository, as we’ll be using it throughout this blog post.

Creating An Acornfile

Clone the repo to your local machine. Note that the repo already has an Acornfile. For this blog post, rename it to something else, create a new Acornfile using your favorite editor, and follow along.

App Container

The core of an Acornfile is the app container. It defines everything that an application requires to execute. It includes references to the images it uses, services it consumes and depends on, storage volume, the environment variables it needs to run, permission for various resources, etc.

This application is built from the Dockerfile in the

app
directory in the repository. In the dockerfile below, we see the Docker image is building a typical NodeJS app, and exposing port 3000.

# Use the official Node.js image as the base image FROM node:14 # Set the working directory in the container WORKDIR /usr/src/app # Copy package.json and package-lock.json to the working directory COPY package*.json ./ # Install the application dependencies RUN npm install # Copy the entire application code to the working directory COPY . . # Expose the application port (e.g. 3000) EXPOSE 3000 # Define the command to run the application CMD ["npm", "start"]

We will use this Dockerfile as the basis for creating our Acornfile. We will add a new

containers
field to the Acornfile and tell Acorn to build the Docker image and publish the port, as shown below. Using Acorn to build the Docker image is beneficial during both the development and deployment of the application. During development, as changes are made to the Acornfile and Dockerfile, the changes are reflected quickly without an extra build step.

name: “Student Entry App” containers: { app: { build: "./app" ports: "3000/http" } }

This defines the app container and instructs Acorn to build the image, and the port the app will be available on. With this basic definition the port 3000 is defined to only be accessible by other Acorns running in the same Acorn project.

In order to access the application from our web browser, we will “publish” the port. Publishing instructs Acorn to expose the port through a load balancer so end users can access the application.

Let's update the code to reflect our desired state:

name: “Student Entry App” containers: { app: { build: "./app" ports: publish: "3000/http" } }

Save this as an Acornfile and deploy it to Acorn using

acorn run -n school
.

If everything is successful, you’ll see Acorn return a URL for the application. When you access the application, you’ll see the form. However, nothing happens when you hit the Submit button. Why?

This is because we’ve not attached a database to our application. Let us see how to do that in the next step.

Adding our Database

Our application requires a database to store the students' records. The simplest approach is to use a MariaDB container. From the official Docker hub image, the MariaDB container requires the following environment variables.

MARIADB_ROOT_PASSWORD

Optionally, though recommended, a username, password, and database name.

MARIADB_USER MARIADB_PASSWORD MARIADB_DATABASE

Let's add the MariaDB container to our Acornfile:

name: "Student Entry App" containers: { app: { build: "./app" ports: publish: "3000/http" } mariadb: { image: “maridb:10.11.5” env: { MARIADB_ROOT_PASSWORD: "" MARIADB_USER: "" MARIADB_PASSWORD: "" MARIADB_DATABASE: "" } } }

In the above Acornfile, we have added a ‘mariadb’ container, and in this case, we are using the official Docker image for MariaDB.

When using a pre-built image, it is best to use a specific version vs. the “latest” or other moving tag. The reasoning is that later on, you will want to know which version you are running should something go wrong and you need to troubleshoot.

If you use a moving tag, the version will be baked into the Acorn image, so it will never change when using that Acorn image. Still, determining which version of the software was used at the time of the build later on can provide an extra step during a production outage.

Also, notice the use of the Environment variables in the Acornfile. These values need to contain passwords, so we want to handle them with care that we aren’t passing secret values around in plain text.

Secrets

The best way to handle passwords is the use of Acorn secrets. The benefit here is that if you just need a password for a development database, as we will in this Acornfile, Acorn will just generate one for you. Let us modify our Acornfile so that secret values are created for our database container. We will use a secret type “basic” for both the user and root password for the image. Adding the secrets, we will get:

name: "Student Entry App" containers: { app: { build: "./app" ports: publish: "3000/http" } mariadb: { image: “maridb:11.2.2” env: { MARIADB_ROOT_PASSWORD: "secret://admin/password" MARIADB_USER: "secret://user/username" MARIADB_PASSWORD: "secret://user/password" MARIADB_DATABASE: "app" } } } secrets: admin: { type: “basic” params: { usernameLength: 11 usernameCharacters: "a-z" passwordCharacters: "A-Za-z0-9^_-" } data: { username: “” password: “” } } secrets: user: { type: “basic” params: { usernameLength: 11 usernameCharacters: "a-z" passwordCharacters: "A-Za-z0-9^_-" } data: { username: “” password: “” } }

Looking at the Acornfile, we have two secrets that house the root and user credentials. When the values for the fields ‘username’ and ‘password’ are left as empty strings

””
Acorn will automatically generate them.

If you have credentials for the database already that you would like to use, you can create those before running your Acorn and then tell Acorn to use those credentials when you run. By having these secrets defined, we never have to pass “secret” data to the Acorn in clear text.

So, if we were to start this Acorn, we would see the two containers startup, but the app container is still not configured to talk to the database. We know that the application will need the following variables to connect to the database:

DB_NAME DB_USER DB_PASS DB_HOST

Now, we could do something like:

DB_USER: “secret://user/username”

Services

While this is valid, it would create a tight coupling within this Acorn. Say you wanted to use an external database when you go to production. You would have a tough time updating the DB_HOST settings. To help make this acorn more modular and composable, we will use a service to connect the “app” container to the database. Using a service, we can later use any Acorn that presents the same service by swapping it at runtime.

Let us modify our Acornfile to use a service and wire up our application container to the database.

name: "Student Entry App" services: db: { default: true container: "mariadb" secrets: ["admin", "user"] ports: "3306" data: dbName: “app” } containers: { app: { build: "./app" dependsOn: [“db”] env: { DB_HOST: “@{service.db.address}” DB_USER: “@{service.db.secrets.user.username}” DB_PASS: “@{service.db.secrets.user.password}” DB_NAME: “@{service.db.data.dbName}” ports: publish: "3000/http" } mariadb: { image: “maridb:11.2.2” env: { MARIADB_ROOT_PASSWORD: "secret://root/password" MARIADB_USER: "secret://user/username" MARIADB_PASSWORD: "secret://user/password" MARIADB_DATABASE: "app" } } } secrets: admin: { type: “basic” params: { usernameLength: 11 usernameCharacters: "a-z" passwordCharacters: "A-Za-z0-9^_-" } data: { username: “” password: “” } } secrets: user: { type: “basic” params: { usernameLength: 11 usernameCharacters: "a-z" passwordCharacters: "A-Za-z0-9^_-" } data: { username: “” password: “” } }

At this point, we tell the Acorn runtime to create an Acorn service using the MariaDB container provided, which will be referenced as db. We also added a “consumes: [db]” field to our application. This creates dependency for the app container on the service DB becoming available first.

We use “consumes” instead of “dependsOn” because, in some situations, permissions are passed along with the credentials. If we were only using containers without the service, we could use “dependsOn: [“db”].”

When you start this Acornfile it will run both the Mariadb container and the application container.

However, there will be no data or schema in our new database instance. Let us look at Jobs to see how we can address DB migrations.

Jobs

Jobs in Acorn are used to perform one-time tasks. These can be configured to execute based on a schedule or some events. Jobs are perfect for tasks like initializing a database, cleaning up files, etc.

In our case, let us add a “jobs” block and create a db-init job to initialize our database.

name: "Student Entry App" services:jobs: { dbinit: { build: "./db-init" env: { "DB_NAME": "@{service.db.data.dbName}" "DB_USER": "@{service.db.secrets.user.username}" "DB_PASS": "@{service.db.secrets.user.password}" "DB_HOST": "@{service.db.address}" } consumes: ["db"] } } containers: { app: { ...

We’ve added a job to initialize our database using the Dockerfile in the db-init directory. It will create the tables and enter some dummy data. Note that we’ve used the service mechanism to pass the credentials and connection information to the job, just as we have done for the application.

This job will respond to all create, update, and delete events. You can customize this by using the

events: [“create”, “update”,delete]
field on the job by only including the events you want the job to respond to. Also, you can set up “cron” jobs using the
schedule
field.

Now, when you execute the Acorn, you’ll see some dummy data, and you’ll also be able to make an entry to the application.

Student entry application

Congratulations! You’ve successfully created an Acornfile for your application.

You can follow similar steps to create Acornfiles for your application as well. Do remember to refer to the best practices that we’ve shared for each of the fields.

General Best Practices

Apart from the best practices that we’ve listed above, there are a few general handy tips that you can use while authoring your Acorn files.

  • Use “acorn fmt” to make the Acornfile look better. If you’re using VSCode, it’s recommended to use fmt on save with the plugin.
  • Define fields in recommended order: name, description, readme, info, icon, args, services, jobs, secrets, localData
  • It is best to keep configuration to a minimum within the Acornfile and allow users to pass in additional configuration and have that merged with the App's internal values.
  • Scaling applications use ‘scale’ for stateless applications where you don’t need persistent data. However, use for loops to make unique instances of associated data based on which the scaling will occur.
  • When dealing with stateful applications, use a unique container per instance. Do not use scale for stateful applications. This ensures each instance has a unique and stable FQDN. Scaling up and down is always deterministic.

Summary

Deploying applications to the cloud is made easier with Acorn. You can get more control over the deployments when you work directly with Acornfiles. By leveraging the best practices listed here, you can write your first Acornfile and optimize your applications just like what we saw in our application.

We invite you to try the Acorn playground to explore and experiment with these best practices. Once comfortable, you can deploy your application to the Acorn sandbox environment or try one of our pre-built applications from our Acorn library. Feel free to contact the Acorn Slack community if you have any issues.