People, Networks, and Identity are three crucial areas of security.
Recent articles about large corporations exposing millions of customers’ data have been in the news lately. The fascinating forensics from some of the breaches was a determination that a contractor working remotely was hacked and exposed a tremendous amount of data that ended up on the dark web.
I find myself wondering what sort of protections these companies and their contractors put in place to protect the company’s intellectual property and, more importantly, the company’s customer data.
I keep my remote work environment separate from home computers. I keep social media, email, and personal information away from the work environment. My home office is set up on a separate network! The reason is simple if the company becomes compromised, the compromise will not affect my family. Conversely, suppose anyone in my family gets hacked, and a hacker breaches our home network. In that case, I have at least separated the company network to reduce the possibility that the hack will not impact the company.
I keep my smart TVs, smart speakers, and home appliances on a separate network, not just because it protects the company network but also because it makes sense to keep smart devices on a separate network. This article will cover how to design separate office and home networks as remote worker.
Let’s start with the Internet connection. I prefer a firewall that can provide at least three separate networks. A network that becomes the wireless network for my family and my personal computers, another network for my work, and another network for smart appliances. Additionally, I have a DMZ network to potentially expose applications from my Kubernetes cluster or my vSphere server.
My remote office uses one firewall and one wireless router to separate networks.
We have personal computers for each member of my family connected to the family network. I have a WiFi router providing access from the family network to our phones, laptops, printers, and computers.
The WiFi router also has the option to allow guests. Many routers support a feature called guest networking, whichย creates a separate Wi-Fi networkย for friends and family to use when they visit. They can access the internet from the guest network, but they can’t access network resources like shared folders, printers, or NAS devices. All smart devices (TVs, smart speakers, light controllers, smart thermostats, Ring doorbells) are set up on the guest network such that they do not have access to our family computers.
The firewall is set up to provide two more networks, a DMZ and an Office Network. The Work laptop is connected to the Office network and thus separated from the family network and the smart devices. Additionally, the work laptop has a VPN into the corporate network to secure its connection across the internet. I do allow the work laptop access to the family printers by setting a rule only to allow print jobs but no connectivity to family computers.
I have a refurbished Dell server that is set up with VMWare vSphere. There was a sale to get a refurbished Dell server with 192 GBytes of Memory and 2 Xeon processors, and no disks that I found on eBay for $250. I stuck a couple of 1 Terabyte SSD drives into it, and the refurbished server does a fantastic job running as a vSphere server in the lab!
Additionally, I have five refurbished Dell mini-tower computers from eBay, each cheaper than purchasing several Rasberry Pi mini-computers. The refurbished Dell desktops have far more memory and computing power than a Rasberry Pi. Four of the desktops are used as a Kubernetes cluster, and the fifth computer is used for backups.
The vSphere server and the Kubernetes cluster are placed into the DMZ network. This allows me to expose application development to the internet with less chance of a compromise impacting the home or office networks.
CI/CD stands for Continuous Integration/Continuous Deployment, which is a software development approach that emphasizes the automation of building, testing, and deploying code changes to production as quickly and frequently as possible. CI/CD aims to streamline the software development process, increase collaboration among team members, and reduce the risk of errors and conflicts during development and deployment.
The key phrase from the description above is “automation,” “deploying changes” to production “quickly,” and “frequently.” Infrastructure as Code supports CI/CD processes by deploying development, test, QA, or production infrastructure environments using code. More importantly, these environments can be ad-hoc CI/CD environments, except for production, which most likely needs to run 24/7; all the other environments can be quickly built when required, and just as quickly dismantled. Development, test, and QA environments can be destroyed until the underlying infrastructure is required again.
Software engineers care about the application’s code, and not so much about the underlying infrastructure that supports the application. However, management cares very much about the cost. Security cares about the “security” of the underlying infrastructure. This is where IaC comes to the rescue. It can deploy the infrastructure using CI/CD processes. IaC can perform infrastructure deployments in minutes using a repeatable, version-controlled, pre-approved, and security-compliant infrastructure.
This is where “Infrastructure as Code (IaC)” benefits everyone.
Benefits of IaC include:
Consistency: IaC enables consistent and repeatable infrastructure provisioning across multiple environments and deployments.
Scalability: IaC makes it easy to scale infrastructure up or down as needed, based on changes in demand or usage patterns.
Automation: IaC reduces manual efforts and errors by automating repetitive tasks such as configuration and deployment.
Version control: IaC allows infrastructure changes to be version-controlled, making it easy to track changes, roll back to previous versions, and collaborate among team members.
Cost savings: IaC can reduce infrastructure costs by enabling developers and system administrators to optimize infrastructure usage and avoid overprovisioning.
Infrastructure as code (IaC) is a software development practice that involves managing and provisioning IT infrastructure using code and automation tools. Instead of manually setting up and configuring servers, networks, and other infrastructure components, IaC enables developers and system administrators to define their infrastructure in code, which can be version-controlled, tested, and deployed like any other software.
Believe it or not, IAC also enables developers, administrators, or engineers more freedom during the development or testing of new systems. IaC allows developers and engineers to deploy pre-approved infrastructure in support of their development and test environments quickly, efficiently, and without manual deployment methods; it can be completely automated. IaC infrastructure deployments can even be included as a procedural step in tools such as Jenkins, Circle CI, Gitlab, etc.
Reduction in operating expense Infrastructure as Code (IaC) can create a complete infrastructure in minutes in a repeatable, consistent, and agreed configuration. In contrast, manual configuration can take tremendously longer (because it is a manual process). And prone to errors. Engineers will use agreed IaC platforms, reducing the probability of deploying infrastructure that is not required or improperly sized.
Better use of Time (Manpower cost savings) An automated installation allows all involved to focus on critical, high-value tasks (not spending half a day manually setting infrastructure, for example). Or worse, it can eliminate permanent infrastructure because a manual setup of infrastructure is prone to error and time-consuming; there is a tendency to leave it running. Instead, IaC can be deployed only when required and destroyed until the infrastructure is required once again.
Disposable Environments (CapEx cost savings) Improving the velocity, Iac makes the build of infrastructure more efficient by allowing someone to quickly set up a complete infrastructure in minutes (not hours).
Terraform (& Ansible) to the rescue
An excellent article by one of the employees at Gruntwork.io is an excellent read about Terraform. The article is about why Gruntworks use Terraform as opposed to other tools. The article logically discusses choosing between Chef, Puppet, Ansible, SaltStack, Cloudformation, and Terraform.
First, let me state for the record, this process does not belong exclusively to software development (not by a long shot). Just having the ability for any IT department to create test and production environments that utilize a documented, repeatable, standard configuration, and easily migrated from test into production, in my opinion, has to be attractive to any IT shop.
โInfrastructure as Codeโ scripts work with the most popular cloud platforms and on-premise platforms. Note: While Infrastructure as Code works with many platforms, the scripts are not automatically transferable from one platform to another platform.
Terraform is platform-agnostic; you can use it to manage bare metal servers or cloud servers like AWS, Google Cloud Platform, OpenStack, and Azure. Or on-prem in private clouds such as VMWare vSphere, OpenStack, or CloudStack. In Terraform lingo, the supported platforms are called providers.
Terraform & Ansible coding empowers conventional businesses, software development businesses, and small startup businesses, all of the above, to deploy standardized, immutable, and repeatable infrastructure into an on-premises data center or cloud environment using Infrastructure as Code. The code is put into configuration management and stored in a repository for all engineers to deploy infrastructure configuration from development through QA tests and release it into production.
The following code will provide a simple example. The first bit of code will create a VPC with one public subnet. We’ll have our declaration of using Terraform Remote State in that very same folder. We will also create an “output” to our Terraform Remote State that provides the “VPC ID” and “Public Subnet ID.”
The second bit of code will create an EC2 resource in the public subnet. It knows the VPC already deployed and subnet information to place the EC2 resource by using a data statement to get remote state information to get the VPC ID and public subnet ID.
Now place the following code in a different folder.
EC2.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
#---------------- State terraform backend location-----
data "terraform_remote_state" "pull-backend-data" {
backend = "s3"
config = {
bucket = "unique-name-terraform-states"
key = "example-terraform.tfstate"
region = "us-west-1"
}
}
# ------------ Determine region from backend data --------------
provider "aws" {
region = data.terraform_remote_state.pull-backend-data.outputs.aws_region
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# Creating controller node
resource "aws_instance" "controller" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t2.micro"
subnet_id = data.terraform_remote_state.pull-backend-data.outputs.public_subnet_1
}
An S3 bucket must exist before launching each of the codes above.
Be sure to edit the parameters in each of the above files, and change the bucket, key, and region!
In your terminal, go to the VPC folder and execute the following commands:
Terraform init
terraform validate
Terraform apply
Once the VPC is up and running, go to the EC2 resource folder and execute the same commands as shown above. I you successfully created an S3 bucket prior to using the above code and correctly renamed the parameters appropriate for your region, you should have an EC2 resource created from modular code.
Normally when using “Terraform Apply” command, Terraform automatically places records into your local folder as a “Terraform State” file. This file contains a custom JSON format that records a mapping from the Terraform resources in your configuration files to the representation of those resources in the real world.
As long as anyone else or any other module does not need to know about the resources created by Terraform, we are happy that Terraform keeps track of what it deployed by creating a “Terraform State” file LOCALLY in the same folder.
However, what if another folder (module) needs to know about a VPC that has already been created? “Terraform Remote State” to the rescue!
Terraform Remote State
Using AWS S3 bucket
The diagram above represents sending resource data to a Terraform remote state. First, we create a shared location, like an AWS S3 bucket. Then we OUTPUT resource data into “Terraform’s remote state.” A module (another folder) then gets the resource data about what has been created from our remote state by using a “Data statement,” which will pull the resource data from Terraform’s Remote State.
Remote State Outputs
First, we need to output the data to “Terraform Remote State.” For example, using an AWS S3 bucket as file storage for Terraform remote state files.
Set S3 as our terraform remote state
Two requirements for creating and using S3 as a remote state
Beyond the obvious requirement that you must have permission to create & manage the contents of the shared location like an AWS S3 bucket, there are crucial caveats to remember.
Before trying to use an S3 bucket in Terraform, the BUCKET must exist before you can declare the bucket as a resource.
The “KEY” component of the configuration will not exist until you create a resource in that bucket.
The resource statement for Terraform Remote State will NOT accept any variables.
Once we’ve put some code into our folder about our AWS S3 bucket Terraform Remote State, we can then start outputting data about our resources into the bucket. The example below is an “OUTPUT” statement that will output our new VPC ID into Terraform Remote State.
The format is:
Output “Name” { value = module.<remote state name>.<output name> “
Another module needs information about resources already deployed (like a VPC that has already been deployed, for example). The other module get the information from Terraform remote state.
In the format:
data "terraform_remote_state" "<data state name> {
backend = "<backend remote state name>"
config = {.......}
}
Where the “Data” statement name can be any name you want to give to this function. It can be any name you want to assign; I like to give it a name like the “Test-VPC” or “Dev-Environment” to remind me what data is being obtained from Remote State.
Once we have declared terraform remote state resources and the configuration of our shared location, we can pull the data.
We use the following format to get the data:
Value = data.terraform_remote_state.<name>.outputs<output name>
Create reusable modules in Terraform to deploy resource in AWS. An example of two teams using the same code to deploy separate AWS resources.
In the previous examples, I’ve shown more and more complicated deployments of AWS infrastructure and resources. The method used in previous examples works great if I am the only person to use the code. Just me, myself, and I deploying architectural wonders into the cloud. Well, that is just not reality, now is it?
The reality is that we create technical solutions by working together as a team. After all, working together is where a team has advantages to form a better solution when we collaborate and share our knowledge and expertise.
This exercise will show continuous improvement (CI) elements by creating reusable modules and continuous development (CD) by deploying the modules.
The code discussed in this example is posted in my GitHub repository
Caveat: The VPC reusable module is simple, creating private and public subnets with only two availability zones in any region. It doesn’t accommodate choosing more than two availability zones, it doesn’t accommodate choices like if you want to disable or enable IPV6 or set up a VPN gateway, etc.
Caveat: Docker Website is also very simple. It is a simple Docker container that is a website developed for this demonstration. You can technically use your docker container or any generic website container.
Assumption: To keep costs down, we are using the latest NAT instance published by AWS. NAT Gateways in a free tier account will incur costs if left in the running state for more than an hour, so I opt to use NAT instances instead of NAT gateways to save a bit of change.
Assumption: You are interested in the methods to create modular code. This blog post will discussin detail not just the how and the why of modular coding, but also I’m trying to express the logic behind some of the requirements as I understand them.
Reusable modules
Simply put, a “module” is a folder. We put some Terraform code into that folder, and Terraform understands the content of a folder as a “MODULE.” The folders (Terraform modules) separate our Code into logical groups.
Think of modules as putting the pieces together to make a complete solution.
In the chart above, we have an example of five different folders. Each folder represents a module, and each module contributes to a complete deployment solution.
We can have a developer publish the VPC module and Security Groups. Have another person develop and publish the Web Template, and yet another developer create the Auto Scaling Group (ASG) and Load Balancer (ALB) modules. Then finally, a deployment team pulls the different modules from a published location and deploys the modules to production.
We’ll start by understanding the use of reusable modules (dry code). As we progress in writing Infrastructure as Code, we need to share code between teams like production, development, and Quality Assurance environments. This exercise will create a “reusable module” for a VPC and another “reusable module” to create an EC2 instance as a website server.
We will then simulate a Development team using the reusable modules to deploy resources into a development environment. After deploying resources for the development team, we will simulate a Quality Assurance (QA) team using the same modules (with different variables) to deploy a QA environment. The Development Team and the QA team will use the same modules (dry code) to deploy different resources in the same region or regions using the same AWS account but different credentials or even launching from different accounts.
The source argument in a module block tells Terraform where to find the source code for the desired child module. Terraform uses this during the module installation step terraform init to download the source code to a directory on a local disk so that it can be used by other Terraform commands.
HashiCorp Terrafrom
in this exercise, we will place our Terraform code into a shared location and, as per normal practice, refer to the shared location as the “source module.” We can create a “source module” in any folder. The folder can be your local drive or a source code management system (SCM) like GitHub, Artifactory, or Bitbucket. A “source module” can be any network folder in your local area network (LAN) or Wide Area Network (WAN), so long as you, the user, has permission to read and write to the network shared folder.
I believe the best place for reusable code is a source code management system (SCM) like GitHub, BitBucket, GitLab, or Artifactory. At the time of this writing, my personal preference is to use GitHub.
We create a reference to a source module by putting a statement in Terraform like the following (which becomes the module configuration block):
module "<name> {
source = "<path to folder>"
variable = "<values we need to pass to the module>"
}
Remember that the module’s “name” can be any name you desire when declaring the module. It does not have to be the same or similar to the source code for the source module to work.
Why are we using S3 remote Terraform State and DynamoDB
Letโs use an example of a three-tier application that is under development. The first tier is a front-end Web service for our customers. Another tier is the application layer that performs ERP1 services, and the third tier will hold the database (back-end services).
We have a developer (Developer-A) responsible for developing and releasing changes to our front-end web service. Another developer (Developer-B) is responsible for developing the ERP1 application service. Both developers have access to make changes in the development environment. Both developers can launch, create and destroy resources in the development environment.
Both developers perform most of their work offline and use the AWS Cloud developerโs environment on a limited basis because most of the development is performed offline and not in the cloud environments. Developer A is ready to test his changes and performs Terraform Init and Terraform Apply to create the environment. So the development environment is now running in AWS and operational.
On the very same day, Developer B will make a major change to the ERP application server. Developer B wants to move the ERP server to a different subnet. Developer B modifies his version of a reusable module, and then Developer B executes the change by performing Terraform Init and Terraform Apply, thus moving the ERP server to a different subnet. Suddenly Developer A, who is working in the same environment, observed major errors on the Front End servers that he had already deployed because developer B had moved the application servers; hence, Developer Bโs change impacted developer Aโs development test.
Developer B went into our reusable module after Developer A had already used the same module to launch the AWS resources. Terraform happily made the changes which caused Developer A to see unexpected failures. If we use โTerraform Remote stateโ in an AWS S3 bucket and DynamoDB to lock our remote state, Developer B would be prevented from executing changes to AWS resources after Developer A has locked the Terraform State. Developer B would then need to communicate and coordinate any necessary change with Developer A.
By putting a Lock on the S3 remote state, we can prevent team members from making a change that impacts AWS resources without coordination between members.
DynamoDBโs locking of Terraform State doesnโt prevent us from making a change to our resources, it simply prevents other team members from making unexpected changes after a resource is deployed.
OK, let’s get started and set up the folders
Let’s create our folder structure before getting started. The first folder, named “Modules,” will hold the reusable modules, and the second folder, named “Teams,” will be used by our team members. The third folder holds a few things to help us manage our Terraform state.
Reusable modules folder structure
You can place the “Modules” folder and the “Teams” folder anywhere. For example, you can put the “modules folder” and its content on a separate computer from the “Teams folder.”
For brevity, why don’t we keep it simple for now and place everything in a folder structure like the following:
Creating the AWS S3 bucket, Terraform state file, and DynamoDB table
Before using an S3 bucket and Terraform remote state file. We should create the bucket and Terraform remote state file independently and, most importantly, create the DynamoDB for locking Terraform remote state before creating any AWS resources that utilize the Terraform remote state.
We will create one AWS S3 bucket. Two Terraform state files, one for our Development team and one for our Test team. And one DynamoDB table that keeps the data regarding locks put in place for our Terraform remote state.
Creating the S3 bucket
Change directory to the folder ~/reusable_modules_exercise/mgmt/S3_bucket/create_s3_bucket
Reminder! Be sure to change the name of the bucket into a “unique name” of your choice
After creating this file, perform terraform init, terraform validate, and terraform apply to create the S3 bucket.
A few things to consume about our “S3_bucket.tf”. The line lifecycle {prevent_destroy = true} prevents someone from accidentally deleting an S3 bucket.
ย resource "aws_s3_bucket_server_side_encryption_configuration" This block of code enables server-side encryption. You certainly want to read up on your choices regarding encryption choices to use either “Amazon S3-managed keys (SSE-S3)” or “AWS key management service key (SSE-KMS).” I recommend reading the Terraform registry and Amazon Docs. As you can see, I’m letting AWS create and manage the key for our bucket by configuring the block with the choice of “sse_algorithm.” Amazon S3-Managed Keys (SSE-S3).
resource "aws_s3_bucket_versioning" "bucket_versioning" This code block establishes if you want to use versioning in the S3 bucket. Versioning allows reverting to a previous version of Terraform state from a disaster recovery standpoint, it makes sense to use versioning. When teams use reusable modules without a DyanamoDB lock, you most definitely want to version your code with a source code management system like GitHub. Nothing wrong with enabling it by default. You might never need to revert to a previous version of Terraform remote state UNTIL you need it, and boy, you’ll wish you had versioning in place when that happens. Especially in a production deployment, maybe not so much in a development environment.
resource "aws_s3_bucket_public_access_block" You might see some examples applying this resource setting via Terraform. Personally, I recommend skipping this optional block of code for an S3 bucket. By default, Public Access is denied for all S3 buckets unless you specifically allow public access (For example – turning an S3 bucket into a static website). I recommend leaving it out, AWS by default denies public access, which is perfect for a Terraform Remote State S3 bucket.
Creating the Remote state files
Remote state for the development team
Change directory to the folder ~terraform/reusable_modules_exercise/mgmt/s3_bucket/Dev_remote_state
dev_remote_state.tf
provider "aws" {
region = "us-west-1"
}
# ------------ configure remote state -------------------------
terraform {
backend "s3" {
bucket = "<unique-name>-terraform-states"
key = "development-terraform.tfstate"
region = "us-west-1"
}
}
After creating this file, perform terraform init, terraform validate, and terraform apply to create the remote state file for our development team.
Don’t worry if Terraform says that nothing happened. If this is the first time executing this code, it does, in fact, create the “tfstate” file.
The code should be in its own folder, separate from creating an S3 bucket. Because the bucket must also already exist to place the “tfstate” file in the bucket.
Remote state for the Test team
Change directory to the folder ~terraform /reusable_modules_exercise/mgmt/s3_bucket/QA_remote_state
test_remote_state.tf
provider "aws" {
region = "us-west-1"
}
# ------------ configure remote state -------------------------
terraform {
backend "s3" {
bucket = "<unique-name>-terraform-states"
key = "qa-terraform.tfstate"
region = "us-west-1"
}
}
After creating this file, perform terraform init, terraform validate, and terraform apply to create the remote state file for our QA team.
Creating the DyanmoDB database
Change directory to the folder ~terraform/reusable_modules_exercise/mgmt/s3_bucket/Create_DynamoDB_Table
Create_DynamoDB_table.tf
provider "aws" {
region = "us-west-1"
}
# ---------- Create DynamoDB for Locking S3 state -------------
resource "aws_dynamodb_table" "test" {
name = "test_db_locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
The secret to creating this DynamoDB table is in the “hash_key.” When Terraform is pointed to a DynamoDB table, it will place the Terraform remote state into DynamoDB’s NoSQL database using the HASH_KEY as the primary ID for each Terraform Remote State. Yes, that’s right, we need only ONE DyanamoDB table that can handle multiple Terraform states. We will be using the DynamoDB database twice in this exercise. Once the Development team with a unique “tfstate” file is placed into DyanamoDB, our QA team will have its own unique “tfstate” file in DynamoDB. Terraform will simply create a new unique “LockID” for each Terraform state file.
Once again, I recommend separating the code into its own folder from the above code. Primarily because we need only one Database for all our teams using the same DynamoDB. Development, Test, QA, and Production deployments can use the same DynamoDB database because each will have its own “tfstate” file and a unique LockID in the database.
Code for Reusable Modules
Reusable modules are “child modules” because when we execute terraform init , the reusable modules are downloaded into the calling directory (which becomes the “parent” module). There is a relationship between the parent module and the child module. The parent module uses the following:
When we initiate terraform init, Terraform knows where to get the child module because of the “Source” module configuration block (shown above). Terraform will download the reusable module from the source into the current directory and configure the downloaded module with values stipulated by the “variable =” as shown above.
The Terraform workflow is different from previous exercises. Here are a few pointers;
Remember the module that is doing all the work is the reusable module which is the “source (child) module” which is downloaded into the current directory.
The “Parent” module calls the “child module” (reusable module) and passes variables to the child module.
We are using Terraform Remote State, BUT there is a really big caveat as to how we use Terraform Remote State in this scenario;
In the previous exercises, we used “inputs” and “outputs” to Terraform Remote State. In this case, while we are still using outputs, but in this case, we are using Terraform State to lock our configuration and not so much to pass inputs and outputs to/from our remote state file.
Code that creates our reusable modules
So now that we have created our S3 bucket, the Terraform state file, and a DynamoDB table, we are ready to set up some code as a reusable module.
Change directory to ~/reusable_modules_exercise/modules
Now let’s create our first reusable module, the VPC. We will start with a terraform_remote_state S3 bucket configuration. It is important to use variables for the bucket name and even more important to use a variable for the key name. Why you might ask? Well, that’s a great question; let me explain. โบ๏ธ
It is recommended that each team use a unique terraform state file. Terraform writes the state data to a remote data store, which can then be shared between all members of a team. We want to separate our team environments because they usually have different requirements and process. Also each team usually requires its own control of release and configuration management. Therefore each team will use a unique terraform state file.
Me
We are going to use a lot of variables. Since we are using the same reusable code for different teams, we will need a method to cause a change of configuration for AWS resources per each team’s requirements. Hence, we use variables for each team to have the ability to apply a variance to an AWS resource.
Examples of variance
Size – A development team might use a “t2.micro” size for an AWS EC2 resource, but our Production team needs to assign a larger type “t3.large” instance type.
Stage – We need to differentiate between development, QA, and production, so we’ll use a variable called “Stage.” Creating a tag called “Stage” and assigning an appropriate variable identifying the team that owns the new resource. We will take advantage of this in other modules by using a filter to identify resources managed by which team.
Region – Our teams might be in different regions, so we’ll enable deployments into different regions using the same code but setting a “Region” variable.
Variables are key! Defining what needs to be configured for the different teams is a very important element when planning the use of reusable code.
Using Variables in reusable modules
Let’s start with an understanding of the usage of variables.
Reusable modules may have variables declared and used only in the module.
Reusable modules will have variables declared in the parent module and passed to the reusable module. This is exactly how we create a variance in deploying a reusable module.
For example, a development team uses region (us-west-1), and the QA team uses region (us-east-1). We will create our variable in the reusable module, the parent module, and the parent module’s configuration block to accomplish the variance.
reusable module declares – variable “region” {}
parent module also declares – variable “region” {}
parent module assigns a value to the variable in the module’s configuration block. See below:
module "foo" {
source = "../foobar"
region = "us-west-1"
}
There is one more variable discussion. When we want to prevent sensitive information from being published on GitHub, we will move an assignment of a value into a private file like “terraform.tfvars”.
In the module configuration block below, we normally assign values to variables, in this case, “bucket” with a value “my-bucket-terraform-states.” However, I don’t want the general public to know the name of my S3 bucket. Instead, I assign a variable in the configuration block and input the value in a file named”terraform.tfvars” instead of the configuration block. We also set up a special file called “gitignore” to instruct GIT to ignore the file “terraform.tfvars” when pushing to GitHub. Hence, the bucket name will not be published on GitHub and thus becomes a privately assigned value.
For example, in the line of code (instance_type = var.instance_type) in the example above, we use a variable where we would normally assign a value.
With any module, a simple thing like creating a variable for “Instance_type” needs to be declared, assigned to a resource, and given a value.
But when using reusable modules, the variables declaration, assignment to a resource, and then giving the variable a value will be placed into at least three, possibly four, different files.
The first rule is to declare the variable in both the parent and child modules. We assign a value to the variable in a configuration block in the parent module.
Type
Module
File
Declare variable
Parent Module
teams/…/variables.tf
Declare variable
Reusable module
modules/vpc/variables.tf
assign variable to a resource
Reusable module
modules/vpc/vpc.tf
Assign a value to the variable
Parent Module (Module configuration block)
teams/…/vpc.tf
To summarize:
The parent module and the child module must both declare a variable that is going to be configured in the parent module and assigned to a resource in the child (reusable) module: variable "instance_type" {}
The child (reusable) module will assign a variable to a resource: instance_type = var.instance_type
Normally, the parent module then assigns a value to the variable in the parent module configuration block:
But when it’s sensitive information, we skip the above step and assign the value in Terraform’s environment file, “terraform.tfvars”.
Let’s pretend that “instance_type” is sensitive information, and we do not want the value of instance_type published to GitHub. So instead of assigning a value in the module’s configuration block, as shown above, we will pass the buck to “Terraform.tfvars.” We instead assign a variable once again in the configuration block and assign a value in “terraform.tfvars, as shown in the example below:
Then assign the value in the Terraform.tfvars file: instance_type = "t2.micro"
So let’s start with the first reusable file
The first reusable module – will be an AWS Virtual Private Cloud (VPC) reusable module.
First, we must decide what is configurable when creating the VPC. Different teams will want some control over the VPC configuration. So what would they want to configure (variance):
We want the S3 remote state bucket, State key, bucket region, and DynamoDB assignment to be configurable, as we want each team to manage their own VPC and the VPC Remote State
We need a tag to identify which team the VPC belongs to and a tag as to who takes ownership of the VPC
We want the region to be configurable by our teams
We want the NAT instance to have configurable sizing as per Team requirements
We might want the SSH inbound CIDR block to change as our teams might be in different regions and networks. Therefore, we need the SSH inbound CIDR block (I call it SSH_location) to be configurable by our teams
We probably want a different EC2 Key pair per team, especially if they are in different regions. I’d go so far as to say that production should be managed from a different account, using different EC2 key pairs and unique IAM policies. So we need the EC2 key pair configurable with reusable code.
As per the above conversation, we must declare the following variables in the parent and child modules that allow different teams to apply their configuration (variance) to the reusable modules.
We will then assign a value to each variable in the parent module.
Remember: All folders are considered Modules in Terraform
So first, we create a “variables.tf’ file in ALL reusable (child) modules: ~/terraform/reusable_modules/modules/vpc/variables.tf ~/terraform/reusable_modules/modules/Docker_Website/variables.tf and we’ll create the same variables file in ALL parent modules, we’ll create a variables file for the development team: ~/terraform/reusable_modules/team/development/variables.tf and we’ll create the same file for the QA team: ~/terraform/reusable_modules/team/QA/variables.tf
Variables that are declared and configured only in the reusable module
Note: in a future version, I might try my hand at doing the same as some of the more famous community VPC modules where we can create a subnet per AZ and/or stipulate how many subnets, like two subnets vs. four subnets. For now, I have hard-coded into the VPC module
Note 2: We want to use our own VPC coding simply because we want to use NAT instances vs. NAT gateways. It’s not an option in any of the community modules.
VPC (reusable module)
Change directory to ~/terraform/reusable_modules_exercise/modules/vpc, and include the following files vpc.tf, variables.tf, security_groups.tg and outputs.tf (documented below and included in my GitHub repository)
variables.tf (in the reusable module)
variable "bucket" {
description = "Name of the S3 bucket that will be holding Terraform Remote State"
type = string
}
variable "state-key" {
description = "Name of the file for the terraform state key"
type = string
}
variable "dynamodb_table" {
description = "Name to be assigned to the DynamoDB table"
type = string
}
variable "region" {
description = "Region where VPC will be located"
type = string
}
variable "bucket-region" {
description = "Region where S3 bucket is placed"
type = string
}
variable "ec2-key" {
description = "Regional EC2 key used by the team"
type = string
}
variable "instance_type" {
description = "EC2 instance type"
type = string
}
variable "ssh_location" {
description = "CIDR block allowed SSH access into resource"
type = string
}
variable "environment" {
description = "Identify the Team's Environment i.e. QA or Development"
type = string
}
variable "owner_name" {
description = "Name to be used on all the resources as deployment owner"
type = string
}
variable "enable_ipv6" {
description = "Requests an Amazon-provided IPv6 CIDR block with a /56 prefix length for the VPC. You cannot specify the range of IP addresses, or the size of the CIDR block."
type = bool
default = false
}
variable "enable_dns_hostnames" {
description = "Should be true to enable DNS hostnames in the VPC"
type = bool
default = true
}
variable "enable_dns_support" {
description = "Should be true to enable DNS support in the VPC"
type = bool
default = true
}
variable "map_public_ip_on_launch" {
description = "Whether to map the public IP on launch. "
type = bool
default = true
}
Security_Groups.tf
The following code establishes security groups for our (VPC) reusable module.
The security group for NAT instances allows HTTP and HTTPS only from the private subnets (thus allowing any instances in the private subnets to reach out to the internet for updates, patches, and download new software).
The security group for Docker Server allows HTTP and HTTPS from my Public IP address (ssh_location variable) and all traffic outbound to the internet. Allowing all traffic outbound to the internet is typical of a “Public Subnet.”
We are placing our Docker server in the public subnet, which is Ok for this exercise. So technically, we don’t need the NAT instances or the private subnets because we only place one EC2 Instance in one public subnet. Just for grins, I kept the private subnets.
output "region" {
description = "AWS region"
value = data.aws_region.current.name
}
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.my-vpc.id
}
output "public_subnet_1" {
description = "Public Subnet 1"
value = aws_subnet.public-1.id
}
output "public_subnet_2" {
description = "Public Subnet 2"
value = aws_subnet.public-2.id
}
output "private_subnet_1" {
description = "Private Subnet 1"
value = aws_subnet.private-1.id
}
output "private_subnet_2" {
description = "Private Subnet 2"
value = aws_subnet.private-2.id
}
output "NAT_sg_id" {
description = "Security group ID for nat-sg"
value = aws_security_group.nat-sg.id
}
output "web-sg_id" {
description = "Security group ID for web-sg"
value = [aws_security_group.web-sg.id]
}
Docker_website (reusable module)
Our teams will use this module to deploy an AWS EC2 instance with scripts to install Docker and launch one Docker container that I created and published publicly in Docker Hub.
Several features to understand about this reusable module.
There is a dependency that the team’s VPC is already deployed
The module first communicates with AWS API to get data about the team’s VPC
For instance, data “aws_vpcs” “vpc” gets data for all VPCs in the region
Our data query to the API includes a filter, which will filter our query to return only the VPC with an environment value whose value is set by the parent module. For instance, if the parent module sets var.enviroment = development , then our query to the API will return only the ID of the VPC created by our development team.
You will notice that we have similar queries to find the team’s public subnet and the team’s security group for a web server.
Change directory to ~/terraform/reusable_modules_exercise/modules/Docker_website and create the following files: docker.tf, variables.tf, bootstrap_docker_web.sh, outputs.tf
docker.tf
#------------------------- State terraform backend location-----
data "terraform_remote_state" "Terraform-State" {
backend = "s3"
config = {
bucket = var.bucket
key = var.state-key
region = var.bucket-region
dynamodb_table = var.dynamodb_table
}
}
# ----------------------- Get existing VPC ---------------------
data "aws_vpcs" "vpc" {
tags = {
Stage = var.environment
Name = "My-VPC"
}
}
# ----------------------- Get region data ----------------------
data "aws_region" "current" {}
# ----------------------- Get existing Public Subnet -----------
data "aws_subnet" "public_subnets" {
tags = {
Stage = var.environment
Name = "public_subnet-1"
}
}
# ---- Get existing Security Group for Web server --------------
data "aws_security_group" "web-sg" {
tags = {
Stage = var.environment
Name = "Web-SG"
}
}
#--------- Get most recent Amazon Linux2 image -----------------
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# -------------------- Creating Web Server ---------------------
resource "aws_instance" "web-server" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = data.aws_subnet.public_subnets.id
vpc_security_group_ids = [data.aws_security_group.web-sg.id]
#subnet_id = data.terraform_remote_state.dev_vpc.outputs.public_subnet_1
#vpc_security_group_ids = [data.terraform_remote_state.dev_vpc.outputs.web-sg_id]
user_data = file("${path.module}/bootstrap_docker_web.sh")
#user_data = file("bootstrap_docker_web.sh")
monitoring = true
key_name = var.ec2-key
tags = {
Name = "Web-Server"
Stage = "${var.environment}"
Owner = "${var.owner_name}"
}
}
output "Web_IP" {
value = [aws_instance.web-server.public_ip]
}
Creating code for the parent modules
Now comes the fun part. This code might appear similar to some community modules developed and published by different companies. Many community modules are complex in trying to solve all possible permutations someone might require of their module. For instance, many community VPC modules try to accommodate someone who may or may not require a VPN or a DirectLink connection to their VPC. Most published community modules allow a VPC to choose how many availability zones to deploy a subnet.
The VPC module in this example, the child module, and the parent module have simple requirements because my goal is to demonstrate how to create a module and only just a simple demonstration. Simplicity is the easiest method to reach a broader audience, right?
I already have a more complex demonstration planned for my next blog post, which will be a method for different teams to deploy an auto-scaled and load-balanced WordPress website using EFS for persistent storage that can use the reusable modules for the development team or a QA team etc. Soon to be published.
So first, let’s look at the variables configuring the reusable module AWS resources specifically for each team’s requirement.
The development team requires its own S3 bucket and remote state file, so it will declare the necessary variables and assign values unique to the development team
The same applies to an EC2-key pair, EC2 instance type, in-bound SSH CIDR block (SSH-Location), etc.
Some of the variables will be assigned a value in the parent modules configuration blog
Some sensitive information variables will assign a value in our “terraform.tfvars” file.
Let’s start with the Development team
Change directory to ~/terraform/reusable_modules_exercise/teams/development and add the following files.
variables.tf (development team)
The variables for our Development team
variable "bucket" {
description = "Name of the S3 bucket that will be holding Terraform Remote State"
type = string
}
variable "state-key" {
description = "Name of the file for the terraform state key"
type = string
}
variable "dynamodb_table" {
description = "Name to be assigned to the DynamoDB table"
type = string
}
variable "region" {
description = "Region where VPC will be located"
type = string
}
variable "bucket-region" {
description = "Region where S3 bucket is placed"
type = string
}
variable "ec2-key" {
description = "Regional EC2 key used by the team"
type = string
}
variable "instance_type" {
description = "EC2 instance type"
type = string
}
variable "ssh_location" {
description = "CIDR block allowed SSH access into resource"
type = string
}
variable "environment" {
description = "Identify the Team's Environment i.e. QA or Development"
type = string
}
variable "owner_name" {
description = "Name to be used on all the resources as deployment owner"
type = string
}
variable "enable_ipv6" {
description = "Requests an Amazon-provided IPv6 CIDR block with a /56 prefix length for the VPC. You cannot specify the range of IP addresses, or the size of the CIDR block."
type = bool
default = false
}
variable "enable_dns_hostnames" {
description = "Should be true to enable DNS hostnames in the VPC"
type = bool
default = true
}
variable "enable_dns_support" {
description = "Should be true to enable DNS support in the VPC"
type = bool
default = true
}
variable "map_public_ip_on_launch" {
description = "Whether to map the public IP on launch. "
type = bool
default = true
}
terraform.tfvars
With sensitive values, our Development team’s values will be declared in the file “terraform.tfvars. Teams can utilize the same S3 bucket for Terraform Remote State; it is the “state-key” that must be unique for each team.”
region = "us-west-1"
environment = "development"
instance_type = "t2.micro"
ec2-key = "<EC2 Key pair for development team>"
ssh_location = "<Your public IP address>"
owner_name = "<Your name, your teams name or email>"
bucket = "<The name of your S3 bucket"
state-key = "development-terraform.tfstate"
dynamodb_table = "test_db_locks"
bucket-region = "<the region for the S3 bucket>"
enable_dns_hostnames = true
enable_dns_support = true
enable_ipv6 = false
main.tf (parent module for development)
We are going to declare the VPC module and the Docker_website module. In this file (parent module), we will declare the source (path) of the child modules and the configuration to be applied to the child modules (by giving values to variables).
Note: module configuration block named “module “Docker_web” below has the line depends_on = [module.dev_vpc]. When putting together different modules like first the VPC, followed by creating our docker website, Terraform does not easily determine the dependencies. Without the “depends_on,” Terraform will try to deploy both modules simultaneously, and without the VPC already in place, our docker website will fail. This is easily fixed by the “depends_on” statement, which tells Terraform the VPC module must be completed before executing the “Docker_web” module.
Yes, we have already declared outputs in the reusable module. But with reusable modules, if you want to see the outputs, we have to declare the outputs in our Parent Module as well. Just like variables, outputs have to be declared both in the child and parent modules.
outputs.tf
output "region" {
description = "AWS region"
value = module.dev_vpc.region
}
output "vpc_id" {
description = "VPC ID"
value = module.dev_vpc.vpc_id
}
output "public_subnet_1" {
description = "Public Subnet 1"
value = module.dev_vpc.public_subnet_1
}
output "public_subnet_2" {
description = "Public Subnet 2"
value = module.dev_vpc.public_subnet_2
}
output "private_subnet_1" {
description = "Private Subnet 1"
value = module.dev_vpc.private_subnet_1
}
output "private_subnet_2" {
description = "Private Subnet 2"
value = module.dev_vpc.private_subnet_2
}
output "NAT_sg_id" {
description = "Security group ID for mat-sg"
value = module.dev_vpc.NAT_sg_id
}
output "Web-IP" {
description = "Security group ID for RDS-sg"
value = module.Docker_web.Web_IP
}
Create Quality Assurance Parent Module
Change directory to ~/terraform/reusable_modules_exercise/teams/quality_assurance and add the following files: main.tf, variables.tf, terraform.tfvars, output.tf.
Variables for the quality assurance team
You might notice the “variables.tf” file for the QA team is exactly the same as the development team’s “variables.tf”. That is because both teams are calling the same reusable modules. The magic happens when we assign a value to the variables
variables.tf
variable "bucket" {
description = "Name of the S3 bucket that will be holding Terraform Remote State"
type = string
}
variable "state-key" {
description = "Name of the file for the terraform state key"
type = string
}
variable "dynamodb_table" {
description = "Name to be assigned to the DynamoDB table"
type = string
}
variable "region" {
description = "Region where VPC will be located"
type = string
}
variable "bucket-region" {
description = "Region where S3 bucket is placed"
type = string
}
variable "ec2-key" {
description = "Regional EC2 key used by the team"
type = string
}
variable "instance_type" {
description = "EC2 instance type"
type = string
}
variable "ssh_location" {
description = "CIDR block allowed SSH access into resource"
type = string
}
variable "environment" {
description = "Identify the Team's Environment i.e. QA or Development"
type = string
}
variable "owner_name" {
description = "Name to be used on all the resources as deployment owner"
type = string
}
variable "enable_ipv6" {
description = "Requests an Amazon-provided IPv6 CIDR block with a /56 prefix length for the VPC. You cannot specify the range of IP addresses, or the size of the CIDR block."
type = bool
default = false
}
variable "enable_dns_hostnames" {
description = "Should be true to enable DNS hostnames in the VPC"
type = bool
default = true
}
variable "enable_dns_support" {
description = "Should be true to enable DNS support in the VPC"
type = bool
default = true
}
variable "map_public_ip_on_launch" {
description = "Whether to map the public IP on launch. "
type = bool
default = true
}
terraform.tfvars
Again, this is where our QA team will create variances required by their team. You’ll not that I give an example of our QA team using the “US-West-2” region instead of “Us-West-1” like the development team uses for their region. Also, note I have stipulated an instance type of “t2.micro” to demonstrate another variance between teams.
region = "us-west-2"
environment = "QA"
instance_type = "t3.micro"
ec2-key = "<EC2 Key pair for development team>"
ssh_location = "<Your public IP address>"
owner_name = "<Your name, your teams name or email>"
bucket = "<The name of your S3 bucket"
state-key = "development-terraform.tfstate"
dynamodb_table = "test_db_locks"
bucket-region = "<the region for the S3 bucket>"
enable_dns_hostnames = true
enable_dns_support = true
enable_ipv6 = false
output "region" {
description = "AWS region"
value = module.QA_vpc.region
}
output "vpc_id" {
description = "VPC ID"
value = module.QA_vpc.vpc_id
}
output "public_subnet_1" {
description = "Public Subnet 1"
value = module.QA_vpc.public_subnet_1
}
output "public_subnet_2" {
description = "Public Subnet 2"
value = module.QA_vpc.public_subnet_2
}
output "private_subnet_1" {
description = "Private Subnet 1"
value = module.QA_vpc.private_subnet_1
}
output "private_subnet_2" {
description = "Private Subnet 2"
value = module.QA_vpc.private_subnet_2
}
output "Web-IP" {
description = "Security group ID for RDS-sg"
value = module.Docker_web.Web_IP
}
Deployment
Be sure to update the “terraform.tfvars” file to your settings. The GitHub repository does not have these files, so you will have to create a file for the development team and another for the QA team.
Please change the directory to ~/terraform/reusable_modules_exercise/teams/development
Perform the following terraform actions:
terraform init
terraform validate
terraform apply
Once completed, Terraform will have deployed our reusable code into AWS inside of the region specified by the settings configured in the parent module
Then change the directory to ~/terraform/reusable_modules_exercise/teams/quality_assurance
And perform the following actions:
terraform init
terraform validate
terraform apply
Once completed, Terraform will have deployed reusable cod for Quality Assurance. If you configured the Quality Assurance configuration with a different region, the same type of AWS resources is installed in a different region using the same reusable code.
Once completed with this exercise, feel free to remove all resources by issuing the following command in the terminal:
Change the directory to each team’s directory and perform the following destroy task. We don’t want to leave our EC2 instances running and forget about them.
AWS allows 750 hours of free tier EC2 hours. If you leave this exercise running, it has six EC2 instances (three for each team); left running it will use up your allowance of free EC2 hours in 5 days.
terraform destroy
This is not for production!
All public websites should have an application firewall between the Web Server and its internet connection, this exercise doesnโt create a firewall. So do not use this configuration for production
Most cloud deployments should have monitoring in place to detect and alert someone should an event occur to any resources that require remediation. this exercise does not include any monitoring
It is a good idea to remove All resources when you have completed this exercise so as not to incur costs
1Enterprise resource planning (ERP) refers to a type of software that organizations use to manage day-to-day business activities such as accounting, procurement, project management, risk management and compliance, and supply chain operations.
AWS Certificate Manager is a service that lets you easily provision, manage, and deploy public and private Secure Sockets Layer/Transport Layer Security (SSL/TLS) certificates for use with AWS services and your internal connected resources. SSL/TLS certificates are used to secure network communications and establish the identity of websites over the Internet as well as resources on private networks. AWS Certificate Manager removes the time-consuming manual process of purchasing, uploading, and renewing SSL/TLS certificates.
Public SSL/TLS certificates provisioned through AWS Certificate Manager are free.
Overview
This exercise will build an auto-scaling group (ASG) of web servers. It is using almost the exact same code as my previous exercise.
The critical difference in this exercise is that we will add Terraform instructions to change our domain settings in AWS Route 53 and create a valid AWS SSL certificate using AWS Certificate Manager to enable SSL traffic to our website (HTTPS).
Prerequisites
You must have or purchase a domain for this exercise
It can be a domain purchased from any domain service, or you can buy a domain with AWS route 53
You must also ensure Route 53 is configured as your domain’s “Name Service” for the domain.
Terraform Installed
AWS account and AWS CLI installed and configured
The Code
Please clone or fork the code from my previous exercise from the GitHub repository.
Make a directory called Terraform, and be sure to change the directory to Terraform. On a Mac (cd ~/terraform). Then clone or fork my repository into the Terraform directory. You should now have a directory “ALB_ASG_Website_using_NAT_instances,” so let’s change directories into that directory.
Now we are going to add a file called “Route53.tf” using our favorite editor (in my case, “Visual Studio Code.”
provider "aws" {
alias = "account_route53" # Specific to your setup
}
# This creates an SSL certificate
resource "aws_acm_certificate" "cert" {
domain_name = "<your domain>"
validation_method = "DNS"
}
# Cert Validation
resource "aws_route53_record" "cert_validation" {
name = tolist(aws_acm_certificate.cert.domain_validation_options)[0].resource_record_name
type = "CNAME"
zone_id = "aws_route53_record" "MyDomain"
records = [tolist(aws_acm_certificate.cert.domain_validation_options)[0].resource_record_value]
ttl = 60
}
# This is a DNS record for the ACM certificate validation to prove we own the domain
resource "aws_acm_certificate_validation" "cert_validation" {
certificate_arn = "${aws_acm_certificate.cert.arn}"
validation_record_fqdns = [tolist(aws_acm_certificate.cert.domain_validation_options)[0].resource_record_name]
}
# Standard route53 DNS record for Domain pointing to an ALB
resource "aws_route53_record" "MyDomain" {
zone_id = "<zone id of your domain>"
name = "<your domain>"
type = "A"
alias {
name = aws_lb.website-alb.dns_name
zone_id = aws_lb.website-alb.zone_id
evaluate_target_health = true
}
}
Be sure to change <your domain> into the exact domain registered in Route 53, for example, “example.com.” If you want to use something like “www.example.com,” it must already be registered exactly as “www.example.com” in Route 53. Also, be sure to get the “Zone ID” of your domain from Route 53 and replace <zone id of your domain> within the above “route53.tf” code.
That is it; the above code will automatically create a certificate in AWS Certificate Manager, the code will automatically add the neccesary DNS entry for the certificate, and will automatically validate the certificate.
Well Ok, one more change to be made
While researching how to use Terraform to automate adding an SSL certificate for our Load Balancer, every example missed a critical component to get this working. I lost a few hours troubleshooting, then banged my head on the desk because of the apparent failure to change the ALB listener to accept HTTPS. I suppose the writers assumed that everyone knows an ALB listener has to change if we use HTTPS traffic instead of HTTP traffic. However, that tidbit of information wasn’t included in any articles I found on the internet. Oh well, onward and upwards!
Change the “alb_listener.tf file
Delete “alb_listener.tf” and we’ll add a new “alb_listener.tf”.
Our new listener instructions will forward HTTPS traffic to our load balancer. The code will also automatically redirect any HTTP traffic to HTTPS, thus forcing all traffic to be protected by an SSL transport.
The resources are free only if you don’t leave them running! There is a limit of EC2 hours allowed per month!
This is not for production!
All public websites should have an application firewall between the Web Server and its internet connection, this exercise doesnโt create the application firewall. So do not use this configuration for production
All websites should have monitoring and a method to scrape log events to detect and alert for potential problems with the deployment.
This exercise uses resources compatible with the AWS Free Tier plan. It does not have sufficient compute sizing to support a production workload.
It is a good idea to remove All resources when you have completed this exercise so as not to incur costs
Using Terraform to deploy an auto-scaled WordPress site in AWS, with an application load balancer, while using EFS as storage for WordPress front end servers
Load balanced and Auto-Scaled WordPress deployment
This exercise will build an auto-scaled WordPress solution. While using EFS as the persistent storage solution. An auto-scaled front end can expand the number of front-end servers to handle growth in the number of users during peak hours. We also need a load-balancer that automatically distributes users amongst front-end servers to accommodate load distribution.
Ideally, we should use a scaling solution based on demand. I could write scaling an ASG based on demand, but demonstrating compliance by increasing client demand (representing peak load), could incur a substantial cost, and I’m trying to keep my exercises to be “compliant with a Free Tier plan.” Soooo, simply using an AWS ASG with desired capacity will be the solution for today.
Ideally, we should also use RDS for our database, which can scale based on demand. Using one MariaDB server that does not scale to user load kind of defeats the purpose of a scalable architecture. However, I’ve written this exercise to demonstrate deploying scaling WordPress front-end servers with an EFS shared file service and not so much as an ideal production architecture. Soooo, one MariaDB that is free tier compliant is our plan for today.
Why are we using EFS?
When scaling more than one WordPress front-end server, we’ll need a method to keep track of users amongst the front-end servers. We need storage common to all front-end servers to ensure each auto-scaled WordPress server is aware of user settings, activity, and configuration. AWS provides a shared file storage system called Elastic File Services (EFS). EFS is a serverless file storage system. EFS is compliant with NFS versions 4.0 and 4.1. Therefore, the latest versions of Amazon Linux, Red Hat, CentOS, and MAC operating systems are capable of using EFS as an NFS server. Amazon EC2 and other AWS compute instances running in multiple Availability Zones within the same AWS Region can access the file system so that many users can access and share a common data source.
Each front-end server using EFS has access to shared storage, allowing each server to have all user settings, configuration, and activity information.
Docker
We will be using Docker containers for our WordPress and MariaDB servers. The previous WordPress exercise used Ansible to configure servers with WordPress and MariaDB. But we are using auto-scaling, so I would like a method to deploy WordPress quickly rather than scripts or playbooks in this exerciseโDocker to the rescue.
This exercise will be using official Docker images “WordPress” and “MariaDB.”
Terraform
We will be using Terraform to construct our AWS resources. Our Terraform code will build a new VPC, two public subnets, two private subnets, and the associative routing and security groups. Terraform will also construct our ALB, ASG, EC2, and EFS resources.
variable "region" {
description = "The region Terraform deploys your instances"
type = string
}
variable "ssh_location" {
type = string
description = "My Public IP Address"
}
variable "vpc_cidr_block" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_count" {
description = "Number of public subnets."
type = number
}
variable "private_subnet_count" {
description = "Number of private subnets."
type = number
}
variable "intra_subnet_count" {
description = "Number of private subnets"
type = number
}
variable "public_subnet_cidr_blocks" {
description = "Available cidr blocks for public subnets"
type = list(string)
default = [
"10.0.1.0/24",
"10.0.2.0/24",
"10.0.3.0/24",
"10.0.4.0/24",
"10.0.5.0/24",
"10.0.6.0/24",
"10.0.7.0/24",
"10.0.8.0/24",
]
}
variable "private_subnet_cidr_blocks" {
description = "Available cidr blocks for private subnets"
type = list(string)
default = [
"10.0.101.0/24",
"10.0.102.0/24",
"10.0.103.0/24",
"10.0.104.0/24",
"10.0.105.0/24",
"10.0.106.0/24",
"10.0.107.0/24",
"10.0.108.0/24",
]
}
variable "intra_subnet_cidr_blocks" {
description = "Available cidr blocks for database subnets"
type = list(string)
default = [
"10.0.201.0/24",
"10.0.202.0/24",
"10.0.203.0/24",
"10.0.204.0/24",
"10.0.205.0/24",
"10.0.206.0/24",
"10.0.207.0/24",
"10.0.208.0/24"
]
}
Security
The load balancer security group will only allow HTTP inbound traffic from my public IP address (in this exercise) at the time of this writing. I will possibly alter this exercise to include the configuration of a domain using Route 53 and a certificate for that domain, such that we can use HTTPS encrypted traffic instead of HTTP traffic. Using a certificate incurs costs because a Route 53 certificate for a domain is not included in a free tier plan. Therefore, I might write managing Route 53 using Terraform as an optional configuration later.
The WordPress Security group will only allow HTTP inbound traffic from the ALB security group and SSH only from the Controller security group.
The MySQL group will only allow MySQL protocol from the WordPress security group and SSH protocol from the Controller security group.
The optional Controller will only allow SSH inbound from My Public IP address.
We are writing the Terraform code to create a general-purpose EFS deployment. You’ll note that I’m using a variable called “nickname” to create a unique EFS name. We are using “general purpose” performance and “bursting” throughput mode to stay within free tier plans and not incur costs. You’ll notice that we are creating a mount point in each private subnet so that our EC2 instances can make NFS mounts to an AWS EFS service.
The method of creating an auto-scaled WordPress deployment uses the same kind of Terraform code found in my previous exercise. If you would like to see more discussions about key attributes, and decisions to make about Terraform coding of an Auto Scaling Group please refer to my previous article.
Notice that I added a dependency on MariaDB in the code. It is not required, it will work with or without this dependency, but I like the idea of telling Terraform that I want our database to be active before creating WordPress.
Notice that we assign variables for EFS ID, dbhost, database name, the admin password, and the root password in the launch template.
This covers the variables needed for WordPress and MariaDB servers.
variable "instance_type" {
description = "Type of EC2 instance to use"
type = string
default = "t2.micro"
}
variable "environment" {
description = "User selects environment"
type = string
}
variable "your_name" {
description = "Your Name?"
type = string
}
variable "key" {
description = "EC2 Key Pair Name"
type = string
}
variable "user" {
description = "SQL User for WordPress"
type = string
}
variable "dbname" {
description = "Database name for WordPress"
type = string
}
variable "password" {
description = "User password for WordPress"
type = string
}
variable "root_password" {
description = "User password for WordPress"
type = string
}
variable "domain_name" {
description = "My Domain Name"
type = string
}
bootstrap_wordpress.tpl
This Terraform code will be used to configure each WordPress server with Docker and launch the WordPress Docker container with associative variables to configure EFS ID, dbhost, database name, and admin password, and root password.
Copy the lb_dns_name, without the quotes, and paste the DNS name into any browser. If you have followed along and placed all of the code correctly, you should see something like the following:
Screen Shot
Notice Sometimes servers in an ASG take a few minutes to configure. Wait a couple of minutes if you get an error from our website and try again.
Open up your AWS Management Console, and go to the EC2 dashboard. Be sure to configure your EC2 dashboard to show tag columns with a tag value “Name”. A great way to identify your resources is using TAGS!!
If you have configured the dashboard to display the tag column "Names" in your EC2 dashboard, you should quickly be able to see one instance with the tag name "Test-MariaDB" and "Test-NAT2" and TWO servers with the Tag Name "Wordpress_ASG".
As an experiment, perhaps you would like to expand the number of Web servers. We can manually expand the number of desired capacity, and the Auto Scaling Group will automatically scale up or down the number of servers based on your command to change desired capacity.
Where ASG_Name in the command line above will be the terminals output of lb_dns_name (without the quotes of course). If you successfully executed the command line in your terminal, you should eventually see in the EC2 dashboard FOUR instances with the tag name “WordPress_ASG”. It does take a few minutes to execute the change. Demonstrating our ability to manually change the number of servers to four instead of two.
Now, go to your EC2 dashboard. Select one of the “WordPress_ASG” instances and select the drop-down box “Instance state”, then select “Stop Instance”. Your Instance will stop and what should happen, is the Auto Scaling Group and Load Balancer health checks will see that one of the instances is no longer working. The Auto Scaling Group will automatically take it out of service and create a new instance.
Now go to the Auto Scaling Groups panel (find this in the EC2 dashboard, left-hand pane under “Auto Scaling”. Click on the tab “Activity”. You should in a few minutes see an activity announcing:
“an instance was taken out of service in response to an EC2 health check indicating it has been terminated or stopped.”
The next activity will be to start a new instance. How about that! Working just like we designed the ASG to do for us. The ASG is automatically keeping our desired state of servers in a healthy state by creating new instances if one becomes unhealthy.
Once completed with this exercise, feel free to remove all resources by issuing the following command in the terminal:
terraform destroy
This is not for production!
All public websites should have security protection with a firewall (not just a security group). Since this is just an exercise, you can you in AWS free tier account, I do recommend the use of this configuration for production.
Most cloud deployments should have monitoring in place to detect and alert someone should an event occur to any resources that require remediation. this exercise does not include any monitoring
It is a good idea to remove All resources when you have completed this exercise so as not to incur costs
Deploy an Auto Scale Group and Application Load Balancer in AWS
AWS no longer provides a NAT AMI. This exercise is based on utilizing AWS NAT AMIs. Therefore, at this time, this exercise will not work.
Application Load Balancer
This exercise will demonstrate using Terraform to deploy an AWS Auto Scaling Group and an application load balancer.
A simple website that shows EC2 Instance
I have created a bit of code that is a simple HTML page that will display some information about the AWS EC2 instance that is the host server of the web page. When you connect to our load balancer, the load balancer will route the end user to one of the EC2 Instances within the auto-scaling group. The web page display will show you the EC2 server details. When closing the web page and reconnecting to the load balancer, you will most likely see different host details, proving the load balancer connects to different servers.
The web page will look something like the following:
ALB – Application Load Balancer
A bit of information on AWS load balancers first:
Classic Load Balancer
Layer 4/7 (HTTP/TCP/SSL traffic)
Network Load Balancer
Layer 4 (TLS/TCP/UDP traffic)
Application Load Balancer
Layer 7 (HTTP/HTTPS traffic)
Classic Load Balancer (CLB) – AWS recommends that you do not use their classic load balancer. The classic load balancer will eventually be deprecated.
Network Load Balancer (NLB) – The network load balancer works at layers 3 & 4 (network and transport layers). The NLB only cares about TLS, TCP, or UDP traffic and port numbers. The network load balancer just forward requests, whereas the application load balancer examines the contents of the HTTP request header to determine where to route the request. This is the distribution of traffic based on network variables, such as IP address and destination ports.
It is layer 4 (TCP) and below and is not designed to take into consideration anything at the application layer such as content type, cookie data, custom headers, user location, or the application behavior.
Application Load Balancer (ALB) – The application load balancer is the distribution of requests based on multiple variables, from the network layer to the application layer. The ALB can route HTTP and HTTPS traffic based on host or path-based rules. Like an NLB, each Target can be on different ports.
The NLB bases its route decisions solely on network and TCP-layer variables and has no awareness of the application. Generally, a network load balancer will determine โavailabilityโ based on the ability of a server to respond to ICMP ping or to complete the three-way TCP handshake correctly. Whereas, an application load balancer goes much deeper and can determine availability based on a successful HTTP GET of a particular page and the verification that the content is as expected based on the input parameters.
ASG – Auto Scaling Group
The auto-scaled web servers can automatically scale up or down according to load on the servers using an Auto-Scaling Group (ASG). However, this is a demonstration and not written for production deployments, so this code does not provide scaling servers based on demand, and instead, the code is written to provide scaling based on desired capacity.
I also have some code about using Terraform to deploy a Load Balanced WordPress Server with ASG and EFS as the persistent storage. That will probably be my next post.
Application Load Balancer – to distribute load amongst more than one server Auto Scaling Group – with launch template and ELB health check Simple Web servers – That will display EC2 instance data like Region, ID, and IP address Using Terraform to deploy infrastructure as code into the AWS cloud
All resources created in this exercise are compliant with an AWS Free Tier Plan
The resources are free only if you don’t leave them running! There is a limit of EC2 hours allowed per month!
You might incur a charge if you leave the Application Load Balancer running for very long. I usually spin this up, prove that it works for about 10 minutes, then run “Terraform Destroy” to ensure I’ve accomplished this exercise for free.
This exercise will perform the following tasks:
Create a VPC with two public and two private subnets
Create NAT instances instead of a NAT Gateway, security groups and network routing
Create an Auto Scaling Group with a launch template
The Auto Scaling Group will create EC2 instances running Apache Webpage
I created a webpage displaying EC2 ID, EC2 hostname, Region, and private IP address. It will demonstrate which EC2 server you connect to via the load balancer by showing its unique IP address.
Create an Application Load Balancer that automatically registers the EC2 servers created by the Auto Scaling Group
In previous exercises, I demonstrated Terraform using modular code. In this exercise, the code will not be modular. All of the code will be placed in one folder.
So first, create your folder to place our code, a folder named “ALB-Website,” perhaps?
Building the VPC
You do not need to create a “Terraform remote state” for this exercise. However, as a best practice, I use an S3 bucket to hold “Terraform’s remote state.” And I will write code that provides output data such that if I need a jump server to troubleshoot an EC2 server in the private network, I can use my modular code to deploy a server I call “the Controller (jump server).”
variable "ssh_location" {
type = string
description = "My Public IP Address"
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "av-zone1" {
type = string
}
variable "av-zone2" {
type = string
}
variable "public_cidr" {
type = string
default = "10.0.1.0/24"
}
variable "public_cidr2" {
type = string
default = "10.0.2.0/24"
}
variable "private_cidr" {
type = string
default = "10.0.101.0/24"
}
variable "private_cidr2" {
type = string
default = "10.0.102.0/24"
}
variable "nickname" {
type = string
}
variable "region" {
type = string
}
variable "key" {
type = string
}
variable "instance_type" {
description = "Type of EC2 instance to use"
type = string
default = "t2.micro"
}
variable "environment" {
description = "User selects environment"
type = string
}
variable "your_name" {
description = "Your Name?"
type = string
}
In the “variables.tf” file, we usually declar a default value to each variable. With this exercise, though, I’m creating a “terraform.tfvars” file. This allows us to add “terraform.tfvars” into our gitignore. A “gitignore” file controls GIT publishing to GitHub, by providin a list of files that informs GITl not be push the list of files in GitHub. The reason we do this is so that some values are not published to the public. By adding “terraform.tfvars” as part of the list in “.gitignore” file, we are informing GIT not to publish the file “terraform.tfvars” This allows us to safely assign values to our variables in “terraform.tfvars” file, like “my public IP address”, I really don’t want to my public IP address to be available to the public in GITHUB.
Terraform will by default look for a “terraform.tfvars” file in your folder. When declared variables do not include a default assignment (as is the case above, the variables are not assigned a default value.”
Lines 24-33, queries AWS API to retrieve the latest image for an EC2 instance that is configured as a NAT Server. You will notice that I was getting the instance manually and hardcoding the AMI ID (using the method in the following paragraph). Then I realized that all I have to do is query AWS API for the latest AWS NAT instance AMI ID.
Note: How to manually get an AMI Instance ID for a AWS NAT server: To find a “NAT AMI” for your AWS region, open the AWS Management Console. Go to the EC2 services. Select the AWS region of your choice in the Menu Bar. In the left-hand panel, find “AMIs” and click on the Amazon Machine Images (AMIs) panel. Select “Public Images” and filter on “amzn-ami-vpc-nat,” then find the most recent creation date and copy the AMI ID to use as your NAT AMI image for your selected region.
One of the early decisions on using an Auto-Scaling Group (ASG) is how the ASG will determine “Load” to scale up or scale down according to a load on our application. I’m not going to write about the different health checks used to determine “Load” on our application in this exercise. That will be a post to write at a later date. Suffice it to say that I’ve selected “Elastic Load Balancing health checks” to check whether the load balancer reports an instance is healthy, confirming whether the instance is available to handle requests to our website.
Building the Auto Scaling Group Code
First, we need to declare to Terraform that we are creating an ASG resource and give our ASG a name.
You’ll notice we’ve stated the health check type as “ELB” and provided the ALB target group name and ARN (find the ALB target group Terraform code below). We have also selected 300 seconds (5 minutes) for a grace period. And finally, “force_delete = true”. We are telling AWS that if any of our website servers are unhealthy for more than 5 minutes, delete the server, which then causes ASG to build another server to meet our desired capacity.
Our next step is to declare ASG sizing by stating the minimum, maximum, and desired number of servers.
min_size = 2
max_size = 4
desired_capacity = 2
Next and perhaps the most essential part of our ASG code is to declare if we are using a launch configuration or a launch template. We are going to use a launch template. Amazon Web Services recommends the use of launch templates but still supports (at the time of this writing) launch configurations.
We need to inform Terraform which launch template to use. We’ll use the “latest” parameter in this case as we have only one template version. Specifying a different version of our template is useful for Blue/Green deployments as an example.
launch_template {
name = aws_launch_template.website.name
version = aws_launch_template.website.latest_version
}
I always throw in a bit of code to obtain the latest data for an AMI (in this case, Ubuntu vs. Amazon Linux). This sets up the ability for Terraform to query AWS API for data regarding the latest regional AMI image to use in a Launch template.
data "aws_ssm_parameter" "ubuntu-focal" {
name = "/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id"
}
We first declare the type of resource and the resource name.
resource "aws_launch_template" "website" {
}
Then we declare the image ID, instance type, lifecycle rule, and security groups. Note the usage of “data.aws_ssm_parameter” obtaining the AMI by telling Terraform to query AWS API for ssm_parameters for AMI ID. The EC2 key pair is not required in the launch template; however, I always include the EC2 key name so that I can SSH into the servers in case of the need to troubleshoot the deployment.
We’ll be using launch templates to configure our servers. For ASG the configuration code is required to be encrypted. So in this example, we add a line that tells Terraform to encode with Base64 and render a template file with the following line:
Next up is the reference to a template file. We are using a file with an extension “TPL.” A bootstrap.tpl appears the same as creating a simple shell command like bootstrap.sh file. The contents look the same, and the difference is how Terraform handles the file in a launch template. Using the extension “.tpl” will allow us to pass external variables into the script (which I will demonstrate in my next exercise).
data "template_file" "bootstrap" {
template = file("bootstrap_web.tpl")
}
We need an IAM policy for our server
I have created some code to create an HTML page for our auto-scaled Apache Web Servers. The code will allow our Web server to show information about the host server, specifically the EC2 details. The launched server web page is just a few lines that display EC2 attributes. The HTML will show the region, the AMI ID, the server’s hostname, and the IP address. Seeing host server information will demonstrate which server our browser has been connected to via our load balancer.
So first, let’s create the IAM policy for our servers that enable our servers to describe the EC2 host details. I have two JSON files. One for creating a role that allows our servers to assume a role that will enable our EC2 to use the service “ec2.amazonaws.com”. The second JSON file creates the IAM policy, allowing the action to “ec2:Describe*”. Our Terraform code below creates the role, the policy, and the profile of our EC2 servers.
resource "aws_iam_role" "this" {
name = "alb-role"
assume_role_policy = file("trust_rel.json")
}
resource "aws_iam_role_policy" "this" {
name = "ec2-describe"
role = aws_iam_role.this.id
policy = file("policy.json")
}
resource "aws_iam_instance_profile" "this" {
role = aws_iam_role.this.name
}
Now let’s put this all together (asg.tf)
#--------- setup AWS permission for Website to describe EC2 ID -
resource "aws_iam_role" "this" {
name = "alb-role"
assume_role_policy = file("trust_rel.json")
}
resource "aws_iam_role_policy" "this" {
name = "ec2-describe"
role = aws_iam_role.this.id
policy = file("policy.json")
}
resource "aws_iam_instance_profile" "this" {
role = aws_iam_role.this.name
}
#--------- Get Ubuntu 20.04 AMI image (SSM Parameter data) -----
data "aws_ssm_parameter" "ubuntu-focal" {
name = "/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id"
}
# ----------- Create Launch Template -------------------------
resource "aws_launch_template" "website" {
image_id = data.aws_ssm_parameter.ubuntu-focal.value
# iam_instance_profile { name = "assume_role_profile" }
instance_type = var.instance_type
key_name = var.key
vpc_security_group_ids = ["${aws_security_group.web-sg.id}"]
user_data = base64encode("${data.template_file.bootstrap.rendered}")
lifecycle { create_before_destroy = true }
}
# ------- Create the Auto Scaling Group -----------------------
resource "aws_autoscaling_group" "website_asg" {
launch_template {
name = aws_launch_template.website.name
version = aws_launch_template.website.latest_version
}
vpc_zone_identifier = [aws_subnet.private-1.id, aws_subnet.private-2.id]
health_check_type = "ELB"
target_group_arns = [aws_lb_target_group.website-target.arn]
health_check_grace_period = 300
force_delete = true
min_size = 2
max_size = 4
desired_capacity = 2
tag {
key = "Name"
value = "website_ASG"
propagate_at_launch = true
}
}
data "template_file" "bootstrap" {
template = file("bootstrap_web.tpl")
}
Create the Application Load Balancer (ALB)
To build our application load balancer, we need to create several key elements:
Some of these actions can be combined, for example, forward and authenticate. This exercise will be using a simple webpage, so we will be simply forwarding it to our Web servers
We will use the action “forward” and set stickiness to false. Stickiness is when our ALB sends clients to the same server in the auto-scaling group in case the client gets disconnected. Since this is a simple demonstration, we don’t care which server a user is connected to when hitting refresh or reconnecting to our servers.
When you create a target group, you specify its target type, which determines the type of target you specify when registering targets with this target group. After you create a target group, you cannot change its target type.
The following are the possible target types:
Instance (The targets are specified by instance ID)
IP (The targets are IP addresses)
Lambda (The target is a Lambda function)
Use the attachment function (in our case to an Auto-scaling Group ARN)
We could just put a couple of servers placed in one or more availability zones and list the Instance IDs or the IP addresses of those servers. Or, we could simply list the CIDR blocks of one or more private subnets, and the target would be any server in the private subnet(s). You could do this, but in our exercise, we want to use an auto-scaling group (ASG) and have the ALB health checks work with the ASG to rebuild servers if they become unhealthy. Therefore we do not want to point at IP addresses or instance IDs.
alb_target.tf
resource "aws_lb_target_group" "website-target" {
name = "website-tg-${random_pet.app.id}-lb"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.my-vpc.id
health_check {
port = 80
protocol = "HTTP"
timeout = 5
interval = 10
}
}
# Create a new ALB Target Group attachment
resource "aws_autoscaling_attachment" "asg_attachment_website" {
autoscaling_group_name = aws_autoscaling_group.website_asg.id
lb_target_group_arn = aws_lb_target_group.website-target.arn
}
output.tf
output "aws_region" {
value = data.aws_region.current.name
}
output "vpc_id" {
value = aws_vpc.my-vpc.id
}
output "Controller-sg_id" {
value = [aws_security_group.controller-ssh.id]
}
output "lb_dns_name" {
value = aws_lb.website-alb.dns_name
}
output "Auto_Scaling_Group_Name" {
value = aws_autoscaling_group.website_asg.name
}
Deploy our Resources using Terraform
Be sure to edit the variables in terraform.tfvars (currently, it has bogus values)
If you are placing this into any other region than us-west-1, you will have to change the AMI ID for the NAT instances in the file “vpc.tf”.
In your terminal, go to the VPC folder and execute the following commands:
Terraform init
terraform validate
Terraform apply
Once the deployment is successful, the terminal will output something like the following output:
Copy the lb_dns_name, without the quotes, and paste the DNS name into any browser. If you have followed along and placed all of the code correctly, you should see something like the following:
Screen Shot
Notice Sometimes servers in an ASG take a few minutes to configure. Wait a couple of minutes if you get an error from our website and try again.
Open up your AWS Management Console, and go to the EC2 dashboard. Configure your EC2 dashboard to show tag columns with a tag value “Name.” A great way to identify your resources is by using TAGS!!
If you have configured the dashboard to display the tag column "Names" in your EC2 dashboard, you should quickly be able to see TWO NAT instances with the tag name "Test-NAT1" and "Test-NAT2" and TWO servers with the Tag Name "Website_ASG".
As an experiment, perhaps you would like to expand the number of Web servers. We can manually expand the number of desired capacity, and the Auto Scaling Group will automatically scale up or down the number of servers based on your command to change desired capacity.
Where ASG_Name in the command line above will be the terminals output of lb_dns_name (without the quotes, of course). If you successfully execute the command line in your terminal, you should eventually see in the EC2 dashboard four instances with the tag name “Website_ASG.” Demonstrating our ability to manually change the number of servers to four instead of two.
Once completed with this exercise, feel free to remove all resources by issuing the following command in the terminal:
terraform destroy
This is not for production!
All public websites should have an application firewall in between the Web Server and its internet connection, this exercise doesn’t create the application firewall. So do not use this configuration for production
All websites should have monitoring and a method to scrape log events to detect and alert for potential problems with the deployment.
This exercise uses resources compatible with the AWS Free Tier plan. It does not have sufficient compute sizing to support a production workload.
It is a good idea to remove All resources when you have completed this exercise so as not to incur costs
You may not require server-side encryption or versioning for this exercise. If multiple personnel use the same S3 bucket, you would undoubtedly want to consider enabling version control. If your team believes specific parameters are sensitive information, I suggest server-site encryption.
An S3 bucket name must be globally unique, that means literrally a unique name in ALL AWS regions
Create an S3 bucket for the Terraform remote state
It can be any S3 bucket name of your choice, but of course, I recommend a name something like “your_nickname.terraform.state”
Create an S3 bucket to hold configuration files for WordPress, MariaDB, and Ansible playbooks
I recommend a name like “your_nickname.ansible_files”
Creating the VPC
The VPC will be making:
One public subnet for the WordPress website and another server I call the Controller
One private subnet for the database server
NAT instances instead of a NAT gateway and the associative routing
Security Groups
Output data that other modules will use to obtain data.
Make sure you have configured an S3 bucket for Terraform Remote State and name the bucket something like “name-terraform-states.”
Create the following code “vpc.tf” in the VPC folder
There are Nine files placed in the Ansible folder. All the files for Ansible will be in my GitHub repository. Be sure to edit the appropriate files as stated below to personalize your choices for things like DB password.
ansible.cfg
hosts.ini
provision-db.yml
provision-wp.yml
The other five files will be placed into the MariaDB server and the WordPress server by Ansible.
Files for MariaDB
50-server.cnf
vars.yml
Files for WordPress
dir.conf
example.com.conf
wordpress.zip
Edit “vars.yml” to reflect your choices of USERNAME, PASSWORD, DBNAME, NEW_ADMIN, NEW_ADMIN_PASSWORD. Ensure you also edit wp-config.php in the “wordpress.zip” archive to reflect the same.
Uncompress wordpress.zip and edit “wp-config.php.” Lines 23,26, and 29 reflect your choices of DB_NAME, DB_USER, and DB_PASSWORD. Make sure they match “vars.yml.”
Create an S3 bucket for the above files
If you have not already created an S3 bucket to hold the ansible files, please do so now.
When we use the command to apply Terraform, the files will automatically be copied from our S3 bucket into the controller server below as part of the bootstrap_controller.sh. So make sure you have configured our S3 bucket and placed the Ansible files into that bucket before running Terraform Apply command.
Create a folder named Controller
A Controller is where all the magic happens. We are creating a jump server (I call it a controller).
After deploying the VPC infrastructure and placing the Ansible files into an S3 bucket, we create three servers (WordPress, MariaDB, and Controller).
We will then use SSH to connect to our Controller and Ansible playbooks to configure MySQL on the MariaDB server and to configure WordPress settings on the WordPress server.
Create the “controller.tf” file in the controller folder
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
#------------------------- State terraform backend location---------------------
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "surfingjoes-terraform-states"
key = "terraform.tfstate"
region = "us-west-1"
}
}
# --------------------- Determine region from backend data -------------------
provider "aws" {
region = data.terraform_remote_state.vpc.outputs.aws_region
}
# #--------- Get Ubuntu 20.04 AMI image (SSM Parameter data) -------------------
# data "aws_ssm_parameter" "ubuntu-focal" {
# name = "/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id"
# }
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# Creating controller node
resource "aws_instance" "controller" {
#ami = data.aws_ssm_parameter.ubuntu-focal.value # from SSM Paramater
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = data.terraform_remote_state.vpc.outputs.public_subnet
vpc_security_group_ids = data.terraform_remote_state.vpc.outputs.Controller-sg
iam_instance_profile = "${aws_iam_instance_profile.assume_role_profile.name}"
user_data = file("bootstrap_controller.sh")
private_ip = "10.0.1.10"
monitoring = true
key_name = var.key
tags = {
Name = "${var.environment}-Controller"
Stage = "${var.environment}"
Owner = "${var.your_name}"
}
}
output "Controller" {
value = [aws_instance.controller.public_ip]
}
Create the “variables.tf” file in the Controller folder
variable "aws_region" {
type = string
default = "us-west-1"
}
variable "key" {
type = string
default = "Mykey" #be sure to update with the name of your EC2 Key pair for your region
}
variable "instance_type" {
description = "Type of EC2 instance to use"
type = string
default = "t2.micro"
}
variable "environment" {
description = "User selects environment"
type = string
default = "Test"
}
variable "your_name" {
description = "Your Name?"
type = string
default = "Joe"
}
This will give our controller permission to copy files from our S3 bucket. Be sure to edit the ARN information to reflect the appropriate S3 bucket name for your deployment.
Be sure to use the name of the S3 bucket in “VPC.tf, servers.tf, controller.tf
Make an S3 bucket for the Ansible files.
Be sure to change the S3 Bucket name in S3_policy.tf (lines 16), shown above, to your S3 bucket name for Ansible files.
Be sure to change the variables in the VPC folder with variables of your choice.
Be sure to change the variables in the server folder to variables of your choice.
Change the vars.yml to reflect your dbname, user name, etc.
Be sure to uncompress the wordpress.zip file, edit the wp-config.php to reflect your dbname and user name of your choice, update the example.com.conf file to your domain name, and as well edit the content of example.com.conf to your domain name. Once the edits are complete, compress the files back into wordpress.zip.
Be sure to copy the ansible files into the bucket you created for the Ansible files
In your terminal, go to the VPC folder and execute the following commands:
Terraform init
terraform validate
Terraform apply
In your terminal, go to the server folder and execute the following commands:
Terraform init
terraform validate
Terraform apply
In your terminal, go to the controller folder and execute the following commands:
Terraform init
terraform validate
Terraform apply
Running the Ansible configuration on the Controller
First, we are going to set up our SSH credentials. This assumes you have already configured an EC2 key pair and assigned the key in the above terraform code. You must have already placed an EC2 key into your ssh directory.
Add EC2 key pair into SSH credentials by issuing the following command
ssh-add ~/.ssh/your-key-name.pem
Then connect to the Controller with “-A” option of SSH command to forward the authentication agent to our Controller. “-A” allows us to connect to the Controller, then jump to our servers inside AWS public and private networks. “-A” as an SSH option also enables the authentication agent for Ansible playbooks to use the very same EC2 key pair to manage MariaDB and WordPress servers.”
ssh -A ec2-user@1.2.3.4 (where 1.2.3.4 represents the public IP address of the Controller)
Once connected to the Controller, change the directory to the WordPress directory (this directory may not exist if you connect to the Controller too soon, as it takes a couple of minutes for the bootstrap to configure the Controller)
cd WordPress
First, we can configure the MariaDB server with Ansible
ansible-playbook provision-db.yml
for some reason, the handler at the end of the Ansible playbook doesn’t restart MySQL, and WordPress gets a “cannot connect to database error.” If this does happen to you (most likely, I might add), then we need to connect to the DB server and restart MySQL
ssh ubuntu@10.0.101.30
sudo -i
service MySQL restart
The final step is to run the Ansible playbook to configure the WordPress server
ansible-playbook provision-wp.yml
If you want to actually test WordPress feel free, you’ll need to create a record for your domain. Goto Route 53, register a domain, create an “A record” and assign the public IP address of WordPress Instance.
Or, if you do not want to use your domain (I recommend the practice of using your domain, but hey, what do I know, hee hee), open your browser and put in the public IP address of the WordPress server.
Open a browser and type in your domain.
If you have followed the exercise correctly, then you should see the following
Initial WordPress Screen
This is not for production!
All public websites should have an application firewall in between the Web Server and its internet connection, this exercise doesn’t create the application firewall. So do not use this configuration for production
All websites should have monitoring and a method to scrape log events to detect and alert for potential problems with the deployment.
This exercise uses resources compatible with the AWS Free Tier plan. It does not have sufficient compute sizing to support a production workload.
It is a good idea to remove All resources when you have completed this exercise, so as not to incur costs
Using Infrastructure as Code with Terraform to create an AWS Load-balanced website
OOPS: Things Change. The code in Github was completely operational. Now it doesn’t work. It was based on Amazon NAT instances that are no longer available.
This exercise creates a load-balanced website (similar to the previous exercise) but with essential differences (NAT Instances instead of NAT gateway and using Docker container instead of a custom AMI as a web server).
The ability to provision resources into AWS using “modular code.”
Four Web Servers behind a Classic load balancer
Ability to launch or destroy bastion host (jump server) only when required
Can add/remove bastion host (jump server) at any time without impact to other resources (Bastion Hosts – Provides administrators SSH access to servers located in a private network)
Difference – NAT Instance instead of NAT gateway
One of the differences between this code and the code sample in the previous exercise is that we’ll use NAT instances instead of a NAT gateway. A NAT gateway incurs costs even when using AWS under a free tier plan. It might only be a dollar or two per day. Still, it is a cost. So just for grins, I’ve created a VPC that uses AWS NAT instances to save a couple of dollars. A NAT instance does not compare to the performance of AWS NAT Gateways, so probably not a good solution for production. Considering we are simply running test environments, a NAT instance that performs a bit slower, and saves a few dollars, is fine with me!
Docker-based website
In the previous exercise, we used a custom AMI saved into our EC2 AMI library. A custom-built AMI works well because it allows us to customize an EC2 instance with our application and configuration and save it as a dedicated AMI image in our AWS account. A custom AMI enables greater control from a release management standpoint because our team has control of the composition of an AMI image.
However, creating a custom AMI and then saving an AMI into our EC2 library produces costs even when using a Free Tier plan. While it is great to use a custom AMI, it’s also essential to save money when we are simply studying AWS deployments within a Free Tier plan.
Docker to the rescue. We can create a custom docker container with our specific application and/or configuration like a custom AMI.
We will be using a boot script to install Docker and launch a Docker container, saving costs by not using a custom AMI image.
I’ve created a few websites (to use as docker containers). These containers utilize website templates that are free to use under a Creative Commons license. We’ll use one of my docker containers in this exercise with the intent to eventually jump into using docker containers in ECS and EKS deployments in future activities.
The change from NAT gateway to NAT instance has an impact on our VPC configuration
VPC Changes
We will use standard Terraform AWS resources code instead of a Terraform Module. Hence we’ll be using standard Terraform code to create a VPC.
Also had to change the security group’s code from using Terraform Modules to using Terraform resource code and the methods of referencing AWS resources instead of modules.
Terraform Outputs had to be changed as well to recognize the above changes
ELB changes
We will use standard Terraform AWS resource code instead of the Terraform community module to create a classic load balancer.
If you performed the previous exercise, you might be tempted to try and use the same VPC code. Unfortunately, we are using NAT instances instead of a NAT gateway. We require a new code to create this VPC. The other modules in this exercise are explicitly written with references to this type of VPC found below.
So let us get started
First, please create the following folder structure shown below.
VPC
The following code “vpc.tf”, “var.tf”, and “security_groups.tf” will be created and placed into the VPC folder.
The code below creates a VPC, two public subnets, two private subnets, two NAT instances (one for each public subnet), routing for the public subnets, and routing for the private subnets.
Code for Classic Load Balancer and Docker web servers (ELB-Web.tf)
The following code “elb-web.tf”, “var.tf”, and “bootstrap_docker.sh” will create an AWS classic load balancer, and four web servers (two in each public subnet). These files will need to be placed into a separate folder, as the code is written to be modular and to obtain data from Terraform Remote state output data. It literally will not work if placed into the same folder as the VPC code.
It is not required to even create the following code for the load-balanced web servers to work. But, because the VPC code is different from the previous exercise, I’m including the code for a jump server (aka bastion host, or as I call it a controller because I use the jump server to deploy ansible configurations on occasion). A jump server is also sometimes necessary to SSH into servers on a private network for analyzing failed deployments. It certainly comes in handy to have a jump server!
The following files will be placed into a separate folder, in this case, named “controller”. The files “controller.tf”, “variables.tf”, and “bootstratp_controller.sh” will create the jump server (Controller).
Once again this is modular code, and won’t work if these files are placed into the same folder as the VPC code. The code depends on output data being placed into Terraform remote state S3 bucket and this code references the output data as inputs to the controller code.
Create file “controller.tf”
Note; I have some code commented out in case you want the controller to be an UBUNTU server instead of an AMI Linux server. I’ve used both flavors over time and hence my module allows me to use choose at the time of deployment by manipulating which lines are commented.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
#------------------------- State terraform backend location-----
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "surfingjoes-terraform-states"
key = "terraform.tfstate"
region = "us-west-1"
}
}
# --------------------- Determine region from backend data -----
provider "aws" {
region = data.terraform_remote_state.vpc.outputs.aws_region
}
# #--------- Get Ubuntu 20.04 AMI image (SSM Parameter data) ---
# data "aws_ssm_parameter" "ubuntu-focal" {
# name = "/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id"
# }
data "aws_ami""amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# Creating controller node
resource "aws_instance" "controller" {
#ami = data.aws_ssm_parameter.ubuntu-focal.value # from SSM Paramater
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = data.terraform_remote_state.vpc.outputs.public_subnet_1
vpc_security_group_ids = data.terraform_remote_state.vpc.outputs.Controller-sg_id
user_data = file("bootstrap_controller.sh")
private_ip = "10.0.1.10"
monitoring = true
key_name = var.key
tags = {
Name = "${var.environment}-Controller"
Stage = "${var.environment}"
Owner = "${var.your_name}"
}
}
output "Controller" {
value = [aws_instance.controller.public_ip]
}
Be sure to change the S3 Bucket name in S3_policy.tf (lines 16 & 17), shown above in Red, into your S3 bucket name
Be sure to change the test.tfvars in the VPC folder, variables of your choice
Be sure to change the test.tfvars in the ELB-WEB folder, to variables of your choice
Be sure to change the main.tf lines 11-13 with the configuration for your S3 bucket to store terraform backend state
In your terminal, go to the VPC folder and execute the following commands:
Terraform init
terraform validate
Terraform apply
In your terminal, go to the elb-web folder and execute the following commands:
Terraform init
terraform validate
Terraform apply
That is it, we have launched and should now have a load-balanced static website with resilience across availability zones and within each zone have at least two web servers for high availability