<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Terraform &#8211; Blog of Kliment Andreev &#8211; A place so I won&#039;t forget things</title>
	<atom:link href="https://blog.andreev.it/tag/terraform/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.andreev.it</link>
	<description></description>
	<lastBuildDate>Mon, 02 Nov 2020 13:55:44 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>AWS: Deploy nginx HA cluster with Packer, Terraform and Ansible in a new VPC environment</title>
		<link>https://blog.andreev.it/2019/12/aws-deploy-nginx-ha-cluster-with-packer-terraform-and-ansible/</link>
					<comments>https://blog.andreev.it/2019/12/aws-deploy-nginx-ha-cluster-with-packer-terraform-and-ansible/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sun, 29 Dec 2019 23:36:48 +0000</pubDate>
				<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Ansible]]></category>
		<category><![CDATA[Packer]]></category>
		<category><![CDATA[Terraform]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=6372</guid>

					<description><![CDATA[In this post, I&#8217;ll explain how to create a whole environment consisting of a&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post, I&#8217;ll explain how to create a whole environment consisting of a new VPC, two public subnets, two private subnets, a bastion host and two application hosts that run nginx web server. These application hosts will be behind an application load balancer. The purpose of the bastion host in the public subnet is to be able to access the application hosts which are in the private subnet and have no public access directly. We&#8217;ll use the official CentOS 7 image from the marketplace, make some modifications with packer and bake our own image that has a local ansible. Once these servers are deployed, they&#8217;ll already have nginx installed and started and a fully functional template website from GitHub (courtesy of <a href="https://www.free-css.com/free-css-templates" rel="noopener noreferrer" target="_blank">free-css.com</a>). This is how it looks like in a diagram.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P142-05.jpg"><img fetchpriority="high" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P142-05.jpg" alt="" width="962" height="879" class="aligncenter size-full wp-image-6403" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P142-05.jpg 962w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-05-300x274.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-05-768x702.jpg 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-05-585x535.jpg 585w" sizes="(max-width: 962px) 100vw, 962px" /></a><br />
Before you start, I suggest you create a directory where you are going to place all these files. Make sure to execute each command from that directory unless told otherwise.</p>
<h1>Packer and ansible</h1>
<p>Packer is super easy to install. Just get the executable and place it somewhere. Once ready to create an image, specify your AWS access key and secret key. The IAM associated with these keys should be able to create an AMI. This is my packer file.</p>
<pre class="brush: xml; title: ; notranslate">
{
  &quot;variables&quot;: {
    &quot;aws_access_key&quot;: &quot;&quot;,
    &quot;aws_secret_key&quot;: &quot;&quot;,
    &quot;ami_name&quot;: &quot;centos7-USWTA&quot;,
    &quot;region&quot;: &quot;us-east-1&quot;,
    &quot;source_ami&quot;: &quot;ami-02eac2c0129f6376b&quot;,
    &quot;instance_type&quot;: &quot;t2.micro&quot;
  },
  &quot;builders&quot;: &#x5B;{
    &quot;access_key&quot;: &quot;{{user `aws_access_key`}}&quot;,
    &quot;secret_key&quot;: &quot;{{user `aws_secret_key`}}&quot;,
    &quot;ami_name&quot;: &quot;{{user `ami_name`}}.{{timestamp}}&quot;,
    &quot;region&quot; : &quot;{{user `region`}}&quot;,
    &quot;source_ami&quot;: &quot;{{user `source_ami`}}&quot;,
    &quot;instance_type&quot;: &quot;{{user `instance_type`}}&quot;,
    &quot;ssh_username&quot;: &quot;centos&quot;,
    &quot;type&quot;: &quot;amazon-ebs&quot;
  }],
  &quot;provisioners&quot;: &#x5B;
    {
     &quot;type&quot;: &quot;shell&quot;,
      &quot;inline&quot;: &quot;sudo yum -y install epel-release&quot;
    },
    {
      &quot;type&quot;: &quot;shell&quot;,
      &quot;inline&quot;: &quot;sudo yum -y install ansible&quot;
    },
    {
      &quot;type&quot;: &quot;file&quot;,
      &quot;source&quot;: &quot;ansible.yml&quot;,
      &quot;destination&quot;: &quot;/home/centos/ansible.yml&quot;
    },
    {
      &quot;type&quot;: &quot;ansible-local&quot;,
      &quot;playbook_file&quot;: &quot;ansible.yml&quot;
    }
  ]
}
</pre>
<p>You can specify your access keys in the script, but you don&#8217;t want to. You can specify the name of the AMI image, the region and the instance type. Do not change the source AMI variable, that&#8217;s the id of the CentOS official image. In the provisioners section, we&#8217;ll install the epel repo so we can install ansible. In addition, packer will get a local ansible playbook and place it under /home/centos in the new image. So, you need the <strong>ansible.yml</strong> playbook in the same directory as <strong>packer.json</strong> file. This is the ansible playbook. You definitely won&#8217;t do it this way in production. In my case I rename the original HTML root and clone a github repo. You&#8217;ll probably want to clone the site in a separate directory and use a modified <strong>nginx.conf</strong> file that suits your needs.</p>
<pre class="brush: xml; title: ; notranslate">
---
- hosts: localhost
  become: true

  tasks:
    - name: install nginx
      yum:
        name: nginx
        state: present

    - name: start nginx
      service:
        name: nginx
        state: started
        enabled: yes

    - name: install git
      yum:
        name: git
        state: present

    - name: remove the original html
      shell: |
        mv /usr/share/nginx/html /usr/share/nginx/html.old

    - name: clone web template
      git:
        repo: https://github.com/klimenta/webtemplate
        dest: /usr/share/nginx/html
        version: master
</pre>
<p>This playbook installs nginx, git and clones a website template from my github account. So, with both files ready (<strong>ansible.yml</strong> and <strong>packer.json</strong>), you can create your own image. Replace your own access key and secret and run this command.</p>
<pre class="brush: bash; title: ; notranslate">
packer build -var 'aws_access_key=AKIA.....' -var 'aws_secret_key=Dp123Sm4.....' packer.json
</pre>
<p>It takes about 5-6 minutes to bake the AMI. If you go to your AWS account, you&#8217;ll see it there.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P142-01.png"><img decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P142-01.png" alt="" width="1501" height="475" class="aligncenter size-full wp-image-6374" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P142-01.png 1501w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-01-300x95.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-01-1024x324.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-01-768x243.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-01-1170x370.png 1170w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-01-585x185.png 585w" sizes="(max-width: 1501px) 100vw, 1501px" /></a><br />
With our image ready, we are ready to deploy the infrastructure environment using Terraform. But before we do that, we want to create the keys that we will use to log in to our servers. In a production environment you&#8217;ll choose to have separate keys for the bastion host and the app hosts, but for the sake of clarity, we&#8217;ll use one key to rule them all.<br />
Type this command and hit Enter twice to skip the passphrase.</p>
<pre class="brush: bash; title: ; notranslate">
ssh-keygen -t rsa -f $PWD/keyUSWTA  -b 2048
</pre>
<p>You&#8217;ll have two files, <strong>keyUSWTA </strong>and <strong>keyUSWTA.pub</strong>. keyUSWTA is your private key, don&#8217;t share it with anyone. The public key will be used to spin-up the instances from our image. If you use putty instead of ssh, you have to <a href="https://www.puttygen.com/convert-pem-to-ppk" rel="noopener noreferrer" target="_blank">convert </a>your private key in a ppk format.</p>
<h1>Terraform</h1>
<p>Terraform is also super easy to install. Just copy the file somewhere in your path and you are ready to go. For Terraform we&#8217;ll have 4 separate files to build the infra. The first file is <strong>variables.tf</strong>. This is where we define all the variables that are in use. The second file is <strong>terraform.tfvars</strong>. This file is where we assign values to our variables. The first file can go to GitHub or any other version control system, the second one can not, especially if you have passwords and keys there. But it&#8217;s up to you. The third file is <strong>outputs.tf</strong>. Terraform dumps the values of this file when it&#8217;s done provisioning. For example, you want to see what&#8217;s your external IP from the instance you just created instead of going to AWS console to find out. And the fourth file is <strong>main.tf</strong> where we define our infrastructure. So, here they are.<br />
<strong>variables.tf</strong></p>
<pre class="brush: bash; title: ; notranslate">
variable &quot;aws_region&quot; {}
variable &quot;aws_az1&quot; {}
variable &quot;aws_az2&quot; {}
variable &quot;aws_profile&quot; {}
variable &quot;vpc_cidr&quot; {}
variable &quot;sub_private1_cidr&quot; {}
variable &quot;sub_private2_cidr&quot; {}
variable &quot;sub_public1_cidr&quot; {}
variable &quot;sub_public2_cidr&quot; {}
variable &quot;bastion_ami_id&quot; {}
variable &quot;instance_type&quot; {}
variable &quot;ip_address&quot; {}
</pre>
<p><strong>terraform.tfvars</strong></p>
<pre class="brush: bash; title: ; notranslate">
aws_profile = &quot;default&quot;
aws_region  = &quot;us-east-1&quot;
aws_az1 = &quot;us-east-1a&quot;
aws_az2 = &quot;us-east-1b&quot;
vpc_cidr = &quot;192.168.200.0/23&quot;
sub_public1_cidr = &quot;192.168.200.0/25&quot;
sub_public2_cidr = &quot;192.168.200.128/25&quot;
sub_private1_cidr = &quot;192.168.201.0/25&quot;
sub_private2_cidr = &quot;192.168.201.128/25&quot;
bastion_ami_id = &quot;ami-02eac2c0129f6376b&quot;
instance_type = &quot;t2.micro&quot;
ip_address = &quot;1.2.3.4/32&quot;
</pre>
<p>This means I&#8217;ll use my default profile for AWS CLI, the region for deployment is us-east-1, then the CIDRs for the VPC and the public and private subnets. The bastion host is based on the AMI there, which is CentOS 7 and t2.micro. And that host will have access from 1.2.3.4 IP only. Change to suit your needs.<br />
<strong>outputs.tf</strong></p>
<pre class="brush: bash; title: ; notranslate">
output &quot;aws_lb&quot; {
  value = aws_lb.albUSWTA.dns_name
}
output &quot;aws_ip&quot; {
  value = aws_instance.ec2USWTABastion.public_ip
}
output &quot;aws_app1_ip&quot; {
  value = aws_instance.ec2USWTAApplication1.private_ip
}
output &quot;aws_app2_ip&quot; {
  value = aws_instance.ec2USWTAApplication2.private_ip
}
</pre>
<p>These are the values that will be dumped on your screen, once Terraform completes. You&#8217;ll see the URL of your load balancer which is how you&#8217;ll access your website. Then, the public IP of the bastion host and the private IPs of your app instances in the private subnet.<br />
And finally, this is the <strong>main.tf</strong> file that provisions the infrastructure. Click the <strong>(+)</strong> sign to expand.</p>
<pre class="brush: bash; collapse: true; light: false; title: ; toolbar: true; notranslate">
provider &quot;aws&quot; {
  region  = var.aws_region
  profile = var.aws_profile
}

# VPC
resource &quot;aws_vpc&quot; &quot;vpcUSWTA&quot; {
  cidr_block = var.vpc_cidr

  tags = {
    Name = &quot;vpcUSWTA&quot;
  }
}

# Public subnet 1
resource &quot;aws_subnet&quot; &quot;subPublic1&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id
  cidr_block = var.sub_public1_cidr
  availability_zone = var.aws_az1

  tags = {
    Name = &quot;subPublic - USWTA - 1&quot;
  }
}

# Public subnet 2
resource &quot;aws_subnet&quot; &quot;subPublic2&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id
  cidr_block = var.sub_public2_cidr
  availability_zone = var.aws_az2

  tags = {
    Name = &quot;subPublic - USWTA - 2&quot;
  }
}

# Private subnet 1
resource &quot;aws_subnet&quot; &quot;subPrivate1&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id
  cidr_block = var.sub_private1_cidr
  availability_zone = var.aws_az1

  tags = {
    Name = &quot;subPrivate - USWTA - 1&quot;
  }
}

# Private subnet 2
resource &quot;aws_subnet&quot; &quot;subPrivate2&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id
  cidr_block = var.sub_private2_cidr
  availability_zone = var.aws_az2

  tags = {
    Name = &quot;subPrivate - USWTA - 2&quot;
  }
}

# Internet gateway
resource &quot;aws_internet_gateway&quot; &quot;igwUSWTA&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id

  tags = {
    Name = &quot;igwInternetGateway - USWTA&quot;
  }
}

# Key pair
resource &quot;aws_key_pair&quot; &quot;keyUSWTA&quot; {
  key_name   = &quot;Key for USWTA&quot;
  public_key = file(&quot;${path.module}/keyUSWTA.pub&quot;)
}

# Security group for bastion host
resource &quot;aws_security_group&quot; &quot;sgUSWTABastion&quot; {
  name = &quot;sgUSWTA - Bastion&quot;
  description = &quot;Allow access on port 22 from restricted IP&quot;
  vpc_id = aws_vpc.vpcUSWTA.id

  ingress {
    from_port = 22
    to_port = 22
    protocol = &quot;tcp&quot;
    cidr_blocks = &#x5B;var.ip_address]
  }

  egress {
    from_port       = 0
    to_port         = 0
    protocol        = &quot;-1&quot;
    cidr_blocks     = &#x5B;&quot;0.0.0.0/0&quot;]
  }

  tags = {
    Name = &quot;Allow access on port 22 from my IP&quot;
  }
}

# Security group for application load balancer
resource &quot;aws_security_group&quot; &quot;sgUSWTAALB&quot; {
  name = &quot;sgUSWTA - ALB&quot;
  description = &quot;Allow access on port 80 from everywhere&quot;
  vpc_id = aws_vpc.vpcUSWTA.id

  ingress {
    from_port = 80
    to_port = 80
    protocol = &quot;tcp&quot;
    cidr_blocks = &#x5B;&quot;0.0.0.0/0&quot;]
  }

  egress {
    from_port       = 0
    to_port         = 0
    protocol        = &quot;-1&quot;
    cidr_blocks     = &#x5B;&quot;0.0.0.0/0&quot;]
  }

  tags = {
    Name = &quot;Allow HTTP access from everywhere&quot;
  }
}

# Security group for application hosts
resource &quot;aws_security_group&quot; &quot;sgUSWTAApplication&quot; {
  name = &quot;sgUSWTA - Application&quot;
  description = &quot;Allow access on ports 22 and 80&quot;
  vpc_id = aws_vpc.vpcUSWTA.id

  ingress {
    from_port = 22
    to_port = 22
    protocol = &quot;tcp&quot;
    cidr_blocks = &#x5B;var.sub_public1_cidr]
  }

  ingress {
    from_port = 80
    to_port = 80
    protocol = &quot;tcp&quot;
    security_groups = &#x5B;aws_security_group.sgUSWTAALB.id]
  }

  egress {
    from_port       = 0
    to_port         = 0
    protocol        = &quot;-1&quot;
    cidr_blocks     = &#x5B;&quot;0.0.0.0/0&quot;]
  }

  tags = {
    Name = &quot;Allow access on ports 22 and 80&quot;
  }
}

# Get the latest image
data &quot;aws_ami&quot; &quot;image&quot; {
  most_recent = true
  owners = &#x5B;&quot;self&quot;]
  filter {
    name = &quot;name&quot;
    values = &#x5B;&quot;centos7-USWTA*&quot;]
  }
}

# ec2 instance - bastion host
resource &quot;aws_instance&quot; &quot;ec2USWTABastion&quot; {
  ami = var.bastion_ami_id
  instance_type = var.instance_type
  key_name = aws_key_pair.keyUSWTA.key_name
  vpc_security_group_ids = &#x5B;aws_security_group.sgUSWTABastion.id]
  subnet_id = aws_subnet.subPublic1.id
  associate_public_ip_address = true

  root_block_device {
    delete_on_termination = true
  }

  tags = {
    Name = &quot;ec2USWTA - Bastion&quot;
  }
}

# ec2 instance - app host 1
resource &quot;aws_instance&quot; &quot;ec2USWTAApplication1&quot; {
  ami = data.aws_ami.image.id
  instance_type = var.instance_type
  key_name = aws_key_pair.keyUSWTA.key_name
  vpc_security_group_ids = &#x5B;aws_security_group.sgUSWTAApplication.id]
  subnet_id = aws_subnet.subPrivate1.id
  associate_public_ip_address = false

  root_block_device {
    delete_on_termination = true
  }

  tags = {
    Name = &quot;ec2USWTA - Application - 1&quot;
  }
}

# ec2 instance - app host 2
resource &quot;aws_instance&quot; &quot;ec2USWTAApplication2&quot; {
  ami = data.aws_ami.image.id
  instance_type = var.instance_type
  key_name = aws_key_pair.keyUSWTA.key_name
  vpc_security_group_ids = &#x5B;aws_security_group.sgUSWTAApplication.id]
  subnet_id = aws_subnet.subPrivate2.id
  associate_public_ip_address = false

  root_block_device {
    delete_on_termination = true
  }

  tags = {
    Name = &quot;ec2USWTA - Application - 2&quot;
  }
}

# Elastic IP for the NAT gateway
resource &quot;aws_eip&quot; &quot;eipUSWTA&quot; {
  vpc = true

  tags = {
    Name = &quot;eipUSWTA&quot;
  }
}

# NAT gateway
resource &quot;aws_nat_gateway&quot; &quot;ngwUSWTA&quot; {
  allocation_id = aws_eip.eipUSWTA.id
  subnet_id     = aws_subnet.subPublic1.id

  tags = {
    Name = &quot;ngwUSWTA&quot;
  }
}

# Add route to Internet to main route table
resource &quot;aws_route&quot; &quot;rtMainRoute&quot; {
  route_table_id = aws_vpc.vpcUSWTA.main_route_table_id
  destination_cidr_block = &quot;0.0.0.0/0&quot;
  gateway_id = aws_nat_gateway.ngwUSWTA.id
}

# Create public route table
resource &quot;aws_route_table&quot; &quot;rtPublic&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id

  tags = {
    Name = &quot;rtPublic - USWTA&quot;
  }
}

# Add route to Internet to public route table
resource &quot;aws_route&quot; &quot;rtPublicRoute&quot; {
  route_table_id = aws_route_table.rtPublic.id
  destination_cidr_block = &quot;0.0.0.0/0&quot;
  gateway_id = aws_internet_gateway.igwUSWTA.id
}

# Associate public route table with public subnet 1
resource &quot;aws_route_table_association&quot; &quot;rtPubAssoc1&quot; {
  subnet_id   = aws_subnet.subPublic1.id
  route_table_id = aws_route_table.rtPublic.id
}

# Associate public route table with public subnet 2
resource &quot;aws_route_table_association&quot; &quot;rtPubAssoc2&quot; {
  subnet_id   = aws_subnet.subPublic2.id
  route_table_id = aws_route_table.rtPublic.id
}

# Application Load Balancer
resource &quot;aws_lb&quot; &quot;albUSWTA&quot; {
  name               = &quot;albUSWTA&quot;
  internal           = false
  load_balancer_type = &quot;application&quot;
  subnets            = &#x5B;aws_subnet.subPublic1.id, aws_subnet.subPublic2.id]
  security_groups = &#x5B;aws_security_group.sgUSWTAALB.id]

  tags = {
    Name = &quot;Application Load Balancer for USWTA&quot;
  }
}

# Target group
resource &quot;aws_lb_target_group&quot; &quot;tgUSWTA&quot; {
  name = &quot;tgUSWTA&quot;
  port = &quot;80&quot;
  protocol = &quot;HTTP&quot;
  vpc_id = aws_vpc.vpcUSWTA.id

  health_check {
    healthy_threshold   = 5
    unhealthy_threshold = 2
    timeout             = 5
    path                = &quot;/index.html&quot;
    port = 80
    matcher = &quot;200&quot;
    interval            = 30
  }
}

# Listener
resource &quot;aws_lb_listener&quot; &quot;lisUSWTA&quot; {
  load_balancer_arn = aws_lb.albUSWTA.arn
  port = &quot;80&quot;
  protocol = &quot;HTTP&quot;

  default_action {
    type = &quot;forward&quot;
    target_group_arn = aws_lb_target_group.tgUSWTA.arn
  }
}

# Add instance 1 to target group
resource &quot;aws_lb_target_group_attachment&quot; &quot;tgaUSWTA1&quot; {
  target_group_arn = aws_lb_target_group.tgUSWTA.arn
  target_id        = aws_instance.ec2USWTAApplication1.id
  port             = &quot;80&quot;
}

# Add instance 2 to target group
resource &quot;aws_lb_target_group_attachment&quot; &quot;tgaUSWTA2&quot; {
  target_group_arn = aws_lb_target_group.tgUSWTA.arn
  target_id        = aws_instance.ec2USWTAApplication2.id
  port             = &quot;80&quot;
}
</pre>
<p>The comments tell you what is being done. Terraform has a great documentation so if you google any of the resources created you&#8217;ll see ample documentation. So, make changes to suit your needs and once you have everything ready initialize the terraform so it downloads the necessary plugins.</p>
<pre class="brush: bash; title: ; notranslate">
terraform init
</pre>
<p>Pay special attention to lines <strong>68-72</strong>, it expects the keys to be the same exact filename and <strong>156-164</strong> it looks for an image that matches that name. That&#8217;s the AMI that we create with packer. Then create the infra. You have to type <strong>yes </strong>to proceed.</p>
<pre class="brush: bash; title: ; notranslate">
terraform apply
</pre>
<p>Once completed (in my case it took 4 minutes and 30 seconds), you&#8217;ll see the variables from the <strong>outputs.tf</strong>. </p>
<pre class="brush: bash; title: ; notranslate">
Apply complete! Resources: 25 added, 0 changed, 0 destroyed.

Outputs:

aws_app1_ip = 192.168.201.91
aws_app2_ip = 192.168.201.187
aws_ip = 54.145.191.102
aws_lb = albUSWTA-1695850209.us-east-1.elb.amazonaws.com
</pre>
<p>Go to <strong>http://albUSWTA-1695850209.us-east-1.elb.amazonaws.com</strong> which is the output from the LB from above and voila, your website is up and running on two HA servers.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P142-02.png"><img decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P142-02.png" alt="" width="1472" height="751" class="aligncenter size-full wp-image-6390" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P142-02.png 1472w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-02-300x153.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-02-1024x522.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-02-768x392.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-02-1170x597.png 1170w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-02-585x298.png 585w" sizes="(max-width: 1472px) 100vw, 1472px" /></a><br />
Go to your AWS console and you&#8217;ll see all of your resources there. e.g. the instances and the targets behind the load balancers.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P142-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P142-03.png" alt="" width="488" height="97" class="aligncenter size-full wp-image-6395" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P142-03.png 488w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-03-300x60.png 300w" sizes="(max-width: 488px) 100vw, 488px" /></a><a href="https://blog.andreev.it/wp-content/uploads/2019/12/P142-04.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P142-04.png" alt="" width="992" height="183" class="aligncenter size-full wp-image-6396" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P142-04.png 992w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-04-300x55.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-04-768x142.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-04-585x108.png 585w" sizes="(max-width: 992px) 100vw, 992px" /></a><br />
Once you are done playing, destroy the resources. </p>
<pre class="brush: bash; title: ; notranslate">
terraform destroy
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2019/12/aws-deploy-nginx-ha-cluster-with-packer-terraform-and-ansible/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AWS, WordPress: Deploy WordPress on AWS Linux using Terraform, EC2, RDS and EFS</title>
		<link>https://blog.andreev.it/2018/08/136-terraform-deploy-wordpress-on-aws-linux-using-terraform-ec2-rds-and-efs/</link>
					<comments>https://blog.andreev.it/2018/08/136-terraform-deploy-wordpress-on-aws-linux-using-terraform-ec2-rds-and-efs/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sat, 11 Aug 2018 16:18:35 +0000</pubDate>
				<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[EFS]]></category>
		<category><![CDATA[RDS]]></category>
		<category><![CDATA[Terraform]]></category>
		<guid isPermaLink="false">http://blog.iandreev.com/?p=4060</guid>

					<description><![CDATA[I&#8217;ve started playing with Terraform in order to automate some server builds and found&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>I&#8217;ve started playing with Terraform in order to automate some server builds and found it to be very useful. It has its own quirks, strange JSON-like syntax and it takes some time to get fully on-board. I&#8217;ve noticed several bugs but there is always a workaround if you google it. At the time of this writing, Terraform is version 0.11.7 so it&#8217;s still some kind of beta. The team behind it are making a lot of changes so expect some things to break. The book that I was using to study was already out of date and some commands were deprecated.<br />
I won&#8217;t explain how to install it and configure it. It&#8217;s very <a href="https://www.terraform.io/intro/getting-started/install.html" rel="noopener noreferrer" target="_blank">easy </a>to get it up and running. In order for the following script to work, make sure that you have Terraform and <a href="https://blog.andreev.it/?p=1905" rel="noopener noreferrer" target="_blank">AWS CLI</a> installed and configured.<br />
Create a separate folder/directory for this project and create three files. The first file is <strong>vars.tf</strong>. That&#8217;s where we define the variables. </p>
<pre class="brush: bash; title: ; notranslate">
variable &quot;region&quot; {
	description = &quot;The region for the deployment&quot;
	default = &quot;us-west-2&quot;
}
	
variable &quot;vpc_id&quot; {
	description = &quot;The VPC ID where WordPress will reside&quot;
	default = &quot;vpc-a36ga9zd&quot;
}

variable &quot;ami_id&quot; {
	description = &quot;The AMI ID for AWS Linux 2 in us-west-2. In other regions, the ID is different&quot;
	default = &quot;ami-a9d09ed1&quot;

}

variable &quot;instance_type&quot; {
	description = &quot;AWS Instance type to be used for the WordPress instance&quot;
	default = &quot;t2.small&quot;
}

variable &quot;volume_size&quot; {
	description = &quot;EBS volume size in GBs for the instance&quot;
	default = 8
}

variable &quot;key_name&quot; {
	description = &quot;The key pair that will be used to log to the server using SSH&quot;
	default = &quot;MyKeyPair-Oregon&quot;
}

variable &quot;ssh_port&quot; {
	description = &quot;The SSH port for the server&quot;
	default = 22
}

variable &quot;http_port&quot; {
	description = &quot;The HTTP port for the server&quot;
	default = 80
}

variable &quot;mysql_port&quot; {
	description = &quot;The MySQL port for the database&quot;
	default = 3306
}

variable &quot;nfs_port&quot; {
	description = &quot;The NFS port for the shared filesystem&quot;
	default = 2049
}

variable &quot;allocated_storage&quot; {
	description = &quot;The size in GBs of the SQL database&quot;
	default = 10
}

variable &quot;instance_class&quot; {
	description = &quot;The size/type of the SQL instance&quot;
	default = &quot;db.t2.micro&quot;
}

variable &quot;db_admin&quot; {
	description = &quot;The dbadmin username&quot;
	default = &quot;dbadmin&quot;
}

variable &quot;db_password&quot; {
	description = &quot;The dbadmin password&quot;
	default = &quot;SuperSecret&quot;
}

variable &quot;db_name&quot; {
	description = &quot;The database name&quot;
	default = &quot;dbwordpress&quot;
}
</pre>
<p>Make sure you specify your region, your VPC ID and the key name. That&#8217;s how you will login to your instance. You can find your key name if you go to <strong>Key Pairs</strong> menu in AWS console.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2018/08/P113-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2018/08/P113-01.png" alt="" width="544" height="227" class="aligncenter size-full wp-image-8274" /></a><br />
I tried to test the same setup on CentOS 7.x but for some reason the user data boot script was never executed, thus making this script unusable. It might work with Ubuntu or some other flavor of Linux, if you can make the user data script execute on boot and make some modifications, e.g. apt-get instead of yum etc&#8230;<br />
The next file is <strong>outputs.tf</strong>. These are the variables that are needed at the end of the run of the script. BTW, the script takes around 4 minutes to complete. </p>
<pre class="brush: bash; title: ; notranslate">
output &quot;efs_dns_name&quot; {
	value = &quot;${aws_efs_file_system.efsWordPress.dns_name}&quot;
}

output &quot;ip_address&quot; {
    value = &quot;${aws_instance.wordpress.public_ip}&quot;
}

output &quot;sql_hostname&quot; {
    value = &quot;${aws_db_instance.dbWordPress.address}&quot;
}
</pre>
<p>At the end of the script, we&#8217;ll get the DNS name of the EFS filesystem, the external IP address of the instance and the MySQL hostname. We&#8217;ll need the last two.<br />
And finally, the main script (<strong>main.tf</strong>) is where all the resources that are needed are specified.</p>
<pre class="brush: bash; highlight: [13,61,62,63,64,65,66,68,83,87]; title: ; notranslate">
provider &quot;aws&quot; {
	region = &quot;${var.region}&quot;
}

resource &quot;aws_security_group&quot; &quot;sgWordPress&quot; {
	name = &quot;sgWordPress&quot;
	vpc_id      = &quot;${var.vpc_id}&quot;

	ingress {
		from_port = &quot;${var.ssh_port}&quot;
		to_port = &quot;${var.ssh_port}&quot;
		protocol = &quot;tcp&quot;
		cidr_blocks =&#x5B;&quot;0.0.0.0/0&quot;]
	}

	ingress {
		from_port = &quot;${var.http_port}&quot;
		to_port = &quot;${var.http_port}&quot;
		protocol = &quot;tcp&quot;
		cidr_blocks = &#x5B;&quot;0.0.0.0/0&quot;]
	}

	ingress {
		from_port = &quot;${var.mysql_port}&quot;
		to_port = &quot;${var.mysql_port}&quot;
		protocol = &quot;tcp&quot;
		self = true
	}

	ingress {
		from_port = &quot;${var.nfs_port}&quot;
		to_port = &quot;${var.nfs_port}&quot;
		protocol = &quot;tcp&quot;
		self = true
	}

	egress {
		from_port = 0
		to_port = 0
		protocol = &quot;-1&quot;
		cidr_blocks = &#x5B;&quot;0.0.0.0/0&quot;]
	}

	tags {
		Name = &quot;sgWordPress&quot;
	}
}

resource &quot;aws_efs_file_system&quot; &quot;efsWordPress&quot; {
  creation_token = &quot;EFS for WordPress&quot;

  tags {
    Name = &quot;EFS for WordPress&quot;
  }
}

data &quot;aws_subnet_ids&quot; &quot;suballIDs&quot; {
	vpc_id = &quot;${var.vpc_id}&quot;
}

resource &quot;aws_efs_mount_target&quot; &quot;mtWordPress&quot; {
  count = &quot;${length(data.aws_subnet_ids.suballIDs.ids)}&quot;
  file_system_id = &quot;${aws_efs_file_system.efsWordPress.id}&quot;
  subnet_id      = &quot;${element(data.aws_subnet_ids.suballIDs.ids, count.index)}&quot;
  security_groups = &#x5B;&quot;${aws_security_group.sgWordPress.id}&quot;]
}

resource &quot;aws_instance&quot; &quot;wordpress&quot; {
	ami = &quot;${var.ami_id}&quot;
	instance_type = &quot;${var.instance_type}&quot;
	vpc_security_group_ids = &#x5B;&quot;${aws_security_group.sgWordPress.id}&quot;]
	key_name = &quot;${var.key_name}&quot; 
	ebs_block_device {
		device_name = &quot;/dev/sdb&quot;
    	volume_size = &quot;${var.volume_size}&quot;
    	delete_on_termination = &quot;true&quot;
  	}

	tags {
		Name = &quot;WordPress Server&quot;
	}

	user_data = &lt;&lt;EOF
		#!/bin/bash
		echo &quot;${aws_efs_file_system.efsWordPress.dns_name}:/ /var/www/html nfs defaults,vers=4.1 0 0&quot; &gt;&gt; /etc/fstab
		yum install -y php php-dom php-gd php-mysql
		for z in {0..120}; do
			echo -n .
			host &quot;${aws_efs_file_system.efsWordPress.dns_name}&quot; &amp;&amp; break
		  	sleep 1
		done
		cd /tmp
		wget https://www.wordpress.org/latest.tar.gz
		mount -a
		tar xzvf /tmp/latest.tar.gz --strip 1 -C /var/www/html
		rm /tmp/latest.tar.gz
		chown -R apache:apache /var/www/html
		systemctl enable httpd
		sed -i 's/#ServerName www.example.com:80/ServerName www.myblog.com:80/' /etc/httpd/conf/httpd.conf
		sed -i 's/ServerAdmin root@localhost/ServerAdmin admin@myblog.com/' /etc/httpd/conf/httpd.conf
		#setsebool -P httpd_can_network_connect 1
		#setsebool -P httpd_can_network_connect_db 1
		systemctl start httpd
		#firewall-cmd --zone=public --permanent --add-service=http
		#firewall-cmd --reload
		#iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
		#iptables -A OUTPUT -p tcp --sport 80 -m conntrack --ctstate ESTABLISHED -j ACCEPT
	EOF

}

resource &quot;aws_db_instance&quot; &quot;dbWordPress&quot; {
	identifier = &quot;dbwordpress&quot;
	engine = &quot;mysql&quot;
	engine_version = &quot;5.7&quot;
	allocated_storage = &quot;${var.allocated_storage}&quot;
	instance_class = &quot;${var.instance_class}&quot;
	vpc_security_group_ids = &#x5B;&quot;${aws_security_group.sgWordPress.id}&quot;]
	name = &quot;${var.db_name}&quot;
	username = &quot;${var.db_admin}&quot;
	password = &quot;${var.db_password}&quot;
	parameter_group_name = &quot;default.mysql5.7&quot;
	skip_final_snapshot = true
	tags {
		Name = &quot;WordPress DB&quot;
	}
}
</pre>
<p>The script starts with the name of the provided that will be used (AWS), then a security group is being defined with ports 22 and 80 open to the world. Opening port 22 to the world is not a good idea, so you can restrict the access. In line 13, you can change the subnets allowed, e.g. instead of [&#8220;0.0.0.0/0&#8221;], you can restrict the SSH to a couple of IPs, e.g. [&#8220;1.2.3.4/32&#8243;,&#8221;12.13.14.15/32&#8221;]. Then, we define the ports for NFS and MySQL. If you can see these ports are open to themselves only. That means that only the resources having assigned that security group can talk between themselves on these two ports. And finally, we allow all outgoing traffic.<br />
Further, we have an EFS file system created and mount targets. The mounts targets define in what availability zones the EFS system will be available. Since each region has different numbers of availability zones, we don&#8217;t know that number, so we have to enumerate the AZs for each region. If you look at lines 61 to 66 you&#8217;ll see how we do that. It&#8217;s pretty much a <a href="https://blog.gruntwork.io/terraform-tips-tricks-loops-if-statements-and-gotchas-f739bbae55f9" rel="noopener noreferrer" target="_blank">for..next loop</a> in Terraform.<br />
At line 68, we have our main resource defined, the instance. You&#8217;ll see that each resource has a lot of input parameters. Some of these are mandatory and some are optional. For each resource, you can find a detailed explanation on the Terraform site. For example, if you google <a href="https://www.google.com/search?q=terraform+aws_instance" rel="noopener noreferrer" target="_blank">&#8220;terraform aws_instance&#8221;</a> the first link that shows up on terraform.io website will be about that.<br />
At line 83 the custom script begins. It will add the EFS mount point to <strong>/etc/fstab</strong> so the NFS system is mounted on each reboot. At line 87 there is a short delay until EFS filesystem becomes available. Because the resources are created in parallel, it takes up to 90 seconds for the EFS DNS name to get propagated. If we don&#8217;t have the delay, the instance will boot up, try to mount the EFS system and fail, because the DNS mount point won&#8217;t be accessible. AWS Linux 2 comes with SELinux disabled and no firewalls, but if you have some other instance, you can uncomment the lines at the end of the user data script. Otherwise, SELinux will prevent Apache and MySQL to work with PHP. Lines 106 and 107 are if you use <strong>iptables </strong>instead of <strong>firewalld</strong>. Tailor to your needs. Finally, at line 112, the MySQL DB is defined.<br />
Once you have all these three files <strong>(vars.tf, outputs.tf and main.tf)</strong> modified and saved in the same folder/directory, you can star the provisioning. First, initialize the script so Terraform can download the plugin for AWS.</p>
<pre class="brush: bash; title: ; notranslate">
terraform init
</pre>
<p>Then, check if everything is OK with the script.</p>
<pre class="brush: bash; title: ; notranslate">
terraform plan
</pre>
<p>And finally, execute the script.</p>
<pre class="brush: bash; title: ; notranslate">
terraform apply
</pre>
<p>It will prompt you to say &#8220;<strong>yes</strong>&#8221; and after the script completes, you&#8217;ll have something like this at the end.</p>
<pre class="brush: bash; title: ; notranslate">
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

Outputs:

efs_dns_name = fs-64307acd.efs.us-west-2.amazonaws.com
ip_address = 34.220.235.222
sql_hostname = dbwordpress.cbh9gck8kp6s.us-west-2.rds.amazonaws.com
</pre>
<p>Go to <strong>http://your_ip</strong> and you should have the welcome WordPress screen. Use the database name, username and the password for the database defined in <strong>vars.tf</strong> (dbwordpress, dbadmin, SuperSecret) and for the hostname use the SQL DB output variable (<strong>sql_hostname</strong>) above.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2018/08/P113-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2018/08/P113-02.png" alt="" width="732" height="409" class="aligncenter size-full wp-image-8275" /></a><br />
And that&#8217;s it! You have your WordPress up and running on RDS and EFS filesystem without even logging to AWS console or your Linux instance.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2018/08/P113-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2018/08/P113-03.png" alt="" width="931" height="559" class="aligncenter size-full wp-image-8276" /></a><br />
Once you are done playing you can destroy all of the resources with one command.</p>
<pre class="brush: bash; title: ; notranslate">
terraform destroy
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2018/08/136-terraform-deploy-wordpress-on-aws-linux-using-terraform-ec2-rds-and-efs/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
