|Practical mod_perl / HTML Book /|
5.9. Three-Tier Server Scheme: Development, Staging, and Production
To facilitate transfer from the development server to the production server, the code should be free of any server-dependent variables. This will ensure that modules and scripts can be moved from one directory on the development machine to another directory (possibly in a different path) on the production machine without problems.
If two simple rules are followed, server dependencies can be safely isolated and, as far as the code goes, effectively ignored. First, never use the server name (since development and production machines have different names), and second, never use explicit base directory names in the code. Of course, the code will often need to refer to the server name and directory names, but we can centralize them in server-wide configuration files (as seen in a moment).
By trial and error, we have found that a three-tier (development, staging, and production) scheme works best:
The staging machine does not have to be anywhere near as powerful as the production server if finances are stretched. The staging machine is generally used only for staging; it does not require much processor power or memory since only a few developers are likely to use it simultaneously. Even if several developers are using it at the same time, the load will be very low, unless of course benchmarks are being run on it along with programs that create a load similar to that on the production server (in which case the staging machine should have hardware identical to that of the production machine).
You can also have the staging and production servers running on the same machine. This is not ideal, especially if the production server needs every megabyte of memory and every CPU cycle so that it can cope with high request rates. But when a dedicated machine just for staging purposes is prohibitively expensive, using the production server for staging purposes is better than having no staging area at all.
Another possibility is to have the staging environment on the development machine.
So how does this three-tier scheme work?
Developers write the code on their machines (development tier) and test that it works as expected. These machines should be set up with an environment as similar to the production server as possible. A manageable and simple approach is to have each developer running his own local Apache server on his own machine. If the code relies on a database, the ideal scenario is for each developer to have access to a development database account and server, possibly even on their own machines.
The pre-release manager installs the code on the staging tier machine and stages it. Whereas developers can change their own httpd.conf files on their own machines, the pre-release manager will make the necessary changes on the staging machine in accordance with the instructions provided by the developers.
The release manager installs the code on the production tier machine(s), tests it, and monitors for a while to ensure that things work as expected.
Of course, on some projects, the developers, the pre-release managers, and the release managers can actually be the same person. On larger projects, where different people perform these roles and many machines are involved, preparing upgrade packages with a packaging tool such as RPM becomes even more important, since it makes it far easier to keep every machine's configuration and software in sync.
Now that we have described the theory behind the three-tier approach, let us see how to have all the code independent of the machine and base directory names.
Although the example shown below is simple, the real configuration may be far more complex; however, the principles apply regardless of complexity, and it is straightforward to build a simple initial configuration into a configuration that is sufficient for more complex environments.
Basically, what we need is the name of the machine, the port on which the server is running (assuming that the port number is not hidden with the help of a proxy server), the root directory of the web server-specific files, the base directories of static objects and Perl scripts, the appropriate relative and full URIs for these base directories, and a support email address. This amounts to 10 variables.
We prepare a minimum of three Local::Config packages, one per tier, each suited to a particular tier's environment. As mentioned earlier, there can be more than one machine per tier and even more than one web server running on the same machine. In those cases, each web server will have its own Local::Config package. The total number of Local::Config packages will be equal to the number of web servers.
For example, for the development tier, the configuration package might look like Example 5-3.
package Local::Config; use strict; use constant SERVER_NAME => 'dev.example.com'; use constant SERVER_PORT => 8000; use constant ROOT_DIR => '/home/userfoo/www'; use constant CGI_BASE_DIR => '/home/userfoo/www/perl'; use constant DOC_BASE_DIR => '/home/userfoo/www/docs'; use constant CGI_BASE_URI => 'http://dev.example.com:8000/perl'; use constant DOC_BASE_URI => 'http://dev.example.com:8000'; use constant CGI_RELATIVE_URI => '/perl'; use constant DOC_RELATIVE_URI => ''; use constant SUPPORT_EMAIL => 'email@example.com'; 1;
The constants have uppercase names, in accordance with Perl convention.
The configuration shows that the name of the development machine is dev.example.com, listening to port 8000. Web server-specific files reside under the /home/userfoo/www directory. Think of this as a directory www that resides under user userfoo's home directory, /home/userfoo. A developer whose username is userbar might use /home/userbar/www as the development root directory.
If there is another web server running on the same machine, create another Local::Config with a different port number and probably a different root directory.
To avoid duplication of identical parts of the configuration, the package can be rewritten as shown in Example 5-4.
package Local::Config; use strict; use constant DOMAIN_NAME => 'example.com'; use constant SERVER_NAME => 'dev.' . DOMAIN_NAME; use constant SERVER_PORT => 8000; use constant ROOT_DIR => '/home/userfoo/www'; use constant CGI_BASE_DIR => ROOT_DIR . '/perl'; use constant DOC_BASE_DIR => ROOT_DIR . '/docs'; use constant CGI_BASE_URI => 'http://' . SERVER_NAME . ':' . SERVER_PORT . '/perl'; use constant DOC_BASE_URI => 'http://' . SERVER_NAME . ':' . SERVER_PORT; use constant CGI_RELATIVE_URI => '/perl'; use constant DOC_RELATIVE_URI => ''; use constant SUPPORT_EMAIL => 'stas@' . DOMAIN_NAME; 1;
Reusing constants that were previously defined reduces the risk of making a mistake. In the original file, several lines need to be edited if the server name is changed, but in this new version only one line requires editing, eliminating the risk of your forgetting to change a line further down the file. All the use constantstatements are executed at compile time, in the order in which they are specified. The constant pragma ensures that any attempt to change these variables in the code leads to an error, so they can be relied on to be correct. (Note that in certain contexts—e.g., when they're used as hash keys—Perl can misunderstand the use of constants. The solution is to either prepend & or append ( ), so ROOT_DIR would become either &ROOT_DIR or ROOT_DIR( ).)
Now, when the code needs to access the server's global configuration, it needs to refer only to the variables in this module. For example, in an application's configuration file, you can create a dynamically generated configuration, which will change from machine to machine without your needing to touch any code (see Example 5-5).
package App::Foo::Config; use Local::Config ( ); use strict; use vars qw($CGI_URI $CGI_DIR); # directories and URIs of the App::Foo CGI project $CGI_URI = $Local::Config::CGI_BASE_URI . '/App/Foo'; $CGI_DIR = $Local::Config::CGI_BASE_DIR . '/App/Foo'; 1;
Notice that we used fully qualified variable names instead of importing these global configuration variables into the caller's namespace. This saves a few bytes of memory, and since Local::Config will be loaded by many modules, these savings will quickly add up. Programmers used to programming Perl outside the mod_perl environment might be tempted to add Perl's exporting mechanism to Local::Config and thereby save themselves some typing. We prefer not to use Exporter.pm under mod_perl, because we want to save as much memory as possible. (Even though the amount of memory overhead for using an exported name is small, this must be multiplied by the number of concurrent users of the code, which could be hundreds or even thousands on a busy site and could turn a small memory overhead into a large one.)
For the staging tier, a similar Local::Config module with just a few changes (as shown in Example 5-6) is necessary.
package Local::Config; use strict; use constant DOMAIN_NAME => 'example.com'; use constant SERVER_NAME => 'stage.' . DOMAIN_NAME; use constant SERVER_PORT => 8000; use constant ROOT_DIR => '/home'; use constant CGI_BASE_DIR => ROOT_DIR . '/perl'; use constant DOC_BASE_DIR => ROOT_DIR . '/docs'; use constant CGI_BASE_URI => 'http://' . SERVER_NAME . ':' . SERVER_PORT . '/perl'; use constant DOC_BASE_URI => 'http://' . SERVER_NAME . ':' . SERVER_PORT; use constant CGI_RELATIVE_URI => '/perl'; use constant DOC_RELATIVE_URI => ''; use constant SUPPORT_EMAIL => 'stage@' . DOMAIN_NAME; 1;
We have named our staging tier machine stage.example.com. Its root directory is /home.
The production tier version of Local/Config.pm is shown in Example 5-7.
package Local::Config; use strict; use constant DOMAIN_NAME => 'example.com'; use constant SERVER_NAME => 'www.' . DOMAIN_NAME; use constant SERVER_PORT => 8000; use constant ROOT_DIR => '/home/'; use constant CGI_BASE_DIR => ROOT_DIR . '/perl'; use constant DOC_BASE_DIR => ROOT_DIR . '/docs'; use constant CGI_BASE_URI => 'http://' . SERVER_NAME . ':' . SERVER_PORT . '/perl'; use constant DOC_BASE_URI => 'http://' . SERVER_NAME . ':' . SERVER_PORT; use constant CGI_RELATIVE_URI => '/perl'; use constant DOC_RELATIVE_URI => ''; use constant SUPPORT_EMAIL => 'support@' . DOMAIN_NAME;
You can see that the setups of the staging and production machines are almost identical. This is only in our example; in reality, they can be very different.
The most important point is that the Local::Config module from a machine on one tier must never be moved to a machine on another tier, since it will break the code. If locally built packages are used, the Local::Config file can simply be excluded—this will help to reduce the risk of inadvertently copying it.
From now on, when modules and scripts are moved between machines, you shouldn't need to worry about having to change variables to accomodate the different machines' server names and directory layouts. All this is accounted for by the Local::Config files.
Some developers prefer to run conversion scripts on the moved code that adjust all variables to the local machine. This approach is error-prone, since variables can be written in different ways, and it may result in incomplete adjustment and broken code. Therefore, the conversion approach is not recommended.