With my next Cloud Project, I decided to challenge myself to create a CloudFormation template for the WordPress EC2 Blog I recently created (read the full story on how I created that here). This would require automating the entire process of setting up the required AWS resources as well as the LEMP stack on the EC2 instance, associating the IP of the instance with a custom domain, while also securing the website with an SSL certificate.
This template assumes that you already have a an existing domain and hosted zone on Route 53, as well as an existing key pair in order to SSH into your instance. Click below to access the full template:
Here are a couple of conditions/acceptance criteria that I set for myself when creating this template and how I achieved them:
1. The template should be flexible enough to allow for customisation of particular attributes during the installation process.
I set up my WordPress Blog using the AWS console and the command line within my instance, and I had full control of where I deployed the EC2 instance, the domain that I associated with the instance, as well as credentials for the database. With the CloudFormation template, I needed to make sure that such attributes were not hard coded, in order for any users of the template to tailor the blog for their own uses. This imitates the nature of real life cloud server deployments where a template should be flexible enough to be deployed in various different scenarios while reserving its functionality.
How did I do it?
I first declared various parameters at the start of the template in order for someone to input custom attributes applicable to their installation (e.g. an existing key pair in their account, database credentials, their existing hosted zones, their existing domain etc.). After this, I used the following functions to reference these answers within the template:
!Ref – used to return the value of a specific parameter. For example:
Resources:
MyWebServerInstance:
KeyName: !Ref ExistingKeyPair
In this scenario, an EC2 instance is being created, and the !Ref function is being used to reference the ‘ExistingKeyPair’ to be used as a value for the ‘KeyName’ key. To put it simply, the EC2 instance will use the key pair that you specify in the ExistingKeyPair parameter as you’re creating your stack.
!Sub – used to substitute variables with values within a string. The variables can be referenced in your code in this format: ${“ExampleVariable”}. It has a similar purpose to the !Ref, however this becomes very powerful when trying to write dynamic bash scripts e.g. in your EC2 User Data. Here’s an example:
UserData:
Fn::Base64: !Sub |
#!/bin/bash
# rest of the bash script
mysqladmin -u root password "${DBRootPassword}"
In this example: a bash script has been added within an EC2 instance, that contains a command to assign a root password to a MariaDB installation. Usually, in the command line you would write the password yourself, however in the template, I’ve used the !Sub function and replaced what would usually have the password with ${DBRootPassword}, referencing an earlier parameter. This allows for the person creating the stack to decide their password before the installation, and more importantly: whatever password they desire. This is true for other credentials such as the database user and password, and database name – allowing for a truly customised installation.
2. The template needed to be secure: Sensitive information should not be present in the code.
This follows security best practices: having passwords existing as plain text in code could make the installation vulnerable to a Man-in-the-middle attack, as someone could intercept the code during the installation and retrieve credentials to be used to access internal components. To remove this vulnerability, I used parameters as well as NoEcho and substitution functions in order to allow for secure user input of credentials, to ensure that the passwords are not stored within the code and are passed at the time of installation. Let’s use the same root password example as we’ve seen before but add some more context:
Parameters:
DBRootPassword:
NoEcho: 'true'
Description: MariaDB root password
Type: String
# rest of the code...
UserData:
Fn::Base64: !Sub |
#!/bin/bash
# rest of the bash script
mysqladmin -u root password "${DBRootPassword}"
In this excerpt from the template, I’ve used the NoEcho property to ensure that the password is hidden from view when creating the stack. The input would appear as asterisks: ‘***’, and would not be echoed during the installation. This follows security best practices, and not only ensures that the user can specify their own secure password beforehand, but also allows for that password to be stored securely within their EC2 instance.
The template should allow a user to (once the stack has been created) access WordPress from their custom domain using HTTPS, with no other setup required.
This was a non-negotiable, the template had to ensure that you had a SSL secured domain ready to start using WordPress once the stack had been installed. While setting up AWS resources wasn’t particularly challenging to automate (they are relatively easy to declare within CloudFormation), my main challenge was automating the installation of all of the internal components, i.e. the LEMP stack and WordPress, while ensuring that everything worked together. In order to automate the installation, I decided to use UserData and bash scripting in order to bootstrap the instance with all of the required software. Here are some lessons that I learnt while developing the script
- A lot of the commands that I used in my previous project were able to be used within the bash script, which showed me just how powerful EC2 UserData can be, allowing you to automate almost all parts of a LEMP stack installation that took me around 30mins to complete first time using the command line.
- One challenge that I faced when creating the script was that certain elements of the installation – like securing the MySQL database or installing an SSL certificate – were interactive. Some of the commands I used in my previous project proved impractical within the script, given that these steps required user input, and the UserData execution during the CloudFormation stack creation process lacked an interactive interface. To circumvent this, I delved into the documentation of the pertinent software, gaining a comprehensive understanding of the interactive elements. With this knowledge, I identified and incorporated command line options as suitable substitutes, ensuring seamless automation within the script.
4. The template should stand the test of time (at least for a little bit!)
Because of certain commands within the installation process referencing particular files, I had to ensure that if someone were to use this template in the near future, they wouldn’t reach any errors within the instance, in order to reduce future maintenance. Of course, this template won’t last forever, but I wanted to ensure that the correct versions of internal components of the stack were installed in order to make everything work (at least in the short to medium term). A couple of things this affected:
- I decided to use parameters allowing the user to specify the specific Ubuntu AMI and availability zone that they wanted to use for their EC2 instance. The other option was to use the Mappings feature of CloudFormation, to map different regions with their AMIs, and then call a specific region’s AMI for the EC2 Instance’s region. This option would be more seamless for the user as they create their instance, however I opted against this because there’s no guarantee that the AMI IDs as well as the regions would stay 100% accurate and applicable to a user aiming to create the stack. To allow for more customisation and relevance, I opted to use parameters to allow users to use the most up-to-date information for the AMI and availability zone.
- There was an instance within the code where a specific file was mentioned (in the code below). This file’s name is dependent on the specific version of php-fpm installed, and if we were to install php-fpm without specifying a version, this file that has been hardcoded may be incorrect once a new version of php-fpm has been released. To circumvent this, I’ve ensured that the version of php matches the file referenced in the code, in order to avoid errors . However, the template can still easily be edited for a user’s individual needs.
sudo apt install php8.1-fpm # and other installation packages
# rest of the bash script
sudo cat > /etc/nginx/sites-available/wordpress << EOL
upstream php-handler {
server unix:/var/run/php/php8.1-fpm.sock;
}
server {
listen 80;
server_name ${ExistingDomainName};
root /var/www/wordpress;
index index.php;
location / {
try_files \$uri \$uri/ /index.php?\$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass php-handler;
}
}
EOL
Conclusion
Overall, this project has not only equipped me with practical knowledge in cloud deployment and configuration management but has also fortified my skills in areas such as automation, security, and infrastructure orchestration. This project not only deepened my understanding of infrastructure-as-code but also honed my proficiency in YAML code and AWS CloudFormation, both crucial skills in the realm of cloud computing. Additionally, crafting bash scripts for configuration management provided me with hands-on experience in automation, enhancing my ability to streamline and scale deployment processes efficiently. Integrating the deployment with a custom domain and fortifying it with an SSL certificate not only highlighted the importance of securely managing web applications in real-world scenarios but also underscored my commitment to security best practices. This included the approach of ensuring that sensitive information was not exposed within the CloudFormation template, showcasing a comprehensive understanding of safeguarding critical data in the deployment process.
Leave a Reply