Warning
This architecture is presented only as an example. Additional configuration may be required based on your needs and security requirements
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.yaml
his
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
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
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).
The ALB performs a health check by accessing the built-in Craft health check route at /actions/app/health-check
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
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.
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
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 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 |
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).
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
Name | Version |
---|---|
aws | 5.84.0 |
cloudflare | ~> 5 |
Name | Version |
---|---|
aws | 5.84.0 |
cloudflare | 5.0.0 |
random | 3.6.3 |
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 |
{ |
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 beidentical 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 bechanged in later chained modules. Attempts to change it will be silently ignored. |
set(string) |
[ |
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 |