February 24, 2021

Wordpress CDK

Following on from the second installment of our experiment to try and find which processor in the EC2 family of instances has the best price/performance we need to have a way to reliably build our WordPress stack for testing.

CDK

I’ve been using CloudFormation for many years now and have used a number of methods to orchestrate this. (Yes I wrote CloudFormation in JSON…). Reasonably early on I picked up and started to use CFNDSL to help construct CloudFormation and even wrote some an Ansible role and some very dodgy Ruby Rakefiles (sorry no public examples) to help orchestrate the CFNDSL code into CloudFormation and then manage validation (when cfn-lint became a thing) and ultimately build ChangeSets and deploy it.

Today though, I’m working more with CDK. There are plenty of write ups by my fellow ambassadors Wolfie and Arjen, so I won’t go too deep today.

To get started with CDK you need to install it and then initiate a project (big assumption you have NodeJS installed):

npm install -g aws-cdk@latest
cdk init app --language=python

VPC

Firstly we need a network (VPC) to run everything in.

        vpc = ec2.Vpc(self, "vpc",
                nat_gateways=0
                )

And that’s it.

RDS

Next we need an RDS instance. To be able to spin up using the Graviton based processors we need to use traditional RDS instead of Aurora.

        db = rds.DatabaseInstance(self, "db",
            engine=rds.DatabaseInstanceEngine.mysql(
                version=rds.MysqlEngineVersion.VER_8_0
            ),
            vpc=vpc,
            availability_zone=vpc.availability_zones[0],
            instance_type=db_instance_type,
            removal_policy=core.RemovalPolicy.DESTROY,
            vpc_subnets=ec2.SubnetSelection(one_per_az=True, subnet_type=ec2.SubnetType.ISOLATED),
            deletion_protection=False
            )

EC2 Instance

Next we need to create an EC2 Instance.

        app = ec2.Instance(self, "app",
            instance_type=app_instance_type,
            machine_image=machine_image,
            vpc=vpc,
            vpc_subnets=ec2.SubnetSelection(one_per_az=True, subnet_type=ec2.SubnetType.PUBLIC),
            availability_zone=vpc.availability_zones[0],
            key_name="gjc",
            user_data_causes_replacement=True
                )

Allow all the connections

Now that we have a RDS and an EC2 instance, we need to allow the security groups to connect.

        db.connections.allow_from(app, ec2.Port.tcp(3306))
        app.connections.allow_from_any_ipv4(ec2.Port.tcp(22))
        app.connections.allow_from_any_ipv4(ec2.Port.tcp(80))

This allows the connection from the app server to the db on port 3306 and allows port 22 (in case we need it) and 80 to get to the http server.

Some Tricks

Let’s enabled detailed monitoring. To do this we need to use an escape hatch:

        cfn_app = app.node.default_child

        cfn_app.monitoring = True

We also need to be able to retrieve the secret that was created when the RDS was created, so let’s do that.

        app.role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name('AmazonSSMManagedInstanceCore'))
        app.add_to_role_policy(
                iam.PolicyStatement(
                    actions=["secretsmanager:GetSecretValue"],
                    resources=[db.secret.secret_full_arn]
                    )
                )

User-data

Let’s install WordPress and configure it:

region=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/[a-z]$//')
aws --region $region secretsmanager get-secret-value --secret-id '''+db.secret.secret_full_arn+''' >> /tmp/details
yum install -y jq httpd mariadb
amazon-linux-extras install -y php7.2
systemctl enable httpd
systemctl start httpd
cd /tmp; wget https://wordpress.org/latest.zip
cd /var/www/html
unzip /tmp/latest.zip
echo "<?php" > wordpress/wp-config.php
echo "define( 'DB_NAME', 'wordpress' );" >> wordpress/wp-config.php
echo "define( 'DB_USER', '$(cat /tmp/details | jq -r '.SecretString' | jq -r '.username')' );" >> wordpress/wp-config.php
echo "define( 'DB_PASSWORD', '$(cat /tmp/details | jq -r '.SecretString' | jq -r '.password')' );" >> wordpress/wp-config.php
echo "define( 'DB_HOST', '$(cat /tmp/details | jq -r '.SecretString' | jq -r '.host')' );" >> wordpress/wp-config.php
echo "define( 'DB_CHARSET', 'utf8' );" >> wordpress/wp-config.php
echo "define( 'DB_COLLATE', '' );" >> wordpress/wp-config.php
curl -s https://api.wordpress.org/secret-key/1.1/salt/ >> wordpress/wp-config.php
echo "\$table_prefix = 'wp_';" >> wordpress/wp-config.php
echo "define( 'WP_DEBUG', false );" >> wordpress/wp-config.php
echo "if ( ! defined( 'ABSPATH' ) ) {" >> wordpress/wp-config.php
echo "        define( 'ABSPATH', __DIR__ . '/' );" >> wordpress/wp-config.php
echo "}" >> wordpress/wp-config.php
echo "require_once ABSPATH . 'wp-settings.php';" >> wordpress/wp-config.php
echo "create database wordpress;" | mysql -h$(cat /tmp/details | jq -r '.SecretString' | jq -r '.host') -u$(cat /tmp/details | jq -r '.SecretString' | jq -r '.username') -p$(cat /tmp/details| jq -r '.SecretString' | jq -r '.password')
curl "http://$(curl http://ipv4.icanhazip.com)/wordpress/wp-admin/install.php?step=2" --data-raw 'weblog_title=Wordpress&user_name=admin&admin_password=oKdy7%212cVumXZLzW8%29&admin_password2=oKdy7%212cVumXZLzW8%29&admin_email=admin%40example.com&blog_public=0&Submit=Install+WordPress&language='

CPU Type Management

We have three options of CPU to choose from for EC2 and two for RDS. To manage this we use environment variables:

        # default is to use graviton CPUs
        machine_image = ec2.MachineImage.latest_amazon_linux(
                generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
                cpu_type=ec2.AmazonLinuxCpuType('ARM_64')
            )

        app_instance_type = ec2.InstanceType.of(
                ec2.InstanceClass.COMPUTE6_GRAVITON2,
                ec2.InstanceSize.LARGE,
            )

        db_instance_type = ec2.InstanceType.of(
                ec2.InstanceClass.MEMORY6_GRAVITON,
                ec2.InstanceSize.LARGE,
            )

        if 'APPCPU' in os.environ:
            if os.environ.get('APPCPU') == 'INTEL':
                machine_image = ec2.MachineImage.latest_amazon_linux(
                        generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
                        cpu_type=ec2.AmazonLinuxCpuType('X86_64')
                    )

                app_instance_type = ec2.InstanceType.of(
                        ec2.InstanceClass.COMPUTE5,
                        ec2.InstanceSize.LARGE,
                    )
            if os.environ.get('APPCPU') == 'AMD':
                machine_image = ec2.MachineImage.latest_amazon_linux(
                        generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
                        cpu_type=ec2.AmazonLinuxCpuType('X86_64')
                    )

                app_instance_type = ec2.InstanceType.of(
                        ec2.InstanceClass.COMPUTE5_AMD,
                        ec2.InstanceSize.LARGE,
                    )

        if 'DBCPU' in os.environ:
            if os.environ.get('DBCPU') == 'INTEL':
                db_instance_type = ec2.InstanceType.of(
                        ec2.InstanceClass.MEMORY5,
                        ec2.InstanceSize.LARGE,
                    )
            # sadly there is no AMD RDS instance types

Conclusion

And that’s pretty much it. CDK makes writing reusable infrastructure really simple and allows us to manipulate the configuration with environment variables and to treat our infrastructure as objects, which allows us to perform actions like enabling monitoring or adding user-data.

The above code is all put together in github with instructions on how to use it if you want to give this ago yourself.

© Greg Cockburn

Powered by Hugo & Kiss.