Summary: Make your toy web apps more robust and manageable by following the Twelve-Factor App methodology.
This is the third and last part in a series of posts detailing the 12 Factor App. In the last post we looked at:
- Build, release, and run stages and how segmenting your development processes can open up possibilities in terms of automation
- Explored the benefits of running your app as a stateless process
If you need a refresher or would like to take a look at the previous posts:
As with the last post, most of the remaining factors come about as downstream results from implementing the previous factors. If you’ve been following along, many of these will be on the easier side of implementing – or it may even be done already if you’re using certain services.
Export services via port binding - https://12factor.net/port-binding
Once your application becomes more or less stateless (Factors II, III, IV, you’re now able to think of your application itself as a backing service. One of the last steps would be to export HTTP as a service by binding to a port.
For example, when developing a frontend application, you may be familiar with a development server listening on your
local port 3000 (i.e.
http://localhost:3000). Exporting your app as a service would work the same. Some frameworks may
have it built-in already and can be deployed with a simple
node app.js. You may have to look into adding additional
dependencies such as uWSGI/Gunicorn in the Python world.
Scale out via the process model - https://12factor.net/concurrency
The obvious first thought when attempting to scale your app is “Can we just run more instances of the app?” If you’ve been following along so far and implemented all the previous factors, the answer should be “Yes!*”
* But you should still look at the remaining factors :)
Some of the main hurdles at this point will be to:
- Assess your infrastructure and see if this can be done automatically, like with Kubernetes. Scaling up and down automatically, depending on usage, can save your site when unexpected traffic hits.
- Assess your code and backing services. If a bunch of users hit an endpoint at the same time on multiple app instances, will something catch on fire? Can your database support all the new connections/queries from the new instance?
- Start thinking about any additional costs when new instances are created. How much additional resources your entire application plus infrastructure will start to consume?
- Think about how data and user flow will work when a new instance is created and destroyed (covered in IX. Disposability).
Maximize robustness with fast startup and graceful shutdown - https://12factor.net/disposability
Since your app is stateless, you won’t have to worry about any stored data in your app instance as anything important is stored in the cloud and/or in a database. Going hand-in-hand with concurrency above, this gives you the freedom of creating and destroying instances at any time. You can create new instances to handle more load on your application and also destroy any excess to avoid incurring extra infrastructure costs.
When thinking about disposability, your app should be resilient against interruptions at any point in its lifecycle.
- What will your app do when a long-running request gets cancelled?
- Will your database be left with incomplete data?
- Will your job queue be left with stale jobs that never finished?
Addressing these sorts of potential problems may lead to a lot of work, but will definitely be worth it since the work you do here will also help mitigate any problems when more catastrophic app failures happen.
Again, if using a Kubernetes cluster, a lot of this is handled automatically, however, you will still need to think about your codebase and backing services and how they will react to sudden interruptions.
Keep development, staging, and production as similar as possible - https://12factor.net/dev-prod-parity
In a modern development environment, this is much less of an issue because of the popularity of Docker and automation. Nevertheless, it’s still important to try to keep development and production environments as similar as possible. Of course, this does not mean you should be developing on the live production database, but if your app in production uses PostgreSQL, you should also be using PostgreSQL in development, and so on.
The most common way of achieving this is to use Docker. Docker makes this easy by making development environments extremely reproducible which leads to less ramp up time for new team members and more time working on your app’s features. Docker also makes it easy to have the same (or nearly the same) backing services available for your app in development with docker-compose.
Keeping development and production as identical as possible should:
- Decrease backing service issues during deployment.
- Give more confidence to developers that their code will work the way it does in their own development environment.
- Allow higher chance of reproducibility for any issues seen on production.
Treat logs as event streams - https://12factor.net/logs
With smaller, earlier stage apps, logging is most likely an afterthought, or not as robust, so this may not seem that important. In a 12 factor app, logs are treated as event streams. This can be done through external services such as Papertrail, or possibly already handled by your app’s deployment platform such as Heroku or Google Cloud Platform.
Essentially 12 factor apps should only be concerned with sending log output to the system’s
stdout. This allows for
other backing services or apps to manage log data and whether or not to store it. Again, this keeps the app stateless as
it decouples log storage and it also centralizes all log data, which is especially useful when there’s more than one app
Logging itself, as well as best practices, what to log, etc. is a whole different beast, but good logging should allow for:
- Finding specific events in the past.
- Large-scale graphing of trends (such as requests per minute).
- Active alerting according to user-defined heuristics (such as an alert when the quantity of errors per minute exceeds a certain threshold). https://12factor.net/logs
Run admin/management tasks as one-off processes - https://12factor.net/admin-processes
More likely than not, your application will have to run admin processes every now and then. An example of this includes database migrations to change table structures and/or data. In a 12 factor app, these admin processes should be stored in the same codebase as the app, and it should also run in the same environment as the app.
This prevents extra dependencies and unexpected issues from things such as one-off shell scripts. Being in the same codebase, and in the same environment, we’re able to control the one-off processes’s execution to ensure it runs as expected.
As examples: In the node world, you could have
yarn scripts that would bootstrap part of your application to
run these one-off tasks. Also in the Python world, Django has its own REPL and framework to create your own custom
commands which run via
python manage.py <YOUR_COMMAND>.
A caveat: while doing research for this, some articles mention that having an REPL available in production (which is what 12factor.net recommends) is a potential security concern as it gives almost complete access to your entire application. Depending on how your infrastructure is designed, you may also want to incorporate more security for one-off admin processes.
According to Wikipedia the Twelve-Factor app is over 10 years old(!) at the time of writing this post. As this post was written, it’s incredible how many of these concepts are still absolutely valuable and used in practice in the modern development world we’re in today. We here at Anvil follow nearly all of the 12 factors and our development team performs like a well oiled machine because of it.
Are you developing your apps with the 12 Factor methodology in mind? Are you using or want to use Anvil as a backing service? If you’re developing something cool with PDFs or paperwork automation, let us know at firstname.lastname@example.org. We’d love to hear from you.