ECS (EC2 Container Service) is one of the latest Web services released by Amazon and it is among the cool kids around. Why? Well it let you deploy and administer Docker containers by integrating deeply with the other Web services offered by Amazon. To name a few, ELB (Elastic Load Balancer), Launching Configuration and Auto Scaling Groups (ASG).
At the base of ECS reside two fundamental concepts, tasks and services.
Each one of these entities require one or more docker containers running inside special EC2 instances called ECS Container Instance. These instances are nothing more than EC2 instances shipped with the Amazon ECS container agent. The agent allows the instances to register themselves to a cluster, i.e. the entity representing the highest level of abstraction in ECS realm. For more info about it here you can find the official definition by Amazon. The instances also need to have a proper IAM role that will let them to be handled by the Auto Scaling Group. The first time you create a cluster you get the opportunity to automatically create a proper IAM role, i.e. ecsInstanceRole
.
Tasks and services are actually the things that get the work done. The first should be generally used to deliver short term, one shot, functionalities related to the deploying application while the latter are meant to ensure the running of long term tasks. The services are indeed in charge of monitoring the tasks and guarantee that they are running as desired (e.g. in the correct number).
Actually there is a lot (lot!) more to discuss about tasks and services but in this article doesn’t want to be a comprehensive guide to ECS (for that I encourage you to take a deep look at the official Amazon doc).
What I want is to share a few choices that I made (with my colleagues) while trying to migrate a WordPress application on ECS (and consequently on Docker).
The first one was to represent our application as an entire cluster in order to have a more fine grained control over the actual architecture of the tasks and the services placed at the base of our application. We still don’t know if it was the right way to go but, until now, we have not encouter any problem with this decision.
To KISS (Keep It Simple and Stupid) I have also decided to deploy the application inside a single docker container with NGINX as application server and the HHVM as PHP “interpreter”.
The database is completely external as it resides on a dedicated RDS instance while the assets are served from an S3 bucket on which are uploaded directly by a WordPress plugin that I have already mentioned in one of my previous articles, i.e. WordPress Offload.
Together with a proper Dockerfile, that I will probably describe in a future post, I also restructured the application wp-config.php
to rely on the environment variables and the PHP dotenv library:
<?php
/**
* The base configurations of a Dockerized WordPress.
* You can find more information by visiting
* {@link http://codex.wordpress.org/Editing_wp-config.php Editing wp-config.php}
*/
# Load the .env if present.
if (file_exists(dirname(__FILE__).DIRECTORY_SEPARATOR.'.env')) {
require 'vendor/autoload.php';
$dotenv = new DotenvDotenv(__DIR__);
$dotenv->load();
}
# The name of the database for WordPress.
define('DB_NAME', getenv('WORDPRESS_DB_NAME'));
# MySQL database username.
define('DB_USER', getenv('WORDPRESS_DB_USER'));
# MySQL database password.
define('DB_PASSWORD', getenv('WORDPRESS_DB_PASSWORD'));
# MySQL hostname.
define('DB_HOST', getenv('WORDPRESS_DB_HOST'));
# Database Charset to use in creating database tables.
define('DB_CHARSET', getenv('WORDPRESS_DB_CHARSET'));
# The Database Collate type. Don't change this if in doubt.
define('DB_COLLATE', getenv('WORDPRESS_DB_COLLATE'));
/**
* Authentication Unique Keys and Salts.
*
* Change these to different unique phrases!
* You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}
* You can change these at any point in time to invalidate all existing cookies. This will force all users to have to log in again.
*
* @since 2.6.0
*/
define('AUTH_KEY', getenv('WORDPRESS_AUTH_KEY'));
define('SECURE_AUTH_KEY', getenv('WORDPRESS_SECURE_AUTH_KEY'));
define('LOGGED_IN_KEY', getenv('WORDPRESS_LOGGED_IN_KEY'));
define('NONCE_KEY', getenv('WORDPRESS_NONCE_KEY'));
define('AUTH_SALT', getenv('WORDPRESS_AUTH_SALT'));
define('SECURE_AUTH_SALT', getenv('WORDPRESS_SECURE_AUTH_SALT'));
define('LOGGED_IN_SALT', getenv('WORDPRESS_LOGGED_IN_SALT'));
define('NONCE_SALT', getenv('WORDPRESS_NONCE_SALT'));
# Enable Amazon S3 and Cloudfront.
define('AWS_ACCESS_KEY_ID', getenv('WORDPRESS_AWS_ACCESS_KEY_ID'));
define('AWS_SECRET_ACCESS_KEY', getenv('WORDPRESS_AWS_SECRET_ACCESS_KEY'));
# Enable WP Cache.
define('WP_CACHE', getenv('WORDPRESS_WP_CACHE'));
define('WPCACHEHOME', getenv('WORDPRESS_WPCACHEHOME')); //Added by WP-Cache Manager
# Enable Secure Logins.
define('FORCE_SSL_LOGIN', getenv('WORDPRESS_FORCE_SSL_LOGIN'));
define('FORCE_SSL_ADMIN', getenv('WORDPRESS_FORCE_SSL_ADMIN'));
# Use external cron.
define('DISABLE_WP_CRON', getenv('WORDPRESS_DISABLE_WP_CRON'));
/**
* WordPress Database Table prefix.
*
* You can have multiple installations in one database if you give each a unique
* prefix. Only numbers, letters, and underscores please!
*/
$table_prefix = getenv('WORDPRESS_TABLE_PREFIX');
/**
* WordPress Localized Language, defaults to English.
*
* Change this to localize WordPress. A corresponding MO file for the chosen
* language must be installed to wp-content/languages. For example, install
* de_DE.mo to wp-content/languages and set WPLANG to 'de_DE' to enable German
* language support.
*/
define('WPLANG', getenv('WORDPRESS_WPLANG'));
/**
* For developers: WordPress debugging mode.
*
* Change this to true to enable the display of notices during development.
* It is strongly recommended that plugin and theme developers use WP_DEBUG
* in their development environments.
*/
define('WP_DEBUG', getenv('WORDPRESS_WP_DEBUG'));
# Absolute path to the WordPress directory.
if (!defined('ABSPATH'))
define('ABSPATH', dirname(__FILE__).DIRECTORY_SEPARATOR);
# To enable the possibility to update plugins directly from back-end (locally).
define('FS_METHOD', getenv('WORDPRESS_FS_METHOD'));
# Disable updates of plugins and themes together with plugins and themes editor.
define('DISALLOW_FILE_MODS', getenv('WORDPRESS_DISALLOW_FILE_MODS'));
# Disable all automatic updates.
define('AUTOMATIC_UPDATER_DISABLED', getenv('WORDPRESS_AUTOMATIC_UPDATER_DISABLED'));
# How to handle all core updates.
define('WP_AUTO_UPDATE_CORE', getenv('WORDPRESS_WP_AUTO_UPDATE_CORE'));
# WP Siteurl.
define('WP_SITEURL', getenv('WORDPRESS_WP_SITEURL'));
# WP Home.
define('WP_HOME', getenv('WORDPRESS_WP_HOME'));
# Enable Multisite.
define('WP_ALLOW_MULTISITE', getenv('WORDPRESS_WP_ALLOW_MULTISITE'));
# Uploads directory.
define('UPLOADS', getenv('WORDPRESS_UPLOADS'));
/** Sets up WordPress vars and included files. */
require_once(ABSPATH.'wp-settings.php');
It defines almost all the constants that can be used inside a wp-config.php
and valorize them with environment variables.
Given the behavior of the getenv()
function, the constants that are boolean by definition (e.g. WP_DEBUG
) should not be defined if they need to be false
. The getenv('SOME_ENV_VARIABLE')
returns indeed false
if 'SOME_ENV_VARIABLE'
is not defined.
To continue to develop locally it is also needed to:
- install composer
- install phpdotenv
- place an
.env
file in the root of the app repo with all the environment variables that your app need to be running. Here there is an example of a possible.env
file:
WORDPRESS_DB_NAME=db_name
WORDPRESS_DB_USER=db_user
WORDPRESS_DB_PASSWORD=db_password
WORDPRESS_DB_HOST=localhost
WORDPRESS_DB_CHARSET=utf8
WORDPRESS_DB_COLLATE=
WORDPRESS_AUTH_KEY=
WORDPRESS_SECURE_AUTH_KEY=
WORDPRESS_LOGGED_IN_KEY=
WORDPRESS_NONCE_KEY=
WORDPRESS_AUTH_SALT=
WORDPRESS_SECURE_AUTH_SALT=
WORDPRESS_LOGGED_IN_SALT=
WORDPRESS_NONCE_SALT=
WORDPRESS_AWS_ACCESS_KEY_ID=MYACCESSKEY
WORDPRESS_AWS_SECRET_ACCESS_KEY=MYACCESSKEY
WORDPRESS_TABLE_PREFIX=wp_
WORDPRESS_WPLANG=en_US
WORDPRESS_WP_DEBUG=
WORDPRESS_FS_METHOD=direct
WORDPRESS_WP_SITEURL=http://my-site.test
WORDPRESS_WP_HOME=http://my-site.test
The modified version of the wp-config.php
take into consideration the possible presence of the .env
file and loads the environment variables making them available to you app:
# Load the .env if present.
if (file_exists(dirname(__FILE__).DIRECTORY_SEPARATOR.'.env')) {
require 'vendor/autoload.php';
$dotenv = new DotenvDotenv(__DIR__);
$dotenv->load();
}
To deploy the application I relied on a automatic building service, i.e. Quay.io, to automatically build the images when changes are pushed to the version control system. ECS can be configured to pull images from a remote repo like the one offered by Quay.io.
Together with the ECS cluster I also created a task definition represeting the application with all the containers needed by it to run. The tasks can be configured through the UI or a JSON
file. At this point you can set the required environment variables needed by the wp-config.php
. An example of the "environment"
section inside the JSON
configuration is the following one:
...
"environment": [
{
"name": "WORDPRESS_DB_USER",
"value": "name_of_the_db_user"
},
{
"name": "WORDPRESS_DB_CHARSET",
"value": "utf8"
},
{
"name": "WORDPRESS_DB_HOST",
"value": "the_host_where_the_db_is_placed" # possibly an RDS endpoint
},
{
"name": "WORDPRESS_NONCE_KEY",
"value": "long_random_key"
},
{
"name": "WORDPRESS_DB_NAME",
"value": "name_of_the_app_db"
},
{
"name": "WORDPRESS_AWS_ACCESS_KEY_ID",
"value": "aws_access_key"
},
{
"name": "WORDPRESS_LOGGED_IN_KEY",
"value": "long_random_key"
},
{
"name": "WORDPRESS_UPLOADS",
"value": "location_of_the_uploads_directory" # by default `wp-content/uploads`
},
{
"name": "WORDPRESS_DISABLE_WP_CRON",
"value": "true"
},
{
"name": "WORDPRESS_AWS_SECRET_ACCESS_KEY",
"value": "aws_secret_access_key"
},
{
"name": "WORDPRESS_DB_PASSWORD",
"value": "db_password_of_the_db_user"
},
{
"name": "WORDPRESS_SECURE_AUTH_KEY",
"value": "long_random_key"
},
{
"name": "WORDPRESS_TABLE_PREFIX",
"value": "table_prefix" # by default `wp_`
},
{
"name": "WORDPRESS_WPLANG",
"value": "en_GB"
},
...
]
...
After creating the task I created a service to handle the delivery of the application and associate with it an ELB to balace the incoming traffic between the instances allocated with the cluster.
The ELB will need to be associated with an Auto Scaling Group which, in turn, will need to be associated with a Launching Configuration. The ELB, the Auto Scaling Group and the Launching Configuration are the entities that control the number of EC2 instances on which the tasks runs on.
The number of EC2 instances that need to be run by a service to handle the deployment of the new versions of the application is always the desired number of EC2 instances plus one. One instance should always be free to hold the new container created by ECS when a new version of the application task is created.
The deploy of a new version of the application a new revision of the related task should be created. This can be accomplished directly from the Amazon Dashboard or by using the aws-cli.
Personally I found particularly usefull this script. It still has some problems (e.g. with the region parameter) but if you go with the following command you shouldn’t encounter any problem at all:
ecs-deploy -k AWS_KEY -s AWS_SECRET_KEY -c CLUSTER -n NAME_OF_SERVICE_TO_DEPLOY -i repo.of.image.building.service/ORGANIZATION/REPO_NAME:TAG
What it does is simply to wrap the aws-cli commands needed to update the service that handles the task that delivers your application. This is done, as mentioned before, by creating a new revision of the task.
I realize that I talked about a lot of stuff and that I barely scratched their surface but I still hope that what I shared can be helpfull to everyone approaching the new “ECS world”.
UPDATE: to have a better overview of the serie here there is a link to the second article related to ECS and WordPress 😉
Cheers!
Leave a Reply