Skip to content

Example infrastructure for deploying Craft CMS on AWS EC2

Notifications You must be signed in to change notification settings

JJimmyFlynn/craft-infra-ec2

Repository files navigation

Warning

This architecture is presented only as an example. Additional configuration may be required based on your needs and security requirements

Architecture Overview

Ansible & Packer

Packer is used to generate an AMI which can be used by EC2 instances. The configuration of this AMI is done through Anisble. There are several roles that are used by the playbook to configure a basic PHP webserver. The roles configure Nginx and PHP-fpm as well as several security defaults. Ansible variables control the logic of the roles. An example of these settings can be found in /ansible/php-webserver/vars.yamlhis

Terraform Variables

Configuration of terraform for a given environment is done via terraform variables. Multiple different environment configurations can be configured by using separate .tfvars files such as dev.tfvars as Terraform workspaces to configure separate environments with separate state. In the real world, a more robust Terraform automation solution should be used.

Spacelift is an excellent choice, as is Env0

VPC

Note

This architecture assumes a fresh VPC. In a real life scenario you may want to utilize an existing VPC.

This application's VPC aims to be as private as possible. The application EC2 instances live within a private subnet and utilize NAT gateways and VPC Endpoints to communicate with other AWS services and the internet. This architecture provisions VPC Endpoints for the following services:

  • S3
  • Cloudwatch
  • SSM
  • SSM Messages

The VPC requires at least 2 AZs. The total number of AZs utilized can be set with the vpc_az_count terraform variable

Load Balancer

An ALB is configured to route traffic to the application EC2 instances target group. As explained in the CloudFront section, this application is frontend by CloudFront and the ALB only allows incoming connections from the CloudFront Service. The ALB listens on HTTPS (Cloudfront is configured to redirect all HTTP traffic to HTTPS).

Health Checks

The ALB performs a health check by accessing the built-in Craft health check route at /actions/app/health-check

S3

This architecture provisions 2 S3 buckets. One for the application's uploaded assets and one for the build artifact from the CI/CD pipeline

The assets S3 bucket's Bucket Policy is configured to make the bucket accessible only to the CloudFront distribution and to the VPC Endpoint for S3

CloudFront

A CloudFront distribution is placed in front of the ALB and therefore all ingress traffic for the application goes through it.

The distribution has two origins: one for the applications assets (which are stored at a /assets path and routed based on that path) which is pointed at the S3 bucket for assets, and one for every other path which is pointed at the ALB.

The ALB only accepts ingress traffic from the CloudFront distribution, ensuring all traffic is subject to CloudFront's protections and caching.

WAF

The CloudFront distribution is also backed by a Web ACL from AWS WAF. This adds further protection to all ingress traffic to the application.

The Web ACL is configured to use a set of AWS managed rulesets:

  • Common Rules
  • Known Bad Inputs
  • Common SQL
  • Common Linux
  • Common Unix
  • Common PHP
  • IP Reputation
  • Bot Protection

EC2

The heart of this infrastructure is EC2. The VMs are deployed with a Launch Template using a custom AMI created by Packer/Ansible.

All instances are deployed within private subnets and utilize VPC enpoints for communication with necessary AWS services.

The instances are deployed in an auto-sacling group which uses a tracking policy to monitor the average CPU utilization and manage the number of instances. A clound init script fetches the latest application release artifact from an S3 bucket on instance creation and grabs the environment variables necessary to run the application from SSM Parameter Store.

Security Groups

Security Group Associated Resources Inbount Rules Outbound Rules
Load Balancer Load Balancer Port 443 from Cloudfront Port 80 to Webserver SG
Webserver Webserver EC2 Instances Port 80 from Load Balancer SG Port 80 to VPC Endpoint SG
Port 443 to VPC Endpoint SG
Port 80 to S3 Prefix List
Port 443 to S3 Prefix List
Port 3306 to RDS SG
Port 6379 to Redis SG
VPC Endpoints All non-S3 VPC Endpoints Port 80 from Webserver SG
Port 443 from Webserver SG
None
RDS RDS Cluster Port 3306 from Webserver SG None
Redis Elasticache Redis Cluster Port 6379 from Webserver SG None

CI/CD

Note

In a real world scenario, the example application and related Github workflows would likely live in a separate git repo. It is included here for completeness.

CI/CD for the example application is performed using Github Actions. The deployment to an environment is composed of two reusable actions:

  • Artifact Build (reusable-build.yaml)
    • This action builds the Composer and NPM dependencies for the application. It then packages the built application into a tarball and uploads it to an artifact S3 bucket. It also updates a SSM parameter store value with the name of the tarball for consumption for the EC2 init script
  • Instance Refresh (reusable-instance-refresh.yaml1)
    • This action initiates an Instance Refresh on the app's autoscaling group. In short, this is like a rolling deployment. It will spin up new instances up to a max-healthy percentage threshold, wait for those instance to become healthy, and then terminate older instances. This continues until all instances have been refreshed (i.e. replaced).

Github Environments

The Github actions utilize Github's Environments feature. Each environment defines its own secrets and variables and are utilized by actions associated with that environment.

Additionally, environments are associated with branches and their variables/secrets are only available to actions which run against those branches.

  • Dev environment => dev branch
  • Staging environment => staging branch
  • Prod environment => prod branch

Requirements

Name Version
aws 5.84.0
cloudflare ~> 5

Providers

Name Version
aws 5.84.0
cloudflare 5.0.0
random 3.6.3

Inputs

Name Description Type Default Required
additional_tag_map Additional key-value pairs to add to each map in tags_as_list_of_maps. Not added to tags or id.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration.
map(string) {} no
attributes ID element. Additional attributes (e.g. workers or cluster) to add to id,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the delimiter
and treated as a single ID element.
list(string) [] no
aurora_instance_count The total number of instances to create in the RDS cluster number 1 no
aurora_max_capacity The maximum ACUs used in the RDS autoscaling policy. Must be more than aurora_min_capacity. Range of 1 - 128 number 4 no
aurora_min_capacity The minimum ACUs used in the RDS autoscaling policy. Must be less than aurora_min_capacity. Range of 0.5 - 128 number 1 no
autoscaling_cpu_tracking_target The target average CPU usage of the autoscaling group used in the target tracking autoscaling policy number 60 no
autoscaling_max_quantity Maximum ec2 instances for the autoscaling group number 3 no
autoscaling_min_quantity Minimum ec2 instances for the autoscaling group number 1 no
cloudflare_api_token n/a string n/a yes
cloudflare_zone_id n/a string n/a yes
context Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as null to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional_tag_map, which are merged.
any
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
no
delimiter Delimiter to be used between ID elements.
Defaults to - (hyphen). Set to "" to use no delimiter at all.
string null no
descriptor_formats Describe additional descriptors to be output in the descriptors output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
{<br/> format = string<br/> labels = list(string)<br/>}
(Type is any so the map values can later be enhanced to provide additional options.)
format is a Terraform format string to be passed to the format() function.
labels is a list of labels, in order, to pass to format() function.
Label values will be normalized before being passed to format() so they will be
identical to how they appear in id.
Default is {} (descriptors output will be empty).
any {} no
domain Domain name of the application and ACM cert string n/a yes
enabled Set to false to prevent the module from creating any resources bool null no
environment ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' string null no
id_length_limit Limit id to this many characters (minimum 6).
Set to 0 for unlimited length.
Set to null for keep the existing setting, which defaults to 0.
Does not affect id_full.
number null no
label_key_case Controls the letter case of the tags keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the tags input.
Possible values: lower, title, upper.
Default value: title.
string null no
label_order The order in which the labels (ID elements) appear in the id.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present.
list(string) null no
label_value_case Controls the letter case of ID elements (labels) as included in id,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the tags input.
Possible values: lower, title, upper and none (no transformation).
Set this to title and set delimiter to "" to yield Pascal Case IDs.
Default value: lower.
string null no
labels_as_tags Set of labels (ID elements) to include as tags in the tags output.
Default is to include all labels.
Tags with empty values will not be included in the tags output.
Set to [] to suppress all generated tags.
Notes:
The value of the name tag, if included, will be the id, not the name.
Unlike other null-label inputs, the initial setting of labels_as_tags cannot be
changed in later chained modules. Attempts to change it will be silently ignored.
set(string)
[
"default"
]
no
name ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a tag.
The "name" tag is set to the full id string. There is no tag with the value of the name input.
string null no
namespace ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique string null no
parameter_store_path The path at which ssm parameters are stored for this application/stage. e.g. /example-application/dev string n/a yes
redis_instance_count n/a number 1 no
redis_node_type n/a string "cache.r7g.large" no
regex_replace_chars Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, "/[^a-zA-Z0-9-]/" is used to remove all characters other than hyphens, letters and digits.
string null no
region The AWS region where resource will be created string n/a yes
role_arn ARN of the role for terraform to assume string n/a yes
stage ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' string null no
tags Additional tags (e.g. {'BusinessUnit': 'XYZ'}).
Neither the tag keys nor the tag values will be modified by this module.
map(string) {} no
tenant ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for string null no
vpc_az_count The number of private and public subnets to be created. Each will be provisioned into their own AZ number 2 no

About

Example infrastructure for deploying Craft CMS on AWS EC2

Topics

Resources

Stars

Watchers

Forks