Most web applications are run in more than one place – usually at least two. The first one is the actual server that serves the application to the users. The second is the developer’s machine. It’s extremely important for developers to be able to test locally before deploying new code to all users.
In this post, we’ll examine the importance of having multiple environments and how they can help you isolate your application and prevent accidentally damaging your production systems.
What are environments?
An environment is a collection of all the configurations needed to run a particular instance of the application. This includes things like database credentials, all the API keys for the services you use, various internal settings of your application and more.
The environment is, in short, all the things you would need to change if you wanted to run a completely different and isolated version of the same application.
The exact content of the environment will vary with your application and technological stack, but the idea is applicable to all frameworks and web applications. I’ll shortly demonstrate how to create an environment from scratch in Ruby.
Why should I use environments?
The main reason to use different configurations per environment is isolation.
Lets consider an application where there’s no environment whatsoever. The configuration on the developer’s machine is the same as the one on the server (which is pretty rare). Because the configuration is the same, it means that the code running on the developer’s machine is connecting to the same database and same services as the production site. By definition, the role of the developer is to change the code of the application. What happens when the developer creates a bug locally and destroy records in the database? While he or she tests his or her code, that same code might be destroying important customer’s information, send emails to users or charging credit cards wrongfully.
As previously mentioned, it’s pretty rare for developers to connect straight to the production database. It’s however not that rare to connect to the same services (external APIs, mail servers).
The services are often not as critical as a database, but can still cause serious headaches if misused. An obvious example is that you probably don’t want to charge a real credit card from a development environment.
A practical example
Here’s an extracted and annotated configuration from one of our apps that loads settings for the current environment. The application itself is written in Ruby and uses Bundler and Rack. Even though this example is in Ruby, the same can easily be achieved with any programming language and framework.
Get the name of the current environment from an environment variable.
ENV[‘RACK_ENV’] ||= ‘development’
Sets the root path of our application.
APPROOT = File.expandpath File.join(dir, “..”)
Set it to a constant for convenience
RACKENV = ENV[‘RACKENV’]
Require the environment specific dependencies
Bundler.require(:default, RACK_ENV)
Load a YAML file containing ALL of our API keys and environment-specific settings.
module MyApp def self.config @config ||= parseconfig(readconfig_hash) end
private def self.readconfighash YAML.load(File.read(“#{APPROOT}/config/#{RACKENV}.yml”)) end end
Use the loaded configuration file to configure our exception manager with the correct api key and to tag all exceptions with this environment.
Raven.configure do |config| config.dsn = MyApp.config.raven.dsn config.tags = { environment: RACK_ENV } config.logger = logger end “`
Best practices for QA environments
Every web app should have 3 environments at the very least. One for development, one for production and one for testing. I would also recommend a fourth one for a staging server.
Once you have these environments setup, you need to make sure that every API key in your environment is configured on a per-environment basis. For the test environment, it might be fine to not even specify a key for some services. You probably don’t want your unit tests to send actual emails or charge a credit card.
Finally, if you setup a staging environment, you should make it as close to production as possible. A staging environment is meant to test new features before deploying them to production. You want to avoid any discrepancies between the two.
Dealing with services
One question we often get at Rainforest is how to deal with services. Some services, such as Stripe, provide a sandbox mode. This means that you can use the same account with a different key, but none of the requests done in Sandbox mode will have any effect on your real customers. You should check whether or not the API you’re using supports such a mode.
If it doesn’t, then the next solution is to create a new account for each environment. This can be tedious and expensive, but it beats breaking important things in production.
What should I do to get started?
Take the following steps to start using environments effectively:
- Identify all of your external dependencies (email servers, APIs, etc)
- Extract the configuration of all these services to a configuration file
- Create a copy of that file for your new environment with a different key for each service
- Load the correct configuration file based on the current environment
There are more steps that are application and language specific, but they should become obvious one you have a working environment setup.
Finally, you should should setup a different environment for your QA tests and use Rainforest for easy QA.