Production-Like AWS Environment Provisioning with Terraform
Learn more about both AWS and Terraform by creating an AWS environment that you can see in-browser using this tutorial.
Join the DZone community and get the full member experience.
Join For FreeThis blog provides a template for provisioning a full AWS infrastructure from the ground using Terraform. So just to keep things simple for this blog, we will create a simple web app which will read some data from a table and will show the result in a browser.
To achieve this simple task, we will try to create a production-ready environment in AWS using Terraform automation which will require us to set up a VPC, Network Gateway, subnets, routes, security groups, an EC2 machine with MySQL installed inside a private network, and a web app machine with Apache and its PHP module in a public subnet.
If you are interested to know the reasons for choosing Terraform for AWS automation, then read my blog on Terraform here. It is an interesting tool for Infrastructure-as-Code (IAC), written in Go language, and gives you the ability to describe a complex infrastructure through configuration scripts written in HCL.
The most important point is that you can use it to provision infrastructure across multiple IAAS providers like AWS, GCP, and Azure.
Prerequisites
There are only two prerequisites for this:
- Terraform – Installation instructions are covered in my previous blog here.
- Free account with AWS. You can register for free Tier AWS account here.
I have used Ubuntu as the OS in this tutorial but instructions should work with any operating system.
Infrastructure-as-Code
The following diagram shows the high-level architecture diagram on how infrastructure will be provisioned in AWS:
Building Blocks
We will be creating following configuration files to describe the infrastructure –
Variable.tf
This file contains the variables and configurable properties used in other scripts. This file also contains the secret and access keys and key name for AWS. You will have to insert them in this file before you proceed further.
However, keep in mind that it is not a good practice to put access and secret keys in a .tf file due to security risks. But for this tutorial, I am putting them in a variables.tf file to keep it simple.
A better approach would be either to use a credential file in ~/.aws/ or prompt user to enter access and secret keys in the shell/command line.
variable "region" {
default = "eu-west-2"
}
variable "AmiLinux" {
type = "map"
default = {
eu-west-2 = "ami-a36f8dc4"
eu-west-1 = "ami-ca0135b3"
us-east-1 = "ami-14c5486b"
}
}
variable "aws_access_key" {
default = ""
description = "user aws access key"
}
variable "aws_secret_key" {
default = ""
description = " user aws secret key"
}
variable "vpc-fullcidr" {
default = "172.16.0.0/16"
description = "the vpc cdir"
}
variable "Subnet-Public-AzA-CIDR" {
default = "172.16.0.0/24"
description = "the cidr of the subnet"
}
variable "Subnet-Private-AzA-CIDR" {
default = "172.16.3.0/24"
description = "the cidr of the subnet"
}
variable "key_name" {
default = "MyAWSKey"
description = "the ssh key to use in the EC2 machines"
}
variable "DnsZoneName" {
default = "ShaanAWSDNS.internal"
description = "the internal dns name"
}
Network.tf
This file does following tasks:
- Set up the provider for AWS
- Create a VPC
- Set the options for internal VPC DNS resolution
provider "aws" {
access_key = "${var.aws_access_key}"
secret_key = "${var.aws_secret_key}"
region = "${var.region}"
}
resource "aws_vpc" "terraformmain" {
cidr_block = "${var.vpc-fullcidr}"
#### this 2 true values are for use the internal vpc dns resolution
enable_dns_support = true
enable_dns_hostnames = true
tags {
Name = "My terraform vpc"
}
}
Routing.tf
This file will accomplish the following configuration:
- attach an internet gateway to the VPC
- define a network ACL
- define two routing tables: one for public access, and the other one for private access
Along with this, we will also define an AWS NAT Gateway to make the database machine more secure as it will need internet access to install MySQL. AWS NAT Gateway will ensure that there are no incoming requests from outside the database. It is important, however, to deploy it in a public subnet and associate an elastic IP to it. The depends_on
attribute allows us to avoid errors and create the NAT gateway only after the internet gateway is in the available state.
# Declare the data source
data "aws_availability_zones" "available" {}
/* EXTERNAL NETWORK , IG, ROUTE TABLE */
resource "aws_internet_gateway" "gw" {
vpc_id = "${aws_vpc.terraformmain.id}"
tags {
Name = "internet gw terraform generated"
}
}
resource "aws_network_acl" "all" {
vpc_id = "${aws_vpc.terraformmain.id}"
egress {
protocol = "-1"
rule_no = 2
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
ingress {
protocol = "-1"
rule_no = 1
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
tags {
Name = "open acl"
}
}
resource "aws_route_table" "public" {
vpc_id = "${aws_vpc.terraformmain.id}"
tags {
Name = "Public"
}
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.gw.id}"
}
}
resource "aws_route_table" "private" {
vpc_id = "${aws_vpc.terraformmain.id}"
tags {
Name = "Private"
}
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = "${aws_nat_gateway.PublicAZA.id}"
}
}
resource "aws_eip" "forNat" {
vpc = true
}
resource "aws_nat_gateway" "PublicAZA" {
allocation_id = "${aws_eip.forNat.id}"
subnet_id = "${aws_subnet.PublicAZA.id}"
depends_on = ["aws_internet_gateway.gw"]
}
Subnets.tf
resource "aws_subnet" "PublicAZA" {
vpc_id = "${aws_vpc.terraformmain.id}"
cidr_block = "${var.Subnet-Public-AzA-CIDR}"
tags {
Name = "PublicAZA"
}
availability_zone = "${data.aws_availability_zones.available.names[0]}"
}
resource "aws_route_table_association" "PublicAZA" {
subnet_id = "${aws_subnet.PublicAZA.id}"
route_table_id = "${aws_route_table.public.id}"
}
resource "aws_subnet" "PrivateAZA" {
vpc_id = "${aws_vpc.terraformmain.id}"
cidr_block = "${var.Subnet-Private-AzA-CIDR}"
tags {
Name = "PublicAZB"
}
availability_zone = "${data.aws_availability_zones.available.names[1]}"
}
resource "aws_route_table_association" "PrivateAZA" {
subnet_id = "${aws_subnet.PrivateAZA.id}"
route_table_id = "${aws_route_table.private.id}"
}
There are two subnets associated with the respective routes: a public and a private.
DNS-and-DHCP.tf
This file will realize three tasks:
- Create private Route53 DNS zone
- Association with VPC
- Create the DNS record for database
resource "aws_vpc_dhcp_options" "shaandhcp" {
domain_name = "${var.DnsZoneName}"
domain_name_servers = ["AmazonProvidedDNS"]
tags {
Name = "My internal name"
}
}
resource "aws_vpc_dhcp_options_association" "dns_resolver" {
vpc_id = "${aws_vpc.terraformmain.id}"
dhcp_options_id = "${aws_vpc_dhcp_options.shaandhcp.id}"
}
/* DNS PART ZONE AND RECORDS */
resource "aws_route53_zone" "main" {
name = "${var.DnsZoneName}"
vpc_id = "${aws_vpc.terraformmain.id}"
comment = "Managed by terraform"
}
resource "aws_route53_record" "database" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "mydatabase.${var.DnsZoneName}"
type = "A"
ttl = "300"
records = ["${aws_instance.database.private_ip}"]
}
It is important to note that the database DNS record depends on the private IP of the EC2 database machine. This machine will be allocated during the database creation.
Securitygroups.tf
resource "aws_security_group" "WebApp" {
name = "WebApp"
tags {
Name = "WebApp"
}
description = "ONLY HTTP CONNECTION INBOUND"
vpc_id = "${aws_vpc.terraformmain.id}"
ingress {
from_port = 80
to_port = 80
protocol = "TCP"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = "22"
to_port = "22"
protocol = "TCP"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "MySQLDB" {
name = "MySQLDB"
tags {
Name = "MySQLDB"
}
description = "ONLY tcp CONNECTION INBOUND"
vpc_id = "${aws_vpc.terraformmain.id}"
ingress {
from_port = 3306
to_port = 3306
protocol = "TCP"
security_groups = ["${aws_security_group.WebApp.id}"]
}
ingress {
from_port = "22"
to_port = "22"
protocol = "TCP"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
This file will create two security groups: one for the web application, and another for the database. As mentioned earlier both will have the outbound (egress) rule to have internet access for yum to install the Apache and MySQL servers, but the connection to the MySQL port will be allowed only from instances that belong to the webapp security group.
EC2.tf
This is the configuration for creating the EC2 machines in AWS with the userdata scripts for installing and setting up the web server and MySQL database.
resource "aws_instance" "phpapp" {
ami = "${lookup(var.AmiLinux, var.region)}"
instance_type = "t2.micro"
associate_public_ip_address = "true"
subnet_id = "${aws_subnet.PublicAZA.id}"
vpc_security_group_ids = ["${aws_security_group.WebApp.id}"]
key_name = "${var.key_name}"
tags {
Name = "My Web App"
}
user_data = <<HEREDOC
#!/bin/bash
yum update -y
yum install -y httpd24 php56 php56-mysqlnd
service httpd start
chkconfig httpd on
echo "<?php" >> /var/www/html/myApp.php
echo "\$conn = new mysqli('mydatabase.ShaanAWSDNS.internal', 'root', 'secret', 'test');" >> /var/www/html/myApp.php
echo "\$sql = 'SELECT * FROM Employees'; " >> /var/www/html/myApp.php
echo "\$result = \$conn->query(\$sql); " >> /var/www/html/myApp.php
echo "while(\$row = \$result->fetch_assoc()) { echo 'the value is: ' . \$row['NAME'], \$row['ADDRESS'];} " >> /var/www/html/myApp.php
echo "\$conn->close(); " >> /var/www/html/myApp.php
echo "?>" >> /var/www/html/myApp.php
HEREDOC
}
resource "aws_instance" "database" {
ami = "${lookup(var.AmiLinux, var.region)}"
instance_type = "t2.micro"
associate_public_ip_address = "false"
subnet_id = "${aws_subnet.PrivateAZA.id}"
vpc_security_group_ids = ["${aws_security_group.MySQLDB.id}"]
key_name = "${var.key_name}"
tags {
Name = "sql database"
}
user_data = <<HEREDOC
#!/bin/bash
sleep 180
yum update -y
yum install -y mysql55-server
service mysqld start
/usr/bin/mysqladmin -u root password 'secret'
mysql -u root -psecret -e "create user 'root'@'%' identified by 'secret';" mysql
mysql -u root -psecret -e 'CREATE TABLE Employees (ID int(11) NOT NULL AUTO_INCREMENT, NAME varchar(45) DEFAULT NULL, ADDRESS varchar(255) DEFAULT NULL, PRIMARY KEY (ID));' test
mysql -u root -psecret -e "INSERT INTO Employees (NAME, ADDRESS) values ('JOHN', 'LONDON UK') ;" test
HEREDOC
}
Web App EC2 Instance:
It is placed in the public subnet so that it can be accessed from browser using port 80. The user data performs the following actions:
- yum update
- installs Apache web server and its PHP module
- starts Apache and sets the config to start it automatically on boot
- using the echo command to place a PHP file in the /var/www/html directory and reads the value from the table created inside the database in another EC2 instance.
MySQL DB EC2 Instance:
It is placed in the private subnet and has its security group. The user data performs the following actions:
- yum update
- installs the MySQL server and runs it
- configures the root user to grant access from other machines
- creates a table in the database and adds one record to the table
Provision it with Terraform
There are three commands which are pretty much required to provision the infrastructure using Terraform.
-
terraform init
The first command isterraform init
which will install the AWS plugin for Terraform and will successfully initialize it.
-
terraform plan
If you would like to review what actions terraform will perform in AWS before actually doing it then you can runterraform plan
command which will check your changes before you unleash them onto the world.
-
terraform apply
The third and most important command isterraform apply
which will apply the changes and provision the environment in AWS.
It will take few minutes for the environment to provision but once completed you can go to the AWS web console and verify that VPC, EC2 instances, network gateway, and subnets are created properly.
Take the public IP of the web app EC2 instance and verify that you are able to access it in the browser and see the results from the table there.
Clean up the Infrastructure Provisioned in AWS
While developing scripts and testing them, you may need to destroy the infrastructure created in AWS. This can be accomplished with a single command ‘terraform destroy’and you don’t have to worry about deleting/terminating each and every service in AWS manually –
Wrapping Up
The scripts used in this article are also available in GitHub.
This was a quick tutorial to show how to provision infrastructure in AWS using Terraform. Even though this was a two-tier set up, you can use the concepts to set up a multi-stack infrastructure with various components. You can also make changes as suggested in my post to keep the AWS access key and secret keys to avoid any security issues.
Published at DZone with permission of Shashank Rastogi. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments