How we’re using Acorn here at Acorn Labs

by | Aug 15, 2022

Spread the word

At Acorn Labs, we believe in eating our own dogfood and we’ve been doing it since our very first release. To support our DNS feature, we’ve deployed a publicly accessible service to create and manage DNS entries for Acorn applications. This service is a great use-case for Acorn. It needs:

  1. A database backend
  2. A REST service frontend
  3. The ability to communicate with external services such as Amazon’s Route53 DNS Service

This blog will breakdown exactly how we used Acorn to take this application from development to production. All the source code for the application is available here.

Here’s a link to the app’s Acornfile for reference. Throughout the rest of the article, we’ll use non-functional snippets of it to aid in our discussion.

For this application, we’ve defined two containers: a database container (named db) and an application container (named default):

containers: {
  default: { ... },
  db: { ... }
}

As-is, this Acornfile is great for development.  When we need to run locally or in an ad-hoc test environment, we can just run:

$ acorn run --dev .

But, there are a few areas where we need better solutions for production, which are discussed below.

From Dev to Prod

A few aspects of this Acornfile are intentionally not production grade. They are the quick and easy solutions to enable rapid development. However, Acorn gives us the power to swap in production-grade solutions when we need to. Let’s discuss how for the following areas:

  • Database
  • Credentials and secrets
  • Accessing the application

Database

The db container definition in our Acornfile is intentionally simple. Frankly, we don’t want a production-grade DB setup for development. So, when it is time to deploy to production, we need something more robust. Amazon’s managed database solution RDS is one possible solution, but we want to show that Acorn is a rock-solid solution for deploying and managing complex stateful applications like highly-available databases. As such, we are dogfooding our own production-grade MariaDB Acorn. You can run the same Acorn that this app is using with the following command:

$ acorn run ghcr.io/acorn-io/library/mariadb:v10.6.8-focal-acorn.3 \
--backup-schedule "15 */6 * * *" --db-name dns \
--cluster-name prod-galera-cluster

If you want more details on this Acorn image, see:

The published images

The source code

So, how did we swap out the dev database for a production instance? We leveraged Acorn’s linking feature. We deployed a MariaDB Acorn, named it prod-db, and then linked to it when deploying our acorn-dns app:

$ acorn run ... --link prod-db:db ghcr.io/acorn-io/acorn-dns:v0.1.0

This causes our independently running MariaDB Acorn app to be discoverable by the acorn-dns app as the name db, overriding the built-in db container.

Credentials and secrets

As is typical with applications that talk to a database, the app needs a set of database credentials. We can take care of credential creation and usage completely in the Acornfile:

containers: {
  default: {
    // ...
    env: {
      ACORN_DB_USER: "secret://db-user-credentials/username"
      ACORN_DB_PASSWORD: "secret://db-user-credentials/password"
      ACORN_DB_HOST: "db"
      ACORN_DB_PORT: "3306"
    }
  },
  db: {
    image: "mariadb:10.7.4"
    env: {
      MARIADB_USER: "secret://db-user-credentials/username"
      MARIADB_PASSWORD: "secret://db-user-credentials/password"
    }
    // ...	
  }
}
secrets: {
  // ...
  "db-user-credentials": {
    type: "basic"
    data: {
      username: "acorn"
    }
  }
}

Here you can see that we are creating a secret called db-user-credentials and referencing it in both the app and the database. No hardcoded credentials, even in development.

But in production, we aren’t using the built-in database and thus we can’t rely on this mechanism. Fortunately, we can simply bind in the credential secret from the MariaDB Acorn at runtime:

$ acorn run --secret prod-dns-db.db-user-credentials:db-user-credentials ... \
ghcr.io/acorn-io/acorn-dns:v0.1.0

This --secret binding syntax works like the linking we talked about earlier. It is telling Acorn to bind the secret named db-user-credentials from the app named prod-db over my app’s secret named db-user-credentials

It’s also worth talking about the AWS credentials at this point. As a general best practice, if your app needs to interact with AWS APIs, you should rely on IAM roles that are dynamically bound to your application. At a node level, you can use instance profiles and at a pod level you can use AWS’s pod identity webhook. Explaining these AWS features is out of the scope of this article but the important takeaway is this: the infrastructure is binding in the credentials dynamically, they aren’t specified by me, the developer, anywhere.

That’s great for production, but when we’re developing, we just need easy access from our locally running instance of the application to a non-production AWS account. That’s where these bits of the Acornfile comes in:

containers: {
  default: {
    // ...
    env: {
      // ...
      AWS_ACCESS_KEY_ID: "secret://aws-creds/access-key"
      AWS_SECRET_ACCESS_KEY: "secret://aws-creds/secret-key"
    }
  },
  // ...
}
// ...
secrets: {
  // ...
  "aws-creds": {
    type: "opaque"
    data: {
      "access-key": ""
      "secret-key": ""
    }
  }
}

When running locally, we can define a set of AWS credentials in an Acorn secret via:

$ acorn secret create --file creds.yaml my-creds

and then bind them into the running Acorn with:

$ acorn run -s my-creds:aws-creds --dev .

When we omit that secret binding (like in production), the environment variables will be blank and AWS’s credential injection mechanisms will kick in.

Accessing the service

When developing locally, this is quite simple: we are using a local Kubernetes solution such as Docker Desktop or Rancher Desktop and through their built-in proxying technologies, our application is available on localhost. We can easily hit the API locally to develop and iterate.

In production, we want to publish the service to a publicly accessible URL. Acorn let’s us publish any container in our app to a domain of our choosing. We just need to use the --publish syntax:

$ acorn run ... --publish alpha-dns.acrn.io:default ghcr.io/acorn-io/acorn-dns:v0.1.0

This will cause requests to https://alpha-dns.acrn.io to be routed to our service. To complete this, out of band from Acorn we need to create a DNS entry that will point alpha-dns.acrn.io at our cluster’s public FQDN or IP.

Note: By default, Acorn will create a DNS entry for your deployed applications. That’s what this acorn-dns service is for! But you can always override and customize it. See the docs on DNS for more details.

Bringing it all together

So, to go from a simple dev environment to a production-grade deployment, we have slowly built up our acorn run commands.

First, we deployed a production-grade database:

$ acorn run --name proddb ghcr.io/acorn-io/library/mariadb:v10.6.8-focal-acorn.3 \
--backup-schedule "15 */6 * * *" --db-name dns --cluster-name prod-galera-cluster

and then we launched the acorn-dns application with all the necessary information to talk to the DB, get AWS credentials, and be exposed publicly:

$ acorn run --secret prod-dns-db.db-user-credentials:db-user-credentials \
--publish alpha-dns.acrn.io:default \
--link proddb:db \
--name prod-acorn-dns ghcr.io/acorn-io/acorn-dns:v0.1.0 --db-name dns \
--route53-zone-id your-zone-here

This is great, but we need to deploy this to production. We need some artifacts.

Publishing the Acorn images in CI

For this app, we need to publish the MariaDB and acorn-dns Acorn images. This is straight-forward using technologies we are already familiar with: image registries and GitHub actions.

The images are OCI artifacts, which means they can be pushed to any image registry such as Docker Hub, GitHub Container Registry, or a private registry of your choosing.

We’ve built GitHub actions for easily integrating Acorn into CI pipelines. These actions give us everything we need to build and publish our images when our repos are tagged. You can see exactly how we setup these actions here:

Now, with the repos tagged and images published, we can finally move on to deploying to production. Enter GitOps.

Deploying with GitOps

Up to this point, we’ve used acorn run commands to explain what we’ve done. But when it comes time to deploy this application to production, we are using GitOps. Acorn integrates quite nicely with any Kubernetes GitOps tool. The basic idea with the majority of these tools is that you commit Kubernetes YAML to a git repository and the tool will pick that up and apply it to your cluster. You can easily get the Kubernetes YAML equivalent of any acorn run command by adding the -o yaml flag to the run command.

Here’s the command and YAML output for deploying our production MariaDB Acorn app:

$ acorn run -o yaml --name prod-db \
ghcr.io/acorn-io/library/mariadb:v10.6.8-focal-acorn.3 \
--backup-schedule "15 */6 * * *" --db-name dns --cluster-name prod-galera-cluster

apiVersion: api.acorn.io/v1
kind: App
metadata:
  name: prod-db
  namespace: acorn
spec:
  deployArgs:
    backupSchedule: 15 */6 * * *
    clusterName: prod-galera-cluster
    dbName: dns
  image: ghcr.io/acorn-io/library/mariadb:v10.6.8-focal-acorn.3

And here’s the command and output for deploying the acorn-dns app, linked to the database we just deployed:

$ acorn run -o yaml --secret prod-dns-db.db-user-credentials:db-user-credentials \
--publish alpha-dns.acrn.io:default --link prod-dns-db:db --name prod-acorn-dns \
ghcr.io/acorn-io/acorn-dns:v0.1.0 --db-name dns --route53-zone-id your-zone-here

apiVersion: api.acorn.io/v1
kind: App
metadata:
  name: prod-acorn-dns
  namespace: acorn
spec:
  deployArgs:
    dbName: dns
    route53ZoneId: your-zone-here
  image: ghcr.io/acorn-io/acorn-dns:v0.1.0
  ports:
  - protocol: http
    publish: true
    serviceName: alpha-dns.acrn.io
    targetServiceName: default
  secrets:
  - secret: prod-dns-db.db-user-credentials
    target: db-user-credentials
  services:
  - service: prod-dns-db
    target: db

For the Kubernetes wonks reading this – the App resource (and all other resources in the api.acorn.io API group) is NOT a CustomResourceDefinition. We’re using Kubernetes API Aggregation to present these types.

Note: At the time of this writing, our API types aren’t well documented. We’re working on that! We’ll update this article with a link once they’re available.

If you were to kubectl apply the above YAML, you would get a running Acorn app equivalent to the associated acorn run commands.

At this point, you can drop that YAML into a Git repo and use the GitOps tool of your choice to deploy it.

And there you have it! That’s how we’re using Acorn at Acorn for our production services.


Spread the word