Tag: aws

  • LAMP Stack App on AWS with Docker, RDS and phpMyAdmin

    Intro

    I was recently tasked with migrating an old LAMP-stack app (PHPv5) running on a Centos6 server to the newer PHPv7 on a Centos8 machine, and ensuring that the code didn’t break in the php upgrade. I figured the best way to do that would be to use Docker to simulate PHP 7 on a Centos8 machine running on my laptop.

    However, the plan changed and instead of deploying the new app on a Centos8 machine, it was decided that we would deploy the app to its own EC2 instance. Since I was already using Docker, and since I no longer had to plan for a Centos8 deployment, I decided to use Ubuntu 20.04 for the EC2 instance. I installed docker and docker-compose, and adapted the code to use proper PHP-Apache and phpMyAdmin Docker images. I also decided to use AWS RDS mysql, and to use the EC2 instance to implement logical backups of the mysql DB to AWS S3.

    The rest of this article consists in more detailed notes on how I went about all of this:

    • Dockerizing a LAMP-stack Application
      • php-apache docker image
      • creating dev and prod versions
      • updating code from PHPv5 to PHPv7
      • handling env variables
      • Adding a phpMyAdmin interface
    • AWS RDS MySQL Setup
      • rdsadmin overview
      • creating additional RDS users
      • connecting from a server
    • AWS EC2 Deployment
      • virtual machine setup
      • deploying prod version with:
        • Apache proxy with SSL Certification
        • OS daemonization
      • MySQL logical backups to AWS S3

    Dockerizing a LAMP-stack Application

    php-apache docker image

    I’ll assume the reader is somewhat familiar with Docker. I was given a code base in a dir called DatasetTracker developed several years ago with PHPv5. The first thing to do was to set up a git repo for the sake of development efficiency, which you can find here.

    Next, I had to try and get something working. The key with Docker is to find the official image and RTFM. In this case, you want the latest php-apache image, which leads to the first line in your docker file being: FROM php:7.4-apache. When you start up this container, you get an apache instance that will interpret php code within the dir /var/www/html and listening on port 80.

    creating dev and prod versions

    I decided to set up two deployment tiers: dev and prod. The dev tier is chiefly for local development, wherein changes to the code do not require you to restart the docker container. Also, you want to have php settings that allow you to debug the code. The only hiccup I experienced in getting this to work was understanding how php extensions are activated within a docker context. It turns out that the php-apache image comes with two command-line tools: pecl and docker-php-ext-install. In my case, I needed three extensions for the dev version of the code: xdebug, mysqli, and bcmath. Through trial and error I found that you could activate those extensions with the middle 3 lines in the docker file (see below).

    You can also set the configurations of your php to ‘development’ by copying the php.ini-development file. In summary, the essence of a php-apache docker file for development is as follows:

    FROM php:7.4-apache
    
    RUN pecl install xdebug
    RUN docker-php-ext-install mysqli
    RUN docker-php-ext-install bcmath
    
    RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini

    When you run a container based on this image, you just need to volume-mount the dir with your php code to /var/www/html to get instant updates, and to map port 80 to some random port for local development.

    Next, we need to write a docker-compose file in order to have this image run as a container along with a phpMyAdmin application, as well as to coordinate environment variables in order to connect to the remote AWS RDS mysql instance.

    An aspect of the set up that required a bit of thought was how to log into phpMyAdmin. The docker-image info was a bit confusing. In the end though, I determined that you really only need one env variable — PMA_HOST — passed to the phpMyAdmin container through the docker-compose file. This env variable just needs to point to your remote AWS RDS instance. phpMyAdmin is really just an interface to your mysql instance, so you then log in through the interface with your mysql credentials. (See .env-template in the repo.)

    (NOTE: you might first need to also pass env variables for PMA_USER and PMA_PASSWORD to get it to work once, and then you can remove these; I am not sure why this seems to be needed.)

    updating code from PHPv5 to PHPv7

    Once I had an application running through docker-compose, I was able to edit the code to make it compatible with PHPv7. This included, amongst other things, replacing mysql_connect with mysqli_connect, and replacing hard-coded mysql credentials with code for grabbing such values from env variables. A big help was using the VSCode extension intelephense, which readily flags mistakes and code that is deprecated in PHPv7.

    AWS RDS MySQL Setup

    rdsadmin overview

    Note: discussions about ‘databases’ can be ambiguous. Here, I shall use ‘DB’ or ‘DB instance’ to refer to the mysql host/server, and ‘db’ to refer to the internal mysql collection of tables that you select with the syntax `use [db name];`. As such, a mysql DB instance can have multiple dbs within it.

    In order to migrate the mysql database from our old Centos6 servers to an RDS instance, I first used the AWS RDS interface to create a mysql db instance.

    When I created the mysql DB instance via the AWS RDS interface, I assumed that the user I created was the root user with all privileges. But this is not the case! Behind the scenes, RDS creates a user called rdsadmin, and this user holds all the cards.

    To see the privileges of a given user, you need to use SHOW GRANTS FOR 'user'@'host'. Note: you need to provide the exact host associated with the user you are interested in; if you are not sure what the host is for the user, you first need to run:

    SELECT user, host FROM mysql.user WHERE user='user';

    In the case of an RDS DB instance, rdsadmin is created so as to only be able to log into the DB instance from the same host machine of the instance, so you need to issue the following command to view the permissions of the rdsadmin user:

    SHOW GRANTS for 'rdsadmin'@'localhost';

    I’ll call the user that you initially create via the AWS console the ‘admin’ user. You can view the admin’s privileges by running SHOW GRANTS; which yields the following result:

    GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, 
    DROP, RELOAD, PROCESS, REFERENCES, INDEX, 
    ALTER, SHOW DATABASES, CREATE TEMPORARY TABLES, 
    LOCK TABLES, EXECUTE, REPLICATION SLAVE, 
    REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, 
    CREATE ROUTINE, ALTER ROUTINE, CREATE USER, 
    EVENT, TRIGGER ON *.* TO `admin`@`%` 
    WITH GRANT OPTION

    The final part — WITH GRANT OPTION — is mysql for “you can give all of these permissions to another user”. So this user will let you create another user for each db you create.

    If you compare these privileges with those for rdsadmin, you’ll see that rdsadmin has the following extra privileges:

    SHUTDOWN, FILE, SUPER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE, SERVICE_CONNECTION_ADMIN, SET_USER_ID, SYSTEM_USER

    Several of these privileges — such as shutdown — can be executed via the AWS console. In summary, rdsadmin is created in such a way that you can never use it directly, and you will never need to. The admin user has plenty of permissions, and one needs to consider best practices as to whether to use the admin user when connecting from one’s application.

    I personally think that it is good general practice to have a separate db for each deployment tier of an application. So if you are developing an app with, say, a ‘development’, ‘stage’, and ‘production’ deployment tier, then it’s wise to create a separate db for each tier. Alternatively, you might want to have the non-production tiers share a single db. The one thing that I believe is certain though is that you need a dedicated db for production, that it needs to have logical backups (i.e. mysqldump to file) carried out regularly, and that you ideally never edit the prod db directly (or, if you do, that you do so with much fear and trembling).

    Is it a good practice to have multiple dbs on a single DB instance? This totally depends on the nature of the applications and their expected load on the DB instance. Assuming that you do have multiple applications using dbs on the same DB instance, you might want to consider creating a specialized user for each application in case compromise of one user compromises ALL your applications. In that case, the role of the admin is ONLY to create users whose credentials will be used to connect an application to the db. The next section shows how to accomplish that.

    creating additional RDS users

    So lets assume that you want to create a user who’s sole purpose is to enable an application deployed on some host HA (application host) to connect to the host on which the DB instance is running Hdb (db host). Enter the RDS DB instance with your admin user credentials and enter:

    CREATE USER 'newuser'@'%' IDENTIFIED BY 'newuser_password';
    GRANT ALL PRIVILEGES ON db_name.* TO 'newuser'@'%';
    FLUSH PRIVILEGES;

    This will create user ‘newuser’ with all of the privileges of the admin user. The ‘user’@’%’ syntax means “this user connecting from any host”.

    Of course, if you want to be extra secure, you can specify that the user can only connect from specific hosts by running this command multiple times replacing the wildcard ‘%’.

    As an aside, if you want to know the name of the host you are currently connecting from, then run:

    mysql> SELECT USER() ;
    +-------------------------------------------+
    | USER()                                    |
    +-------------------------------------------+
    | admin@c-XX-XX-XXX-XXX.hsd1.sc.comcast.net |
    +-------------------------------------------+
    1 row in set (0.07 sec)

    In this case, the host ‘c-XX-XX-XXX-XXX.hsd1.sc.comcast.net’ has been determined as pointing to my home’s public IP address (assigned by my ISP). (I assume that under the hood mysql has used something like nslookup MYPUBLIC_IPADDRESS to determine the hostname as it prefers that rather than my present IP address, which is assumed to be less permanent.)

    enabling user to change password

    As of Nov 2022, there seems to be an issue with phpmyadmin whereby a user thus created cannot change his/her own password through the phpmyadmin interface. Presumably under the hood the sql command to change the user’s password is such as to require certain global privileges (and this user has none). A temporary solution is to connect to the DB instance with your admin user and run:

    GRANT CREATE USER ON *.* TO USERNAME WITH GRANT OPTION; 

    connecting from a server

    One thing that threw me for a while was the need to explicitly white-list IP addresses to access the DB instance. When I created the instance, I selected the option to be able to connect to the database from a public IP address. I assumed that this meant that, by default, all IP addresses were permitted. However, this is not the case! Rather, when you create the DB instance, RDS will determine the public IP address of your machine (in my case – my laptop at my home public IP address), and apply that to the inbound rule of the AWS security group attached to the DB instance.

    In order to be able to connect our application running on a remote server, you need to go that security group in the AWS console and add another inbound-rule for MySQL/Aurora for connections from the IP address of your server.

    AWS EC2 Deployment

    virtual machine setup

    I chose Ubuntu server 20.04 for my OS with a single core and 20GB of storage. (The data will be stored in the external DB and S3 resources, so not much storage is needed.) I added 4GB of swap space and installed docker and docker-compose.

    apache proxy with SSL Certification

    I used AWS Route 53 to create two end points pointing to the public IP address of the EC2 instance. To expose the two docker applications to the outside world, I installed apache on the EC2 instance and proxy-ed these two end points to ports 5050 and 5051. I also used certbot to establish SSL certification. The apache config looks like this:

    <IfModule mod_ssl.c>
    <Macro SSLStuff>
        ServerAdmin webmaster@localhost
        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
        Include /etc/letsencrypt/options-ssl-apache.conf
        SSLCertificateFile /etc/letsencrypt/live/xxx/fullchain.pem
        SSLCertificateKeyFile /etc/letsencrypt/live/xxx/privkey.pem
    </Macro>
    
    <VirtualHost _default_:443>
        Use SSLStuff
        DocumentRoot /var/www/html
    </VirtualHost>
    
    <VirtualHost *:443>
        Use SSLStuff
        ServerName dataset-tracker.astro-prod-it.aws.umd.edu
        ProxyPass / http://127.0.0.1:6050/
        ProxyPassReverse / http://127.0.0.1:6050/
    </VirtualHost>
    
    <VirtualHost *:443>
        Use SSLStuff
        ServerName dataset-tracker-phpmyadmin.astro-prod-it.aws.umd.edu
        ProxyPass / http://127.0.0.1:6051/
        ProxyPassReverse / http://127.0.0.1:6051/
        RequestHeader set X-Forwarded-Proto "https"
        RequestHeader set X-Forwarded-Port "443"
    </VirtualHost>
    </IfModule>

    OS daemonization

    Once you clone the code for the applications to the EC2 instance, you can begin it in production mode with:

    docker-compose -f docker-compose.prod.yml up -d

    … where the flag ‘-d’ means to start it in the background (‘daemonized’).

    One of the nice things about using docker is that it becomes super easy to set up your application as a system service by simply adding restart: always to your docker-compose file. This command will cause docker to take note to restart the container if it registers an internal error, or if the docker service is itself restarted. This means that if the EC2 instance crashes or is otherwise restarted then docker (which, being a system service, will itself restart automatically) will automatically restart the application.

    MySQL logical backups to AWS S3

    Finally, we need to plan for disaster recovery. If the EC2 instance gets messed up, or the AWS RDS instance gets messed up, then we need to be able to restore the application as easily as possible.

    The application code is safe, thanks to github, and so we just need to make sure that we never lose our data. RDS performs regular disk backups, but I personally prefer to create logical backups because, in the event that the disk becomes corrupted, I feel wary about trying to find a past ‘uncorrupted’ state of the disk. Logical backups to file do not rely on the intergrity of the entire disk, and thereby arguably provide a simpler and therefore less error-prone means to preserve data.

    (This is in accordance with my general philosophy of preferring to backup files over than disk images. If something serious goes wrong at the level of e.g. disk corruption, I generally prefer to ‘start afresh’ with a clean OS and copy over files as needed, rather than to try and restore a previous snapshot of a disk. This approach also helps maintain disk cleanliness since disks tend to accumulate garbage over time.)

    To achieve these backups, create an S3 bucket on AWS and called it e.g. ‘mysql-backups’. Then install an open-source tool to mount S3 buckets onto a linux file system with sudo apt install s3fs.

    Next, add the following line to /etc/fstab:

    mysql-backups /path/to/dataset-tracker-mysql-backups fuse.s3fs allow_other,passwd_file=/home/user/.passwd-s3fs 0 0

    Next, you need to create an AWS IAM user with permissions for full programmatic access your S3 bucket. Obtain the Access key ID and Secret access key for that user and place them into a file /home/user/.passwd-s3fs in the format:

    [Access key ID]:[Secret access key]

    Now you can mount the S3 bucket by running sudo mount -a (which will read the /etc/fstab file).

    Check that the dir has successfully mounted by running df -h and/or by creating a test file within the dir /path/to/dataset-tracker-mysql-backups and checking in the AWS S3 console that that file has been placed in the bucket.

    Finally, we need to write a script to be run by a daily cronjob that will perform a mysql dump of your db to file to this S3-mounted dir, and to maintain a history of backups by removing old/obsolete backup files. You can see the script used in this project here, which was adapted from this article. Add this as a daily cronjob, and it will place a .sql file in your S3 dir and remove obsolete versions.

  • WordPress Backup Restoration Practice

    Intro

    This is Part II of a multi-part series on creating and managing a WordPress-NextJs app. In the first part, we set up a WordPress (WP) instance on AWS EC2 with OpenLiteSpeed, MySql on RDS, and S3 for our media storage. The WP site will serve as a headless CMS for a hypothetical client to manage his/her content; it will act as the data source for nextJs builds to generate a pre-rendered site deployed on AWS Cloudfront.

    In this part, having set up our WP backend, we will practice restoring the site in the event that the EC2 instance gets bricked.

    Rationale

    In this section, I am going to argue for why we only need to be able to recreate a corrupted EC2 instance given our setup so far.

    One of the advantages of (essentially) storing all of our application’s state in AWS RDS and S3 is that these managed services provide backup functionality out of the box.

    S3 has insane multi-AZ “durability” out of the box of with, to quote a quick search google, “at least 99.999999999% annual durability, or 11 nines… [; t]hat means that even with one billion objects, you would likely go a hundred years without losing a single one!” You could make your S3 content even more durable by replicating across regions but, this is plenty durable for anything I’ll be building any time soon.

    RDS durability requires a little more understanding. On a default setup, snapshots are taken of the DB’s disk storage every day, and retained for a week. RDS uses these backups to provide you with the means to restore the state of your DB to any second of your choosing between your oldest and most recent backups. This is your “retention period”. At extra cost, you can set the retention period to be as far back as 35 days, and you can also create manual snapshots to be stored indefinitely.

    These RDS backups, while good and all, do not guard against certain worst-case scenarios. If, say, your DB instance crashes during a write operation and corrupts the physical file, then you will need to restore a backup before the end of the retention period; this could mean that you will lose your data if you do not also regularly check the WP site to make sure everything is in order.

    Perhaps even worse is the case where some mishap occurs with your data that does not show up in an immediate or obvious way by visiting your site. For example, suppose you or a client installs a crappy plugin that deletes a random bunch; which you are not going to know about unless you do a thorough audit of your site’s content (and who’s got time for that!)

    For this reason, you also really want to create regular logical backups of your DB and save them to e.g. S3 Glacier.

    EC2 Restoration

    We’ll practice reconstructing a working EC2 instance to host our WP site. This section assumes that you have set up everything as prescribed in Part I of this series.

    First, in order to create a more authentic restoration simulation, you might want to stop your existing/working EC2 instance. Before you do that though, consider whether or not you have noted the details for connecting to your WP DB; if not, first note them down somewhere “safe”. Also be aware that if you did not associate an elastic-ip address with your EC2 instance, then it will get reset when you restart it later.

    Next, create a new EC2 Ubuntu 20.04 instance, open port 80 and 7080, ssh into it, and run the following:

    sudo apt update
    sudo apt upgrade -y
    curl -k https://raw.githubusercontent.com/litespeedtech/ols1clk/master/ols1clk.sh -o ols1clk.sh
    sudo bash ols1clk.sh -w

    The ols1clk.sh script will print to screen the details of the server and WP it is about to install. Save those details temporarily and proceed to install everything.

    In the AWS RDS console, go to your active mysql instance, go to its security group, and add an inbound rule allowing connections from your the security group attached to your new EC2 instance. Copy the RDS endpoint.

    Back in the EC2 instance, check that you can connect to the RDS instance by running:

    mysql --host RDS_ENDPOINT -u WPDB_USER -p

    … and entering the password for this user. Also make sure you can use the WP database within this instance.

    If the connection works, then open the file /usr/local/lsws/wordpress/wp-config.php in your editor and replace the mysql connection details with those corresponding to your RDS instance and WP DB_NAME. You also need to add the following two lines in order to override the fact that the DB is set with a site base url corresponding to your previous EC2 instance:

    define('WP_HOME', '[NEW_IP_ADDRESS]');
    define('WP_SITEURL', '[NEW_IP_ADDRESS]');

    … where NEW_IP_ADDRESS is taken from your new EC2 console.

    Now you can go to NEW_IP_ADDRESS in a browser and expect to find a somewhat functioning WP site. If you try navigating to post though you will get a 404 error. To fix this, you need to go into OLS Admin account on port 7080, login using the new credentials generated by ols1clk.sh, go to the “Server Configuration” section, and under the “General” tab, in the “Rewrite Control” table, set the “Auto Load from .htaccess” field to “Yes”. Now you can expect to be able to navigate around posts.

    (Side note: it’s very surprising to me that ols1clk.sh, in installing WP, does not set this field to Yes.)

    The images will not work of course, because the DB records their location at the address of the old EC2 instance, which is not running. So in an actual restoration scenario, we would need to point the previous hostname from the old instance to the new instance, and then set up S3fs (which I am not going to test right now).

    Having gone through this backup restoration practice, you can switch back to the old EC2 instance and, since we had to play around with our login credentials on a non-SSL site, it is a good idea to update your WP login password.

  • Headless WordPress with OpenLiteSpeed using AWS EC2, RDS & S3

    Intro

    I’ve heard good things about Litespeed so decided to try setting it up on AWS and to perform some comparisons with Apache. I also decided that I would try AWS RDS and S3 for data persistence.

    This article assumes general knowledge of setting up and administering an EC2 instance, and focuses on OpenLiteSpeed (OSL) setup.

    EC2 Setup

    I set up an EC2 instance with Ubuntu 20.04 LTS through the AWS Console. I chose 15GB of EBS storage, which I expect will be more than enough so long as this instance remains dedicated to one WordPress instance with data and media files stored externally (~5GB for OS, ~4GB for swap space, leaving ~5-6GB to spare). I’ve started off with 1GB RAM (i.e. the free-tier-eligible option).

    Then you need to ssh into your EC2 instance, do the usual set up (add swap space, add configurations for vim, zsh, tmux, etc.), and, if you plan for this to be a production WordPress site, then you’ll want to set up backups using the Life Cycle Manage.

    Installing OpenLiteSpeed

    Once your EC2 instance is configured for general usage, we need to install OpenLiteSpeed. Although it claims to be “compatible” with Apache, there are a lot of differences to setting it up and operating it. I used the official guide as well as this googled resource.

    NOTE: this section describes how to install OLS manually; see below for the option of installing OLS, php, wordpress, mysql, and LSCache through a convenience “one-click” script.

    First, to install “manually”, you need to add the relevant repository:

    wget -O - http://rpms.litespeedtech.com/debian/enable_lst_debian_repo.sh | sudo bash

    Then you can install the package:

    sudo apt install openlitespeed

    This will install to /usr/local/lsws (laws = “Lite Speed Web Server”) and includes a service allowing you to run:

    sudo systemctl start|stop|restart|status|enable|disable lsws

    OLS will be running and enabled upon installation. Installing OSL also installs a bunch of other packages, including a default version of php73, so OSL is ready to work with php out of the box.

    Managing OpenLiteSpeed

    Unlike apache — where all configuration is performed by editing files within /etc/apache2, OSL comes with an admin interface allowing you to configure most things graphically. This interface runs on port 7080 (by default) so, to access it on your EC2 instance, you need to open up port 7080 in your security group, and then navigate to your_ec2_ip_address:7080, where you will see a login form.

    Now, first time you do all of this, you will not have set up SSL certification, so any such login will be technically insecure. So my approach is to:

    Kick off with a weak/throwaway password to get stuff up initially, set up SSL, then switch to a proper/strong/long-term password, and hope that, during these few minutes, your ISP or some government power does not packet-sniff out your credentials and take over your OSL instance.

    To set up the initial credentials, run this CLI wizard:

    sudo /usr/local/lsws/admin/misc/admpass.sh

    Then use those credentials to login into the interface. By modifying settings here, you cause details within config files to get updated within /usr/local/lsws, so in theory you never need to directly alter settings in these files directly.

    By default, OSL will be running the actual server on port 8088. We want to change that to 80 to make sure things are working. So go to “Listeners”, click “View” on the Default listener, and edit the port to 80. Save and restart the server. Now you can got to your_ec2_ip_address in the browser to view the default server setup provided by OSL.

    This default code is provided in /usr/local/lsws/Example/html. Let’s create the file /usr/local/lsws/Example/html/temp.php with the contents:

    <?php
    echo "Is this working?";
    phpinfo();
    ?>

    And then go to your_ec2_ip_address/temp.php to confirm things are working. If you’ve followed these instructions precisely, then you’d expect to see something like this:

    A note on LiteSpeed & PHP

    The first time I tried installing OSL, I was rather confused on what one needed to do to get php working with it. The instructions I had come across told me to run the following commands after installing OSL:

    sudo apt-get install lsphp74
    sudo ln -sf /usr/local/lsws/lsphp74/bin/lsphp /usr/local/lsws/fcgi-bin/lsphp5

    This might have been necessary back in Ubuntu18.04, but with Ubuntu20.04 it is not necessary: installing OSL comes with the package lsphp73 already installed, so you only need to install this if you care about having php 7.4 over 7.3.

    It was also frustrating to be told to create the soft link given above without any explanation as to what it does or why it is needed. As far as I can discern, you need this soft link iff you want to specify the php interpreter to be used with fast-cgi scripts. But since I never deal with cgi stuff, I am pretty sure one can skip this.

    Furthermore, the instructions I read were incomplete. To get OSL to recognize the lsphp74 interpreter, you need to perform the additional step of setting the path in admin console. To do that, go to “Server Configuration” and the “External App” tab. There you need to edit the settings for the “LiteSpeed SAPI App” entry, and change the command field from “lsphp73/bin/lsphp” to “lsphp73/bin/lsphp”. Save, restart OSL, and check that the php version coming through in the temp.php page set up earlier is 7.4.

    SSL Setup

    I followed the instructions here, though they’re slightly out of date for Ubuntu20.04.

    Point a subdomain towards your EC2 instance; in this example, I’ll be using temp.rndsmartsolutions.com.

    Run sudo apt install certbot to install certbot.

    Run sudo cerbot certonly to kick off the certbot certificate wizard. When asked “How would you like to authenticate with the ACME CA?”, choose “Place files in webroot directory (webroot)”.

    Add your domain name(s) when prompted, then when it asks you to “Input the webroot for [your domain name]” , enter “/usr/local/lsws/Example/html“. This is the default dir that OLS comes with, and certbot will then know to add a temporary file there in order to coordinate with the CA server to verify that you control the server to which the specified domain name is pointing.

    If successful, certbot will output the certificate files onto your server. You now have to use the OLS console to add those files to your server’s configuration. Go to the “Listeners” section and under the “General” tab change the”Port” field to 443 and the “Secure” field to Yes. In the SSL tab set the “Private Key File” field to, in this example, /etc/letsencrypt/live/temp.rndsmartsolutions.com/privkey.pem and the “Cerficate File” field to /etc/letsencrypt/live/temp.rndsmartsolutions.com/fullchain.pem. Now restart the server and try navigating to your the domain that you just tried to setup with https and you can expect it to work.

    If SSL is working for the main server for the default Example virtual host on port 443, then you can use those same certificates for the WebAdmin server listening on port 7080. To do so, go to the “WebAdmin Settings > Listeners” section and view the “adminListener” entry. Under the SSL tab, set the “Private Key File” and “Certificate File” fields to the same values as above (i.e. pointing to our certbot-created certificates), and then save and restart the server. Now you can expect to be able to access the WebAdmin interface by visiting, in this example, temp.rndsmartsolutions.com:7080.

    Now that we can securely access the WebAdmin interface without the threat of packet sniffing, we can set a new password to something strong and longterm by going to “WebAdmin Settings > General” and under the “Users” tab we can view the entry for our username and a form for updating the password.

    Creating Further Virtual Hosts

    In general, we want to be able to add further web applications to our EC2 instance that funnel through the OSL server in one way or another. For that, we need to be able to set up multiple virtual hosts. Let’s start off with a super basic html one, and then explore the addition of more sophisticated apps.

    First, I’ll go to my DNS control panel (AWS Route 53) and add another record pointing the subdomain temp2.rndsmartsolutions.com to my EC2 instance.

    Now, in the WebAdmin interface, go to the “Virtual Hosts” section and click “+” to Add a new Virtual Host. Create a name in the “Virtual Host Name” field; this can be set to the text of the subdomain that, in this case, is “temp2”. In the “Virtual Host Root” field, set the path to the directory that you plan to use for your content. You need to create this dir on your EC2 instance; I tend to put them in my home folder so, in this case, I am using “/home/ubuntu/temp2”. While you’re there, create a dir called “html” in there and place a test index.html with some hello-world text. (You can determine the exact dir name to hold the root content by going to the “General” tab and setting the field “Document Root” to something other than “$VH_ROOT/html/”; in this case, we have set “$VH_ROOT” to “/home/ubuntu/temp2”.)

    Under the table titled “Security” within the “Basic” tab of “Virtual Hosts” section you probably want to also set things as depicted in the image below.

    Having created the Virtual Host entry for our new app, go to the “Listeners” section. Since earlier we changed the Default listener to have “Secure” value of “Yes” and the “Port” to have value “443”, we need to create a separate listener for port 80, that does not need to be secure. (At minimum we need the listener on port 80 in order for our next certbot call to be able to perform its verification steps.) So create such a listener and give it two “Virtual Host Mappings” to our two existing virtual hosts as depicted below.

    We now have two listeners — one for 80 and one for 443 — and both virtual hosts can be reached at their respective domains. Going now to temp2.rndsmartsoliutions.com is expected to show the hello-word index.html file created earlier.

    In order for SSL to work with out new virtual host, we need to expand the domains within our certificate files. So go to your SSH EC2 terminal and re-run certbot certonly as follows:

    sudo certbot certonly --cert-name temp.rndsmartsolutions.com --expand -d \
    temp.rndsmartsolutions.com,\
    temp2.rndsmartsolutions.com

    If all goes well, the certificates will get updated and once you restart the server you will be able to access your new virtual host at, in this example, https://temp2.rndsmartsolutions.com.

    (Note: you can add the SSL configurations to the virtual hosts rather than the listeners, but I prefer the latter.)

    Finally, we want to be able to tell OLS to redirect all traffic for a given virtual host from http to https. We’ll exemplify this here with the temp2 virtual host. Go to the “Virtual Hosts” section and view the temp2 virtual host. Under the “Rewrite” tab set the field “Enable Rewrite” to Yes in the “Rewrite Control” table. Then add the following apache-style rewrite syntax to the “Rewrite Rules” field:

    rewriteCond %{HTTPS} !on
    rewriteCond %{HTTP:X-Forwarded-Proto} !https
    rewriteRule ^(.*)$ https://%{SERVER_NAME}%{REQUEST_URI} [R,L]

    Restart the OSL server and you can now expect to be redirected to https next time you visit http://temp2.rndsmartsolutions.com.

    Certbot Renewal Fix

    This section was inserted several months later to fix a problem with the set up described in the last section. What I discovered was that adding these apache rewrite rules disrupted the way that certbot renews the certificate automatically. It wants to check you control the domain by adding temporary documents and then accessing them over http. However, these rewrite rules redirect you to https, which certbot doesn’t like (presumably because it doesn’t want to assume you can use https.)

    The fix I came up with is to disable the rewrite rules and, if I want to have an application that can only be accessed over https, then I create an additional virtual host listening on port 80 for the same domain, and then I just make that application a single index.php with the following redirection logic (taken from here):

    if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === "off") {
        $location = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
        header('HTTP/1.1 301 Moved Permanently');
        header('Location: ' . $location);
        exit;
    }

    So now, if you accidentally go to this domain, you will be redirected to the https listener, which will route you to the actual application you want users using at this domain. (And if someone accidentally goes to a non-root extension of this domain, then OLS will issue a “not found” error.)

    Installing WordPress with One-Click Install

    I discovered (somewhat after the fact) that you can actually install OLS, php, mysql, wordpress, and LSCache with a convenient script found here. To use, download and execute with:

    curl https://raw.githubusercontent.com/litespeedtech/ols1clk/master/ols1clk.sh -o ols1clk.sh
    sudo bash ols1clk.sh -w

    … where the -w flag indicates that you also want wordpress installation. This will prompt you with all of the items and credentials that the script plans to make, including installing mysql. If you have already installed OLS, it will wipe out your existing admin user+password with the new one declared, and will likely disrupt your Example settings, and create confusion by adding virtual hosts and/or listeners that conflict with what you’ve already set up. In short, do NOT use this script if you have already set up OLS — just install wordpress manually; Digital Ocean provides thorough guides for accomplishing this; e.g. see here.

    Once you have cleanly run the ols1clk.sh script, a virtual host will be ready for you, so go to the domain/ip-address for this instance, and you will encounter the set up wizard for wordpress. (Obviously, ideally, you first go through the relevant steps as laid out already for setting up SSL before going through the wizard.)

    However, before you do anything in the WP setup wizard, you need to change mysql DB configurations unless you want to host on your EC2 instance; we’ll outline that process in the next section.

    Installing WordPress Manually

    Having install OLS and lsphp manually already, and not wanting to disrupt my setup with the the ols1clk.sh script, I installed WP following the instructions here. I deviated from these instructions slightly since they are for apache and I am using OLS.

    One difference in particular is permissions … [TBC]

    AWS RDS Mysql Setup

    AWS using some confusing terminology. I normally think of a mysql DB as being the thing that you create with the sql command CREATE DATABASE name_of_db;, but when you click on “Create database” in the AWS console, what you are really setting up is/are the server(s) that host(s) what is, in general, a distributed mysql-server. Contra to AWS’s confusing nomenclature, I shall refer to these managed servers as the mysql or RDS “instance”, and the entities created therein via CREATE DATABASE as the “DBs”.

    Anyhow, click on “Create database” and go through the wizard to create a mysql instance. I am using the free-tier instance for now, with Mysql Community 8.0.23.

    You need to create a master user and password. You do not need a super strong password since we will also be setting the connectivity such that the instance can only be accessed from AWS resources sharing the same (Default) VPC. Since I only intend to connect to this instance from EC2 instances in the same VPC that are themselves very secure (I SSH in via SSL certs), we do not need another lay of “full” security to have to note down somewhere. (Obviously, if you want to connect from outside AWS, then you need super strong credentials.) We also choose default “Subnet group” and “VPC security group”.

    In the section “Additional Configuration” we have the choice to create an initial DB, but we will not since we will do that manually under a different non-admin username.

    Note how confusing AWS is here in stating that it “does not create a database” when the wizard will in fact create what AWS also calls a “database”

    After the console is done creating the instance, it will make available an endpoint of the form instance-name.ABCD.us-east-1.rds.amazonaws.com that we can use to test connecting from our EC2 instance.

    First, we want to ensure that we control precisely what EC2 instances we can connect from. In the RDS console, select the instance you just created, select the “Connectivity and security” tab, and scroll down to the “Security group rules” table. This will show you all of the rules for inbound/outbound traffic determined by the setting with in the security group assigned to the instance upon creation. You’ll want it to look like the following image:

    Click on that security group link to edit the associated inbound/outbound rules. For the sake of security, it’s sufficient to just limit all inbound traffic to the mysql instance, and leave all outbound traffic unrestricted. Here, I’ve limited the traffic to be inbound only from AWS resources using the specific security groups shown in the above image; these are associated with two separate EC2 instances that I set up.

    Back in the EC2 instance hosting my OLS server, install the mysql client with sudo apt install mysql-client. Then run:

    mysql --host END_POINT -u admin -p

    … where END_POINT is given to you in the RDS console for your mysql instance. You’ll be prompted for the admin password you created for the instance, and you can then expect to connect to the instance.

    We set up the mysql instance without an initial database, so lets now create one explicitly for our wordpress site. We also want to create username-password combo for specific use with this wordpress instance who only has permissions to read/write that db. Run the SQL commands:

    CREATE DATABASE dbname;
    CREATE USER 'newuser'@'%' IDENTIFIED BY 'password';
    GRANT ALL PRIVILEGES ON dbname . * TO 'newuser'@'%';
    FLUSH PRIVILEGES;

    … replacing dbname, newuser, and password with values of your choice. (Note the syntax ‘username’@’%’ means “a user with username connecting from any host”.) You can now exit this mysql session and try logging in again with the user you just created to make sure that that user is able to connect to the RDS instance remotely.

    Next, go to the dir in whcih you downloaded wordpress, and open the file wordpress/wp-config.php. Go down to the section for “MySQL settings” and enter the details for this user we just created, as well as the endpoint for the RDS instance in the DB_HOST slot. It also makes your WP more secure to change the $table_prefix variable to e.g. 'wp_sth_'.

    WordPress Troubleshooting

    If you have difficulty getting your wordpress to work, try adding the line:

    define( 'WP_DEBUG', true );
    

    … to true in wp-config.php to get an error stack. In my case, I had trouble getting php to recognize mysqli_connect; I got it working by installing sudo apt install lsphp74-mysql and restarting the OLS server. I also had some trouble getting php to recognize the php-curl module; I eventually got it working after running all sorts of installs (sudo apt install php-curl, etc.) and restarts, though I am not sure what the eventual solution was exactly (all I know is that I did not need to edit any OLS config files directly). After playing with OLS for several days now, I am tempted to say that you never want to edit an OLS config file directly; there is always a way to do things though the interface, or your WP/.htaccess files.

    Setting Up WordPress

    Once you have your WP interface working (i.e. you can login through a browser), you need to perform some essential set up.

    First, go to Settings > Permalinks and select “Post name”. Make sure the REST API is working by going to /wp-json/wp/v2/. If it is not working, try /index.php/wp-json/wp/v2/. If that works, then you need to get OLS to perform rewrites to skip the index.php part. OLS does not read .htaccess files by default (that WP supplies), so to get OLS to recognize those files go to your OLS admin, go the “Server Configuration” section, and in the “Rewrite Control” table, set the “Auto Load from .htaccess” field to “Yes” and restart. If your REST API is still not working, then, well, you’ve got some investigation to do.

    (I think it’s the case that because we are loading .htaccess files at the server level, OLS will read these files into memory upon first encounter, so subsequent use will be cached; if you set this setting at the virtual host level then OLS will consult the file system on each request.)

    I have heard it said that it’s a good idea to prevent users from executing the wp-config.php file. I think the idea here is that, by default, all it takes is an accidental deletion of the first line <?php for the file to be treated as plain text and, therefore, for its content to be printed to screen. The usual precaution against this on an Apache server is to add the following to the root .htaccess file:

    <Files wp-config.php>
        <IfModule mod_authz_core.c>
        	Require all denied
        </IfModule>
        <IfModule !mod_authz_core.c>
        	Order deny,allow
        	Deny from all
        </IfModule>
    </Files>

    However, this will not work with OLS because it “only supports .htaccess for rewrite rules, and not for directives”. We therefore need to add the following instead:

    RewriteEngine on
    RewriteRule ^wp-config\.php$ - [F,L]

    … and then confirm that you get a 403 Forbidden Error upon visiting /wp-config.php.

    Next, we want to install some vital plugins. I try to keep these plugins to as few as possible since I am wary of conflicts and bloat; our goal is to keep WP operating in a super-lean manner, but we still need some essentials.

    First, we want LSCache, since this is the big motivation of using OSL. The plugin claims to be an all-round site accelerator, cache-ing everything on all levels. When you install it, the has default settings with all site-acceleration features in place.

    Next, we want WordFence (WF) to provide site protection against attacks, as well as to provide free Two-Factor Authorization (2FA) logins. Install WF and enable auto-updates. WF will go into learning mode for a week or so.

    In order to set up 2FA you need a smart phone with client app. I will describe how to set up 2FA using the free “Duo Mobile” app on an iPhone. In the WF menu, go to “Login Security” and with your iPhone use the camera to scan the code. It will give you the option to open within Duo Mobile. Then, back in the WF interface, input the latest code from Duo Mobile for your WP site to activate it. Also download the recovery codes and keep them safe. Under the WF “Settings” tab for “Login Security”, I also make 2FA required for all types of user (though I only plan to use administrator and maybe Editor roles for this headless CMS). You can also require recaptcha via WF, but this is overkill for my purposes.

    WF will also want you to enable “Extended Protection” mode. If you agree to this, then it will prompt you to download your old version of the site’s .htaccess file (presumably in case WF screws it up if/when you uninstall it later). I am a bit skeptical about this feature since it sounds like it would incur quite a performance hit. However, since the overall architecture I am building here aims to put all of the serious site load onto AWS Cloudfront — with WP just functioning as the headless CMS for the convenience of the client — I have opted for now to add this extra layer of security.

    For this feature to be enabled on Litespeed, you need to follow these instructions, namely, go to the “Virtual Hosts” section and in the entry for your WP site, go to the “General” tab and add the following to the field “php.ini Override”:

    php_value auto_prepend_file /path/to/wp/wordfence-waf.php

    You may also need to go to LSCache and purge all caches.

    Offloading Media to AWS S3

    We want to offload the serving of our media files to AWS S3 with Cloudfront. This will also ensure that we can scale to any amount of media storage. We also want to avoid having duplicates on our EC2 disk.

    At first I assumed the best way to go about this would be through a WP plugin, and I tried out W3 Total Cache. However, these free plugins always seem to have a downside. In this case, W3 Total Cache would not automatically delete the file on disk after uploading to S3.

    I therefore decided to pursue a different strategy using S3fs. This is an open source apt-installable package that lets you mount an S3 bucket to your disk. Writing to disk in such a mounted volume therefore has the effect of uploading directly to S3, and leaving no footprint on your EC2 storage. You also don’t need any WP plugins.

    To set up S3fs on Ubuntu 20.04, first install it along with the AWS CLI:

    sudo apt install s3fs awscli

    In the AWS console (or via terraform if you prefer), create a new S3 bucket, Cloudfront distribution, and ACM SSL certificate. You can see this post for guidance on those steps, but note that, in this case, we are going to create a user with only the permissions needed to edit this particular S3 bucket.

    To create that user, go to the IAM interface in the AWS Console and click to “Add user”. Give the user “Programmatic Access”, then, under the “Set permissions” step, select “Attach existing policies directly” and the click on “Create policy”. Paste the following into the “JSON” tab:

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [
                    "s3:ListAllMyBuckets",
                    "s3:ListBucket"
                ],
                "Resource": "*"
            },
            {
                "Sid": "VisualEditor1",
                "Effect": "Allow",
                "Action": [
                    "s3:PutObject",
                    "s3:PutObjectAcl",
                    "s3:GetObject",
                    "s3:GetObjectAcl",
                    "s3:DeleteObject"
                ],
                "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
            },
            {
                "Sid": "VisualEditor3",
                "Effect": "Allow",
                "Action": "cloudfront:ListDistributions",
                "Resource": "*"
            }
        ]
    }

    [Note to self: will need to update this policy when it comes time to enable user to invalidate Cloudfront distributions]

    Skip tags and save the policy with a name like “my-wps3-editor-policy”. Now, back in the user-creation wizard, search for and select the policy you just created. Skip tags and create the user. You will then be able to access the key and secret key for programmatic use of this user.

    Back in the EC2 console, run the following to set this user as the user who will mount the S3 bucket (replacing the keys):

    touch ${HOME}/.passwd-s3fs
    echo ACCESS_KEY_ID:SECRET_ACCESS_KEY > ${HOME}/.passwd-s3fs
    chmod 600 ${HOME}/.passwd-s3fs

    We will be mounting the S3 bucket to wp-content/uploads within your WP dir. Before mounting, we need to enable other users to read/write to the mounted dir so that WP can properly sync the files we upload. To enable that, you need to edit the file /etc/fuse.conf by simply uncommenting the line user_allow_other.

    Now we can mount the dir BUT, before you do that, check if you already have content in the uploads dir. If you do, move that content to /tmp, and then make sure the uploads dir is empty, and then run the following:

    s3fs BUCKET_NAME /path/to/wp_dir/wp-content/uploads -o allow_other -o passwd_file=${HOME}/.passwd-s3fs

    Now you can copy back the contents that you may have moved, and/or upload something new, and expect it to appear within the corresponding S3 bucket.

    (Optionally, you can also run `aws configure`, and enter the credentials for this user, if you want to interact with the S3 bucket from the command line.)

    Finally, we want to mount this dir to S3 upon EC2 instance reboots, so add this to /etc/fstab file:

    BUCKET_NAME /path/to/wp_dir/wp-content/uploads fuse.s3fs allow_other,passwd_file=/path/to/home/.passwd-s3fs 0 0

    To test that it works, unmount uploads and then run sudo mount -a. If it looks like it works, you can then try actually rebooting, but be careful since messed up fstab files can brick your OS.

    Here are some final notes on using S3/S3fs:

    • You can setup a local image cache to speed up performance of serving files from WP, but performance doesn’t matter here since this will only be used by the CMS admin.
    • s3fs does not sync the bucket between different clients connecting to it; so if you want to create a distributed WP-site then you might want to consider e.g. yas3fs.
    • Depending on the amount of content on your site, you might benefit from creating a lifecycle rule on your S3 bucket to move objects from “Standard” storage to “Standard Infrequent Access” (SIA) storage. However, SIA will charge you for each file smaller than 128kb as though it were 128kb; since WP tends to make multiple copies of images of different sizes, which are often smaller than 128kb, this might offset your savings.

    URL Rewrites

    The last thing to consider is getting the end-user interface to serve up content from the Cloudfront URL instead of the WP URL. If you use a WP Plugin to sync with S3, then it will do the rewrites for you.

    In my case though, I am going to avoid having to work with php and do all the rewrites within my nextJs frontend app. See the next part for setting that up.

    Next Part

    The next part in the series is on practicing backup restoration.

  • AWS Production Static Site Setup

    Intro

    I’ve fumbled through the process of setting up a production static site on AWS a few times now. These are my notes for the next time to get through the process faster.

    Overview

    We want to be able to run a local script that wraps around the AWS CLI to upload assets to an AWS S3 Bucket (with user credentials with limited restrictions). The S3 Bucket is to be set up for serving a static-site and serve as the origin of a Cloudfront instance, which is itself aliased to a Route 53 hosted-zone record, all glued together with a ACM certificate.

    Finally, we need a script to copy over the contents of a directory for the static site to S3 in such a way as to compress all image files. In summary:

    • S3 Bucket
    • Cloudfront Instance
    • Certificate Manager Instance
    • Route 53 Configuration
    • AWS User for CLI uploads

    S3 Bucket

    Setting up an S3 Bucket is quite straightforward to accomplish in the AWS GUI Console. When asked about “Block all public access“, just uncheck, and don’t apply checks to any of the sub options. (Everyone I’ve seen just seems to ignore these convoluted suboptions without explanation.)

    Under permissions you need to create a bucket policy that will allow anyone to access objects in the bucket. So copy the ARN for the bucket (e.g. “arn:aws:s3:::rnddotcom-my-site-s3-bucket”) and use the “Policy Generator” interface to generate some JSON text as depicted below.

    Note: under the “Actions” option you need to select just the “GetObject” option. Click “Add Statement” and “Generate Policy” to get the JSON. Copy/paste it into the bucket’s policy text field and save. The following JSON is confirmed to work.

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "AddPerm",
                "Effect": "Allow",
                "Principal": "*",
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::rnddotcom-site-s3-bucket/*"
            }
        ]
    }

    Next, when you enable “Static website hosting”, you must specify the “Index document” since the S3 servers will not default to index.html.

    Upload Static Files (with gzip compression)

    When developing, I always want to be able to re-upload/re-deploy my software with a script. For that, I use a bash script that wraps around the AWS CLI. You can install it on a Mac with homebrew.

    For an example of such a script, see here in my terraform-aws-modules this repo. For this to work, you need to have AWS credentials for a user with access to this bucket.

    A good practice is to create a user with just enough permissions for the resources you need to access. So go to the AWS IAM console, and create a user with “Programmatic Access”.

    In the permissions step, click on “Attach existing policies directly” and select — in this example — the “AmazonS3FullAccess” policy and click on “Next: Tags”.

    Skip through Tags, create the user, and copy the “Access key ID” and “Secret access key” items to somewhere safe. If you are using the script I shared above, then you can add these items directly to your local .env file. By sourcing the .env file, you give these credentials priority over those stored in ~/.aws/credentials (which is handy if you manage multiple AWS accounts.)

    export AWS_ACCESS_KEY_ID="..."
    export AWS_SECRET_ACCESS_KEY="..."

    Now you can run the above bash script that wraps around the AWS CLI to upload the contents of a local directory. The script also includes logic to pick out image files and compress them before uploading.

    You now have a complete simple http static site, great for development, etc.

    Cloudfront I

    If you need a production site then you need to have SSL encryption (at minimum to look professional), CDN distribution, and a proper domain.

    So next go to Cloudfront in the AWS GUI Console and create a new “Distribution”. There are a lot of options here (CDN’s are complicated things after all), and you just have to go through each one and give it some thought. In most cases, you can just leave the defaults. A few notes are worth making:

    • Grant Read Permissions on Bucket“: No we already set these up
    • Compress Objects Automatically“: Select yes; here is a list of type of file that CloudFront will compress automatically
    • Alternate Domain Names (CNAMEs)“: Leave this blank — sort it out after creating a distribution
    • Default Root Object“: Make sure to set this to index.html
    • Viewer Protocol Policy“: Set this to “Redirect HTTP to HTTPS” (as is my custom)

    SSL Certification

    Now we need to point host name to the CloudFront distribution. Surprisingly, it seems you NEED to have SSL, and to have it setup first for this to happen. So go to ACM and click on “Request a Certificate”. Select “Request a public certificate” and continue.

    Add your host names and click continue. Assuming you have access to the DNS servers, select “DNS Validation” and click ‘next’. Skip over tags and click on “Confirm and Request”.

    The next step will be to prove to AWS ACM that you do indeed control the DNS for the selected hosts you wish to certify. To do this, the AWS console will provide details to create DNS records whose sole purpose will be for ACM to ping in order to validate said control.

    Screenshot

    You can either go to your DNS server console and add CNAME records manually, or, if you’re using Route 53, just click on “Create record in Route 53”, and it will basically do it automatically for you. Soon thereafter, you can expect the ACM entry to turn from “Pending validation” to “Success”.

    Cloudfront II

    Now go back and edit your Cloudfront distribution. Add the hostname to the space “Alternate Domain Names
    (CNAMEs)“, choose “Custom SSL Certificate (example.com)”, and select the certificate that you just requested, and save these changes.

    Route 53

    Finally, go to the hosted zone for your domain in Route 53, and click on “Create Record”. Leave the record type as “A” and toggle the “Alias” switch. This will transform the “Value” field to a drop down menu letting you select “Route traffic to”, in this case, “Alias to Cloudfront distribution”, and then a region, and then in the final drop down you can expect to be able to select the default url to the CloudFront instance (something like “docasfafsads.cloudfront.net”).

    Hit “Create records” and, in theory, you have a working production site.

    NextJs Routing

    If you are using nextJs to generate your static files then you will not be able to navigate straight to a page extension because, I have discovered, the nextJs router will not pass you onto the correct page when you fall back to index.js, as it would if you’re using e.g. react router. There are two solutions to this problem, both expressed here.

    • Add trailing slash to all routes — simple but ugly solution IMO
    • (Preferred) Create a copy of each .html file without the extensions whenever you want to reupload your site; requires extra logic in your bash script

    Trouble Shooting

    • The terraform-launched S3 bucket comes with the setting “List” in the ACL section of permissions tab; it’s not clear to me what difference this makes.
    • I was getting a lot of 504 errors at one point that had me befuddled. I noticed that they would go away if I first tried to access the site with http and then with https. I was saved by this post, and these notes that I was then prompted to find, that brought my attention to a setting that you cannot access in the AWS GUI Console called “Origin Protocol Policy”. Because I originally created the Cloudfront distribution with terraform, which can set this setting, and it set it to “match-viewer”, the Cloudfront servers were trying to communicate with S3 with the same protocol that I was using. So when I tried to view the site with https and got a cache miss on a file, Cloudfront would try to access the origin with https; but S3 doesn’t handle https, so it would fail. When I tried using http, Cloudfront would successfully get the file from S3, so that the next time I tried with https I would get a cache hit. Now, since I don’t like using http in general, and in fact switched to redirecting all http requests to https, I was stuck until I modified my terrafform module to change the value of Origin Protocol Policy to http-only. I do not know what the default value of Origin Protocol Policy is when you create a Cloudfront distribution through the Console — this might be a reason so always start off with terraform.