<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Sarah Aligbe on Medium]]></title>
        <description><![CDATA[Stories by Sarah Aligbe on Medium]]></description>
        <link>https://medium.com/@sarahligbe12?source=rss-5caac2e3c7e3------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*wrs6WfS0vBhnkjaw.jpg</url>
            <title>Stories by Sarah Aligbe on Medium</title>
            <link>https://medium.com/@sarahligbe12?source=rss-5caac2e3c7e3------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 11:04:30 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@sarahligbe12/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Automating Infrastructure Deployment and Configuration Management with Terraform and Ansible on AWS.]]></title>
            <link>https://medium.com/@sarahligbe12/automating-infrastructure-deployment-and-configuration-management-with-terraform-and-ansible-on-aws-c98b8cee77f?source=rss-5caac2e3c7e3------2</link>
            <guid isPermaLink="false">https://medium.com/p/c98b8cee77f</guid>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[ansible]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[devops]]></category>
            <dc:creator><![CDATA[Sarah Aligbe]]></dc:creator>
            <pubDate>Wed, 01 Feb 2023 02:55:21 GMT</pubDate>
            <atom:updated>2023-02-01T10:22:32.618Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/794/1*jYYc2_ELGkW4KMAJvFf11Q.png" /></figure><h3>Introduction</h3><p>Welcome to this step-by-step guide on how to automate the deployment and management of a web server infrastructure on AWS using Terraform and Ansible. In this project, you will:</p><ul><li>Learn to use Terraform to create a VPC and three EC2 instances and put them behind an Application Load Balancer</li><li>Export the public IP addresses of the instances to a file called host-inventory</li><li>Learn how to use AWS Route53 to set up a domain name and an A record for a subdomain that points to the ALB IP address</li><li>Use Ansible to automate the configuration of the servers, including: Installing Apache, setting the time zone to Africa/Lagos, and displaying a simple HTML page that can be used to identify the servers.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/841/1*T70OjNQ56odVD0GgeXJMUA.png" /><figcaption>AWS Architecture</figcaption></figure><p>This project is designed for beginners who are familiar with the basic concepts of AWS and have some experience creating resources via the AWS console, and Ansible. If you are new to Terraform, this project will be a great starting point for you to learn how to use these automation tools to deploy and manage your web server infrastructure on AWS.</p><h4>Pre-requisites</h4><ol><li>An AWS account with an IAM user with:</li></ol><ul><li>AmazonVPCFullAccess</li><li>AmazonEC2FullAccess</li><li>AmazonRoute53FullAccess</li></ul><p>The Access Key and Secret Access Key are required</p><p>2. <a href="https://developer.hashicorp.com/terraform/downloads?product_intent=terraform">Terraform</a> installed.</p><p>3. <a href="https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-ansible-on-ubuntu-20-04">Ansible</a> installed.</p><p>4. A Linux OS (I used Ubuntu 20.04); Ansible does not run on Windows. You can use an EC2 instance, Vagrant, or a Mac.</p><p>5. A Name.com account; This is not necessary, but it is needed if you want to automate the process of adding the nameservers generated by Route53 to your Name.com domain (if this is your preferred domain provider).</p><h4>What is Terraform?</h4><p>Terraform is an open-source infrastructure as code software tool that enables users to provision and manage infrastructure resources. It is platform-agnostic and can be used to provision resources on various cloud providers, such as AWS, Azure, and GCP. Terraform’s configuration files are written in HashiCorp Configuration Language (HCL), which is a simple and easy-to-learn language. Terraform is declarative, meaning that you describe the desired state of your infrastructure, and the tool figures out how to reach that state. You don’t need to specify the step-by-step process, Terraform handles that for you.</p><h4>What is Ansible?</h4><p>Ansible is an open-source automation tool that enables users to automate the configuration, deployment, and management of servers. It is agentless and uses a simple, human-readable language to define automation tasks. Ansible uses YAML (YAML Ain’t Markup Language) to write its playbooks, which is also easy to understand.</p><p>By using Terraform to provision the infrastructure resources and Ansible to automate the configuration of the servers, you will be able to achieve a high level of automation and consistency in your web server infrastructure deployment and management.</p><h3>Automating Your Infrastructure</h3><p>In this section of the article, we will delve into the process of automating the infrastructure for our web server. This includes creating a VPC, setting up EC2 instances, configuring an Application Load Balancer (ALB), and setting up a subdomain for the resources in Route53. While all of these configurations can be done in one <strong>main.tf</strong> file, it is better to separate them for better organisation and maintainability. Terraform is smart enough to understand how the resources are related, so by separating the configurations into different files, we can make it easier to manage and understand the infrastructure.</p><p>Additionally, we will be using a <strong>variables.tf</strong> file to define variables that will be used across the configurations and a <strong>terraform.tfvars</strong> file to store the actual values of the variables. This allows for better management of sensitive information and flexibility in changing variable values across different environments. By the end of this section, you will have a fully automated and configured infrastructure.</p><p>Before we begin, you need to add your AWS Access Key and Secret Key as environment variables in your terminal. This is because when using Terraform to manage AWS resources, it needs your AWS access key and secret key to communicate with the AWS API. These keys are used to authenticate and authorise Terraform to access your AWS account. To keep sensitive information separate, it’s best to add your keys as environment variables instead of hardcoding them in Terraform configuration files or scripts. This way, you can safely store your Terraform code in a version control system without risking exposing sensitive information.</p><p>To do this, run the following commands:</p><pre>export AWS_ACCESS_KEY_ID=&quot;youraccesskey&quot;<br>export AWS_SECRET_ACCESS_KEY=&quot;yoursecretkey&quot;</pre><h4>Step 1: Create the providers.tf file</h4><p>In Terraform, providers are plugins that interact with specific services or platforms. They are defined in the Terraform configuration files and are necessary for Terraform to create, update, and delete resources. A providers.tf file is used to organise and manage these providers. It allows you to specify the provider name, version, and any required authentication or configuration settings. It also makes it easy to maintain consistency across different environments and parts of the codebase.</p><p>Note: You can also include provider configurations in the main.tf file, but using a separate providers.tf file is recommended for better organisation.</p><pre>terraform {<br>  required_providers {<br>    aws = {<br>      source  = &quot;hashicorp/aws&quot;<br>      version = &quot;~&gt; 4.0&quot;<br>    }<br>  }<br>}<br><br>provider &quot;aws&quot; {<br>  region = var.region<br>}</pre><p>Here, we’ve added AWS to the required providers block, which includes the source and version of the provider, in this case AWS versions 4.0 to 4.9.</p><p>We’ve also included an “aws” provider block, which specifies the authentication credentials to access the AWS API and the region in which Terraform should provision our infrastructure. Terraform pulls the access key and secret key from the environment variable we defined earlier, the region is defined in our variables.tf file.</p><h4>Step 2: Create the vpc.tf file</h4><p>A Virtual Private Cloud (VPC) is a virtual network that is dedicated to your AWS account. It enables you to launch AWS resources in a virtual network that you’ve defined. With a VPC, you can define your own network space, and control how your network and the Amazon EC2 resources inside it are exposed to the Internet.</p><p>In the <strong>vpc.tf </strong>file, you will create:</p><ul><li>A VPC</li><li>Three subnets</li><li>An Internet Gateway (IGW)</li><li>A Route table with the subnets associated</li><li>Two Security groups — one for the Load balancer and the other for the instances</li></ul><p>Firstly, you will create the<strong>variables.tf</strong> file and define the variables to be used in the <strong>vpc.tf</strong> file.</p><pre><br>variable &quot;region&quot; {<br>  description = &quot;AWS region&quot;<br>  type = string<br>  default = &quot;us-east-1&quot;<br>}<br><br>variable &quot;vpc_block&quot; {<br>  description = &quot;cidr block and name tag for the vpc&quot;<br>  type = object({<br>    cidr_block = string<br>    name = string<br>  })<br>}<br><br>variable &quot;subnet_blocks&quot; {<br>  description = &quot;cidr, availability zone and name tag for the subnets&quot;<br>  type = map(object({<br>    cidr = string<br>    az = string<br>  }))<br>}<br><br>variable &quot;igw&quot; {<br>  description = &quot;internet gateway name&quot;<br>  type = string<br>}<br><br>variable &quot;rtb&quot; {<br>  description = &quot;cidr block and name tag for route table&quot;<br>  type = object({<br>    cidr_block = string<br>    name = string<br>  })<br>}<br><br>variable &quot;sg&quot; {<br>  description = &quot;Application load balancer and instances security group name&quot;<br>  type = list(string)<br>}<br><br>variable &quot;inbound_ports&quot; {<br>  description = &quot;Security groups inbound ports&quot;<br>  type = list(number)<br>  default = [80,443]<br>}</pre><p>The variables.tf file contains variable blocks. Each variable block has the following:</p><ul><li>A variable name in quotes</li><li>A “type” keyword that specifies the type of the variable. It could either be string, number, bool, list, map, tuple or object</li><li>A “description” keyword that describes what the variable represents. It is good practice to always add a description in the variable block</li><li>A “default” keyword which specifies a default value for the variable. If this is not provided the value of the variable ia assigned from the <strong>terraform.tfvars</strong> file or can be passed when you run <strong>terraform plan </strong>or<strong> </strong><strong>terraform apply </strong>.</li></ul><p>Secondly, you will assign values to the defined variables in the <strong>terraform.tfvars</strong> file</p><pre>vpc_block = {cidr_block = &quot;172.17.0.0/16&quot;, name = &quot;project_vpc&quot;}<br>subnet_blocks = {<br>  subnet_01 = {<br>    cidr = &quot;172.17.1.0/24&quot;<br>    az = &quot;us-east-1a&quot;<br>  }<br>  subnet_02 = {<br>    cidr = &quot;172.17.2.0/24&quot;<br>    az = &quot;us-east-1b&quot;<br>  }<br>  subnet_03 = {<br>    cidr = &quot;172.17.3.0/24&quot;<br>    az = &quot;us-east-1c&quot;<br>  }<br>}<br>igw = &quot;project_igw&quot;<br>rtb = {cidr_block = &quot;0.0.0.0/0&quot;, name = &quot;project_rtb&quot;}<br>sg = [&quot;project_alb_sg&quot;, &quot;project_instances_sg&quot;]</pre><p>The file is in a key-value format, where key is the variable name, which is the same as the one defined in the <strong>variables.tf</strong> file, and value is the desired value for that variable according to the type defined in the <strong>variables.tf</strong> file.</p><p>Finally, the <strong>vpc.tf</strong> file</p><pre>#vpc.tf<br><br>resource &quot;aws_vpc&quot; &quot;main&quot; {<br>  cidr_block            = var.vpc_block.cidr_block<br>  enable_dns_hostnames  = true<br>  tags = {<br>    Name                = var.vpc_block.name<br>  }<br>}<br><br>resource &quot;aws_subnet&quot; &quot;subnets&quot; {<br>  for_each              = var.subnet_blocks<br><br>  vpc_id                = aws_vpc.main.id<br>  cidr_block            = each.value[&quot;cidr&quot;]<br>  availability_zone     = each.value[&quot;az&quot;]<br><br>  tags = {<br>    Name                = each.key<br>  }<br>}<br><br>resource &quot;aws_internet_gateway&quot; &quot;main&quot; {<br>  vpc_id                = aws_vpc.main.id<br><br>  tags = {<br>    Name                = var.igw<br>  }<br>}<br><br>resource &quot;aws_route_table&quot; &quot;main&quot; {<br>  vpc_id                = aws_vpc.main.id<br><br>  route {<br>    cidr_block          = var.rtb.cidr_block<br>    gateway_id          = aws_internet_gateway.main.id<br>  }<br><br>  tags = {<br>    Name        = var.rtb.name<br>  }<br>}<br><br>resource &quot;aws_route_table_association&quot; &quot;main&quot; {<br>  for_each              = aws_subnet.subnets<br><br>  subnet_id             = each.value.id<br>  route_table_id        = aws_route_table.main.id<br>}<br><br>resource &quot;aws_security_group&quot; &quot;alb_sg&quot; {<br>  name                  = var.sg[0]<br>  description           = &quot;Allow HTTPS and HTTP inbound traffic for application load balancer&quot;<br>  vpc_id                = aws_vpc.main.id<br><br>  dynamic &quot;ingress&quot; {<br>    for_each            = var.inbound_ports<br><br>    content {<br>      from_port         = ingress.value<br>      to_port           = ingress.value<br>      protocol          = &quot;tcp&quot;<br>      cidr_blocks       = [&quot;0.0.0.0/0&quot;]<br>    }<br>  }<br><br>  egress {<br>    from_port        = 0<br>    to_port          = 0<br>    protocol         = &quot;-1&quot;<br>    cidr_blocks      = [&quot;0.0.0.0/0&quot;]<br>  }<br>}<br><br>resource &quot;aws_security_group&quot; &quot;instance_sg&quot; {<br>  name                  = var.sg[1]<br>  description           = &quot;Allow HTTPS and HTTP inbound traffic for the instances&quot;<br>  vpc_id                = aws_vpc.main.id<br><br>  dynamic &quot;ingress&quot; {<br>    for_each            = var.inbound_ports<br><br>    content {<br>      from_port         = ingress.value<br>      to_port           = ingress.value<br>      protocol          = &quot;tcp&quot;<br>      security_groups   = [aws_security_group.alb_sg.id]<br>    }<br>  }<br><br>  ingress {<br>    description         = &quot;Allow SSH&quot;<br>    from_port           = 22<br>    to_port             = 22<br>    protocol            = &quot;tcp&quot;<br>    cidr_blocks         = [&quot;0.0.0.0/0&quot;]<br>  }<br><br>  egress {<br>    from_port        = 0<br>    to_port          = 0<br>    protocol         = &quot;-1&quot;<br>    cidr_blocks      = [&quot;0.0.0.0/0&quot;]<br>  }<br>}</pre><p>A resource block describes a specific resource that you want to create, update, or delete. It defines the provider that is responsible for managing the resource with the type of resource (e.g., <strong>aws_vpc</strong> ), the name of the resource (e.g., “main”), and any configuration options that are specific to that resource (e.g., all the configurations between the curly braces).</p><p>The <strong>for_each</strong> keyword in the <strong>aws_subnet</strong> and <strong>aws_route_table_association</strong> resources is a special argument that allows you to create multiple resources of the same type. It is useful when you need to create multiple resources that are similar but have different properties. The <strong>for_each</strong> argument takes a list or map data structure, and Terraform will create a new resource for each element in the list or map. For example, in the case of the <strong>aws_subnet</strong> resource, we needed to create three subnets with different CIDR blocks and availability zones. To accomplish this, we defined a variable in the <strong>variables.tf</strong> file with a type of map(object) and the properties for each subnet were assigned in the <strong>terraform.tfvars</strong> file. We then used the <strong>for_each</strong> argument to loop through the map and create the three subnets with the defined properties. Similarly, in the <strong>aws_route_table_association</strong> resource, we used the <strong>for_each</strong> argument to loop through the subnets created and extract the subnet ID for each one, in order to associate them with the route table.</p><p>You can reference an attribute in a resource that has not been created yet by using the resource type, name, and attribute. For example, to reference the ID of the VPC in the <strong>aws_subnet</strong> resource block, use “<strong>aws_vpc.main.id</strong>”. This links the ID of the VPC to the subnet resource after it is created.</p><p>In the <strong>aws_security_group</strong> resource block, we use a <a href="https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks"><strong>dynamic block </strong></a>to create multiple ingress rules that allow traffic on ports 80 and 443. This allows us to efficiently create multiple ingress rules with the same configuration, rather than defining each rule individually. The dynamic block functions similarly to the<strong>for_each</strong> feature, which can be used to create multiple resources with the same configuration.</p><blockquote>You can use ingress rules to allow incoming traffic on specific ports or from specific IP addresses, and use egress rules to allow outgoing traffic on specific ports or to specific IP addresses.</blockquote><h4>Step 3: Create the instances.tf file</h4><p>An EC2 instance is a virtual server in Amazon Web Services’ Elastic Compute Cloud (EC2) for running applications on the Amazon Web Services infrastructure. EC2 instances can be launched with various configurations of CPU, memory, storage, and networking capacity and can be running different operating systems. You can configure security and networking, and you can also launch multiple instances to create a scalable and fault-tolerant application.</p><p>In this file, you will do the following:</p><ul><li>Retrieve the latest Ubuntu20.04 AMI(Amazon Machine Image) from AWS in order to create the EC2 instances</li><li>Create an SSH key-pair in order for Ansible to connect to the instances and configure the webserver</li><li>Save the private key generated in the project directory</li><li>Create three EC2 instances and export their public IP addresses to a file called host-inventory</li><li>Execute the ansible playbook to configure the servers</li></ul><p>Firstly, you’ll update the <strong>variables.tf</strong> file to define variables for the <strong>instances.tf</strong> file</p><pre>#variables.tf<br><br>variable &quot;ssh_key&quot; {<br>  description = &quot;SSH Key name&quot;<br>  type = string<br>}</pre><p>Secondly, you’ll update the <strong>terraform.tfvars</strong> file to assign values to the defined variables</p><pre>#terraform.tfvars<br><br>ssh_key = &quot;project_key&quot;</pre><p>Thirdly, the instances.tf file</p><pre>#instances.tf<br><br>data &quot;aws_ami&quot; &quot;latest_ubuntu_image&quot; {<br>  most_recent = true<br>  owners = [&quot;amazon&quot;]<br><br>  filter {<br>    name = &quot;name&quot;<br>    values = [&quot;ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*&quot;]<br>  }<br><br>  filter {<br>    name = &quot;virtualization-type&quot;<br>    values = [&quot;hvm&quot;]<br>  }<br>}<br><br>resource &quot;tls_private_key&quot; &quot;main&quot; {<br>  algorithm = &quot;RSA&quot;<br>  rsa_bits = 4096<br>}<br><br>resource &quot;aws_key_pair&quot; &quot;generated_key&quot; {<br>  key_name = var.ssh_key<br>  public_key = tls_private_key.main.public_key_openssh<br>}<br><br>resource &quot;local_file&quot; &quot;ssh_key&quot; {<br>  content = tls_private_key.main.private_key_pem<br>  filename = &quot;${var.ssh_key}.pem&quot;<br>  file_permission = &quot;0400&quot;<br>}<br><br>resource &quot;aws_instance&quot; &quot;main&quot; {<br>  for_each                     = aws_subnet.subnets<br><br>  ami                           = data.aws_ami.latest_ubuntu_image.id<br>  instance_type                 = &quot;t2.micro&quot;<br>  subnet_id                     = each.value.id<br>  vpc_security_group_ids        = [aws_security_group.instance_sg.id]<br>  associate_public_ip_address   = true<br>  key_name                      = var.ssh_key<br><br>  provisioner &quot;local-exec&quot; {<br>    command = &quot;echo &#39;${self.public_ip}&#39; &gt;&gt; ./host-inventory&quot;<br>  }<br>}<br><br>resource &quot;null_resource&quot; &quot;ansible-playbook&quot; {<br>  provisioner &quot;local-exec&quot; {<br>    command = &quot;ansible-playbook --private-key ${var.ssh_key}.pem main.yml&quot;<br>  }<br><br>  depends_on = [aws_instance.main]<br>} </pre><p>Data sources access existing infrastructure and retrieve information about it, such as the Amazon Machine Image (AMI). This information can be used in Terraform configuration files to make decisions about how to manage that infrastructure. An AMI includes the necessary information to launch an instance, including the operating system, application server, and application. It’s considered good practice to source the AMI directly from AWS because it ensures that the image is the latest version and optimized for the region in which you’re launching the instance. This is particularly important because AMI IDs can change over time and are different in each region. Data sources are defined using the <strong>data</strong> block. Data blocks follow the same syntax as resource blocks.</p><p>We’ll start by using the data block to retrieve information about the AMI.</p><p>Next, we’ll use the <strong>tls_private_key</strong> resource to generate a secure private key in PEM and OPENSSH PEM formats. You can use this private key for SSH connections. SSH keys come in pairs, with a public key used to encrypt data sent to the server and a private key used to decrypt data received from the server. The private key is typically kept on the client side, while the public key is uploaded to the server. The <strong>aws_key_pair</strong> resource uploads the public key, generated with the <strong>tls_private_key</strong> resource, to enable logging into the instances created. The <strong>local_file</strong> resource stores the private key generated on the local machine.</p><blockquote>Terraform does not encourage using tls_private_key in a production environment because the SSH key is stored as plain text in the state file and that can pose a security risk. However, you can use it in this case. Do not commit your state file to version control.</blockquote><p>You’ll use the <strong>aws_instance</strong> resource to create three instances in each of the three subnets that you created earlier using the <strong>for_each</strong> loop. To execute a command on the local machine where you are running Terraform, use the <strong>local_exec</strong> provisioner. This provisioner is typically used within a resource block and can only function within the context of a resource. This means that the command will be executed after the resource is created or updated. You can use the provisioner to export the public IP of the instances you created to the host-inventory file. The <strong>self</strong> variable allows you to reference the current resource and its attributes within the same resource block. The attribute in this case is the public IP of each of the instances you created.</p><blockquote>The host-inventory file lists the hosts, their IP addresses or hostnames, and their associated groups to be managed by Ansible</blockquote><p>The <strong>null_resource</strong> block is a resource type that does not create any actual underlying resources. It is used to trigger provisioners and perform tasks. It uses the <strong>local_exec</strong> provisioner to run the Ansible playbook created in <strong>main.yml</strong> on the local machine. <strong>depends_on</strong> is a property that is used to specify dependencies between resources in Terraform. This means that Terraform will make sure that the <strong>aws_instance.main</strong> resource is created or updated before the “ansible-playbook” resource is executed.</p><p>Next, you’ll have to create the <strong>main.yml</strong> file referenced in the <strong>null_resource</strong> block in order to configure the servers (instances) created.</p><pre>#main.yml<br><br>---<br><br>- hosts: all<br>  tasks:<br><br>  - name: update cache<br>    apt:<br>      update_cache: yes<br><br>  - name: Install Apache2    <br>    apt:<br>      name: apache2<br>      state: present<br><br>  - name: Set timezone to Africa/Lagos<br>    timezone:<br>      name: Africa/Lagos<br><br>  - name: Display hostname<br>    shell: echo &quot;&lt;h1&gt;Hello world!! This is $(hostname -f) and the date is $(date)&lt;/h1&gt;&quot; &gt; /var/www/html/index.html<br>    notify: restart_apache<br><br>  handlers:<br>  - name: restart_apache<br>    systemd:<br>      name: apache2<br>      state: restarted</pre><p>You’ll also need to create the<strong>ansible.cfg</strong> file that contains settings for Ansible, such as the location of the Ansible configuration directory, the location of the inventory file, and the default user when connecting to remote hosts.</p><pre>#ansible.cfg<br><br>[defaults]<br>inventory=host-inventory<br>remote_user=ubuntu<br>host_key_checking=false<br><br>[privilege_escalation]<br>become=yes<br>become_method=sudo</pre><h4>Step 4: Create the load_balancer.tf file</h4><p>A Load balancer is a service that distributes incoming network traffic across multiple EC2 instances, ensuring that no single instance is overwhelmed with too much traffic. This improves the overall performance and availability of your application. Target groups are used in conjunction with a load balancer to distribute incoming traffic across multiple resources. When you create a target group, you specify the protocol and port number for the traffic, and then you register the resources that you want to receive traffic. The load balancer then uses the target group to route incoming traffic to the registered resources.</p><p>In this file, you will do the following:</p><ul><li>Create the load balancer</li><li>Create the target group</li><li>Attach the instances to the target group</li><li>Create an HTTP listener in your load balancer.</li></ul><p>Firstly, update the <strong>variables.tf</strong> file to define variables for the <strong>load_balancer.tf</strong> file</p><pre>#variables.tf<br><br>variable &quot;lb_name&quot; {<br>  description = &quot;Load balancer name&quot;<br>  type = string<br>}<br><br>variable &quot;tg_name&quot; {<br>  description = &quot;Target group name&quot;<br>  type = string<br>}</pre><p>Next, update the <strong>terraform.tfvars</strong> file to assign values to the defined variables</p><pre>#terraform.tfvars<br><br>lb_name = &quot;project-alb&quot;<br>tg_name = &quot;project-tg&quot;</pre><p>Next, create the <strong>load_balancer.tf</strong> file</p><pre>#load_balancer.tf<br><br>resource &quot;aws_lb&quot; &quot;main&quot; {<br>  name = var.lb_name<br>  internal = false<br>  load_balancer_type = &quot;application&quot;<br>  security_groups = [aws_security_group.alb_sg.id]<br>  subnets = [for subnet in aws_subnet.subnets : subnet.id]<br>}<br><br>resource &quot;aws_lb_target_group&quot; &quot;main&quot; {<br>  name = var.tg_name<br>  port = 80<br>  protocol = &quot;HTTP&quot;<br>  vpc_id = aws_vpc.main.id<br><br>  health_check {<br>    port = 80<br>    protocol = &quot;HTTP&quot;<br>    path = &quot;/&quot;<br>    matcher = &quot;200-299&quot;<br>  }<br>}<br><br>resource &quot;aws_lb_target_group_attachment&quot; &quot;main&quot; {<br>  for_each = aws_instance.main<br><br>  target_group_arn = aws_lb_target_group.main.arn<br>  target_id = each.value.id<br>  port = 80<br>}<br><br>resource &quot;aws_lb_listener&quot; &quot;http&quot; {<br>  load_balancer_arn = aws_lb.main.arn<br>  port = 80<br>  protocol = &quot;HTTP&quot;<br><br>  default_action {<br>    type = &quot;forward&quot;<br>    target_group_arn = aws_lb_target_group.main.arn<br>  }<br>}</pre><p>We’ll start by creating the Application Load Balancer (ALB) resource using the <strong>aws_lb</strong> resource block.</p><p>Next, we’ll create the target group resource. You’ll also define a health check for the target group, setting the port, protocol, and path for the check, as well as the status code range that is considered healthy.</p><p>Next, we’ll attach the instances we created to the target group using the <strong>aws_lb_target_group_attachment </strong>resource.</p><p>We’ll also create the ALB listener on port 80 with a default action to forward the traffic to the target group you created earlier.</p><h4>Step 5: Create the route53.tf file</h4><p>Amazon Route 53 is a highly available and scalable Domain Name System (DNS) web service. It is a way to route end users to Internet applications by translating human-friendly domain names into IP addresses.</p><p>In this file, you will do the following:</p><ul><li>Create a route53 hosted zone</li></ul><blockquote>A hosted zone is a container for records in the Amazon Route 53 DNS service that you can use to route traffic for a domain and its subdomains.</blockquote><ul><li>Set up an “A” record to point your ALB to your subdomain</li></ul><blockquote>An “A” record (Address record) in Amazon Route 53 is a type of DNS record that maps a domain name to an IPv4 address or Alias, such as an ALB. This allows you to associate a domain name with a specific IP address, so that when users enter the domain name into their web browser, they are directed to the correct website</blockquote><blockquote>A subdomain is a domain that is part of a larger domain. It is created by adding a prefix to the root domain name, separated by a dot.</blockquote><p>Firstly, you’ll update your<strong> variables.tf</strong> file</p><pre>#variables.tf<br><br>variable &quot;domain_names&quot; {<br>  description = &quot;Describes the domain and subdomain names&quot;<br>  type = map(string)<br>}</pre><p>Next, you’ll assign values to the declared variables in your<strong> terraform.tfvars</strong> file</p><pre>#terraform.tfvars<br><br>domain_names = {domain_name = &quot;sarahligbe.live&quot;, subdomain_name = &quot;terraform-test.sarahligbe.live&quot;}</pre><p>Finally, you’ll create the <strong>route53.tf</strong> file</p><pre>#route53.tf<br><br>resource &quot;aws_route53_zone&quot; &quot;main&quot; {<br>  name = var.domain_names.domain_name<br>}<br><br>resource &quot;aws_route53_record&quot; &quot;subdomain_a_record&quot; {<br>  zone_id = aws_route53_zone.main.zone_id<br>  name = var.domain_names.subdomain_name<br>  type = &quot;A&quot;<br><br>  alias {<br>    name = aws_lb.main.dns_name<br>    zone_id = aws_lb.main.zone_id<br>    evaluate_target_health = true<br>  }<br>}</pre><p>After creating a hosted zone, AWS Route53 automatically creates a set of default records, including a Start of Authority (SOA) record and four name server (NS) records. You can then add or update the name servers with your domain provider, such as Name.com, to route traffic for your domain and subdomains. This brings us to our final step, which is to automate the addition of our Route53 generated name servers to our domain provider, which is Name.com in this case.</p><h4>Step 6: Create the namedotcom.tf file</h4><p>This is an optional step. If you’re using a different domain provider, you can Google whether or not Terraform has a provider for them.</p><p>To use this provider, you should generate a token at <a href="https://www.name.com/account/settings/api">https://www.name.com/account/settings/api</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/336/1*RoiWTE0xwPtMR6uD4ntMZw.jpeg" /><figcaption>Name.com generate new token</figcaption></figure><p>You can give the token whatever name you like.</p><p>Next, you’ll whitelist your IP address. Whitelisting your IP address is a security measure that helps ensure that only authorized users are able to access the API. This is because when you whitelist an IP address, you are essentially creating a list of trusted IP addresses that are allowed to access the API. This helps to prevent unauthorised access to the API from unknown or untrusted sources.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/345/1*qnYk4uF72h_bhaK2XCLYpQ.jpeg" /><figcaption>Name.com whitelist IP</figcaption></figure><p>Your current IP address is displayed there, you can copy it and add it.</p><p>Your public IP address may change, however, for a couple of reasons. One common reason is that your internet service provider (ISP) uses dynamic IP addressing, which assigns a new IP address to your device when it connects to the internet. This is done to conserve IP addresses and ensure that they are used efficiently. Additionally, your IP address may change if you are using a mobile device and switch between cellular and WiFi connections, or if you are connected to a VPN. You can update your new IP address on Name.com if it changes and delete the old one.</p><p>It’s not good practice to leave sensitive information, such as your token, in your <strong>variables.tf</strong> file or <strong>terraform.tfvars</strong> file, hence we will be storing our token generated and our username as environment variables and then referencing them within our <strong>variables.tf</strong> file.</p><p>You can store your Terraform variables as environment variables by prefixing the variable name with <strong>TF_VAR</strong>. Example:</p><pre>export TF_VAR_token=&quot;yourtokenname&quot;<br>export TF_VAR_username=&quot;yourusername&quot;</pre><p>Next, you’ll update your <strong>variables.tf</strong> file</p><pre>#variables.tf<br><br>variable &quot;token&quot; {<br>  description = &quot;Name.com API token&quot;<br>  type = string<br>}<br><br>variable &quot;username&quot; {<br>  description = &quot;Name.com username&quot;<br>  type = string<br>}</pre><p>Next, we’ll update the <strong>providers.tf</strong> file to include the namedotcom provider</p><pre>#providers.tf<br><br>terraform {<br>  required_providers {<br>    aws = {<br>      source  = &quot;hashicorp/aws&quot;<br>      version = &quot;~&gt; 4.0&quot;<br>    }<br><br>    namedotcom = {<br>      source = &quot;lexfrei/namedotcom&quot;<br>      version = &quot;1.2.4&quot;<br>    }<br>  }<br>}<br><br>provider &quot;aws&quot; {<br>  region = var.region<br>}<br><br>provider &quot;namedotcom&quot; {<br>  token = var.token<br>  username = var.username<br>}</pre><p>Here, we’ve added namedotcom to the required providers block, which includes the source and version of the provider.</p><p>We’ve also added a namedotcom provider block, which specifies the authentication credentials to access the Name.com API. It requires a token and a username to be passed as variables (which we already defined). These credentials are used to access the API and make changes to domain names registered on Name.com.</p><p>Finally, create the <strong>namedotcom.tf</strong> file</p><pre>#namedotcom.tf<br><br>resource &quot;namedotcom_domain_nameservers&quot; &quot;domain_name&quot; {<br>  domain_name = var.domain_names.domain_name<br>  nameservers = [<br>    aws_route53_zone.main.name_servers[0],<br>    aws_route53_zone.main.name_servers[1],<br>    aws_route53_zone.main.name_servers[2],<br>    aws_route53_zone.main.name_servers[3]<br>  ]<br>}</pre><p>Here, we use the <strong>namedotcom_domain_nameservers</strong> resource block to add the four nameservers that were created using the <strong>aws_route53_zone</strong> resource block.</p><h4>Step 7: Build the infrastructure</h4><p>Now that we’ve successfully written the Terraform and Ansible scripts to provision our infrastructure and configure the servers, the next thing to do is proceed with building the infrastructure.</p><p><strong>Terraform init</strong></p><p>The first Terraform command you’ll run is <strong>terraform init . </strong>This command is used to initialise a working directory that contains Terraform configuration files, and initialise the backend if one is set. It sets up the necessary infrastructure to run Terraform, such as downloading plugins for the providers specified in your Terraform scripts. By running this command, you can ensure that Terraform is prepared to run correctly before you start building your infrastructure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/738/1*rPP4K0Axt4ocqRTkkCNJvA.jpeg" /><figcaption>terraform init</figcaption></figure><p>You should run this command whenever you set up a new Terraform project or after making changes to your Terraform configuration.</p><p><strong>terraform init</strong> creates a <strong>.terraform</strong> directory and a <strong>.terraform.lock.hcl</strong> file.</p><p>The <strong>.terraform</strong> directory stores the Terraform plugins and other information required for Terraform to properly manage and configure your infrastructure. It is regenerated whenever you run <strong>terraform init</strong> so there’s no need to commit it to version control.</p><p>The <strong>.terraform.lock.hcl</strong> lock file is used to enforce consistency between Terraform runs and prevent unintended changes to your infrastructure. This should be added to version control, as stated in the image above.</p><p><strong>Terraform plan</strong></p><p><strong>terraform plan</strong> is an optional but advisable command to run. This command shows what changes Terraform will make to your infrastructure based on the configuration files you have defined. The plan is an important step before applying changes to your infrastructure, as it provides an opportunity to review and confirm the changes Terraform will make.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/984/1*3Eq9Ihoju1b7rJKBaAzSQw.jpeg" /><figcaption>terraform plan</figcaption></figure><p><strong>Terraform apply</strong></p><p><strong>terraform apply</strong> is the final command to run in order to implement the changes declared in your Terraform configuration files. This command takes the plan generated by <strong>terraform plan</strong> and applies it to create, update, or delete the infrastructure components you have defined.</p><p>The command will prompt you to confirm the changes before actually applying them. You can, however, run <strong>terraform apply — auto-approve</strong> to apply the changes without getting the prompt.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/439/1*-eTbImgBYq2aIyEK2vNJtw.jpeg" /><figcaption>terraform apply</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r3DoshvTq3PtkOS5Av6V_w.gif" /><figcaption>Our website!!!</figcaption></figure><p>After you run <strong>terraform apply</strong>, Terraform creates a state file. Terraform uses the state file to keep track of the infrastructure it manages. It stores the state of the resources, including their attributes and relationships to other resources.</p><p>The state file is important because it enables Terraform to determine what actions are necessary to bring the infrastructure into the desired state defined in Terraform configuration files. By default, the state file is stored locally, but it can also be stored in a remote backend, such as a S3 bucket, for increased security and collaboration in a team environment. Do not commit your state file to version control, as it contains information about the infrastructure that Terraform manages and could be used maliciously if it falls into the wrong hands.</p><p>Whenever you change, add, or remove any resources, Terraform creates a state backup file that stores the previous state of the resources before the changes.</p><p><strong>Terraform destroy</strong></p><p>One of the great things about Terraform is how you can easily destroy all the infrastructure resources you manage with it. This ensures that there are no unused resources that are consuming your cloud provider’s resources and accruing charges.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/442/1*iGIhb7uzPZYLvtjC7V011g.jpeg" /><figcaption>terraform destroy</figcaption></figure><h4>Commit to Version Control</h4><p>You can commit your Terraform configuration files to version control, such as Git. However, you shouldn’t commit every file to version control, as they may contain sensitive information and secrets that can be accessed by unauthorised parties, leading to potential security risks.</p><p>Do not commit your <strong>terraform.tfvars</strong> file if it contains any sensitive information.</p><p>Use <strong>.gitignore</strong> file to exclude the following files that shouldn’t be committed:</p><pre>#.gitignore<br><br>#The .terraform directory<br>.terraform/*<br><br>#The tf state file and any other backup<br>*.tfstate<br>*.tfstate.*</pre><h3>Conclusion</h3><p>We’ve been able to automate and configure our infrastructure using Terraform and Ansible, commit to version control, and adhere to some security best practices. You can learn more about Terraform by checking out their official documentation.</p><p>If you have any questions, please leave them in the comment section, and I’ll be happy to answer. By following this process, you should now have a solid foundation for automating your infrastructure and keeping it secure.</p><h3>Additional Resources</h3><ol><li><a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs">Terraform AWS documentation</a></li><li><a href="https://cloud.google.com/docs/terraform/best-practices-for-terraform">Terraform best practices</a></li><li><a href="https://registry.terraform.io/providers/lexfrei/namedotcom/latest/docs">Terraform namedotcom documentation</a></li><li><a href="https://developer.hashicorp.com/terraform/language/meta-arguments/for_each">Terraform for_each meta-argument</a></li><li><a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html">How to find an AMI</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c98b8cee77f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Create a Highly Available AWS Infrastructure for Hosting a Simple Web App Using Ansible]]></title>
            <link>https://medium.com/@sarahligbe12/how-to-create-a-highly-available-aws-infrastructure-for-hosting-a-simple-web-app-using-ansible-fee5567e5c72?source=rss-5caac2e3c7e3------2</link>
            <guid isPermaLink="false">https://medium.com/p/fee5567e5c72</guid>
            <category><![CDATA[cloud]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[ansible]]></category>
            <dc:creator><![CDATA[Sarah Aligbe]]></dc:creator>
            <pubDate>Fri, 13 Jan 2023 00:57:18 GMT</pubDate>
            <atom:updated>2023-03-02T10:10:02.940Z</atom:updated>
            <content:encoded><![CDATA[<h3>How to Create a Highly Available and Secure AWS Infrastructure for Hosting a Simple Web App Using Ansible.</h3><h3>Introduction</h3><p>For <a href="https://www.altschoolafrica.com/">Altschool Africa’s</a> holiday challenge, students in the Cloud Engineering track were required to deploy a highly available AWS infrastructure to host a simple web app that displays the hostname of each server (that’s the gist of it).</p><h4>Planning for High Availability</h4><p>A highly available infrastructure is one that is designed to handle failures and continue to operate without interruption.</p><p>There are several key considerations that should be taken into account to ensure that the application can handle failures and continue to operate without interruption. Some of these include:</p><ul><li>Redundancy: One of the most important considerations is to ensure that there is redundancy built into the system so that if one component fails, there is another component that can take over and keep the system running. This can include implementing load balancers and deploying the servers in different availability zones. Load balancers come with built-in redundancy, this means that when one server fails, the load balancer will divert all the traffic to another server without affecting the user.</li><li>Failover: Which is the mechanism in place that can automatically switch to a backup system or component in the event of a failure. Load balancers have automatic failover.</li><li>Scaling: This is the ability to effectively scale up or down based on demand. This can include implementing autoscaling groups for the automatic scaling of servers.</li></ul><p>It is important to have a security plan in place that covers all aspects of the infrastructure, including network security and encryption. AWS Certificate Manager allows you to request a public certificate to secure your web application.</p><p>Taking these considerations into account, you can design a highly available web application that is resilient to failures, secure, and can continue to operate in the event of an unexpected downtime.</p><p>By using Ansible, it will be easier to automate many of these processes and provide a more consistent and reliable approach.</p><h4>The Architecture Diagram</h4><figure><img alt="Aws architecture diagram with a public subnet and private subnet each in two availability zones. The public subnets have a NAT gateway and the private subnets have EC2 instance enclosed in an autoscaling group. An application load balancer and a certificate manager lie between the two availability zones. The availability zones are in a VPC with an Internet gateway at the top. The VPC is in a region which is in AWS cloud with route53 at the top." src="https://cdn-images-1.medium.com/max/841/1*kIHBnlKETro8JLPix1jumQ.png" /><figcaption>AWS architecture diagram for a highly available infrastructure</figcaption></figure><h4>Pre-requisites</h4><p>This article guides beginners on automating the provisioning and configuration of AWS resources with Ansible. Before starting, ensure you have a basic understanding of creating AWS resources through the AWS Management Console and some experience using Ansible for automation. If you lack knowledge in either of these areas, familiarise yourself with the basics before proceeding. We will not cover the basics of creating <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#working-with-iam-roles">IAM roles</a> or the process of selecting a <a href="https://www.linkedin.com/pulse/aws-ip-ranges-your-amazon-vpc-neal-davis/">CIDR range</a> for your VPC and subnets.</p><p>The following steps should be taken before proceeding with the playbook:</p><ol><li>Create an IAM role with:</li></ol><ul><li>AmazonVPCFullAccess</li><li>AmazonEC2FullAccess</li><li>AmazonRoute53FullAccess</li><li>AwsCertificateManagerFullAccess.</li></ul><p>2. Create an EC2 instance (Ubuntu 20.04, preferably) that will serve as the ansible control node and attach the IAM role to it. This way, there will be no need to include an access key or secret key in our playbook. (This is where you will run the playbook from so it should be able to connect to the internet)</p><p>3. You should have the following software and dependencies installed:</p><ul><li>Python3 &gt;= 3.8.10</li><li><a href="https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-ansible-on-ubuntu-20-04">Ansible</a> &gt;= 2.12.0. Run ansible --version to see the version of ansible-core you have installed.</li><li><a href="https://www.educative.io/answers/installing-pip3-in-ubuntu">pip3</a> (in order to install the latest version of boto3)</li><li>boto3 (my version is 1.26.43) — Ansible uses boto3 to communicate with AWS API to allow users manage AWS infrastructure through Ansible modules.</li></ul><pre>sudo pip3 install boto3</pre><p>You can upgrade to the latest version of boto3 by running:</p><pre>sudo pip3 install boto3 --upgrade</pre><ul><li><a href="https://installati.one/ubuntu/20.04/unzip/">unzip</a> — To unzip the AWS CLI that will be downloaded</li><li><a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">AWS CLI v2 </a>— this is because ansible currently does not have an AWS Certificate Manager(ACM) module for requesting public certificates, hence the need for the AWS CLI.</li></ul><p>After successful installation of the AWS CLI, configure it by running:</p><pre>aws configure</pre><p>The AWS CLI prompts you for the following:</p><ul><li>Access key ID — This should be left blank</li><li>Secret access key — This should also be left blank</li><li>Region — Enter your preferred region (e.g., <strong>us-east-1</strong>)</li><li>Output format — <strong>json</strong></li></ul><h3>The Ansible Playbook</h3><p>The Ansible project tree is going to look like this:</p><figure><img alt="Result of running the tree command on the ansible playbook." src="https://cdn-images-1.medium.com/max/466/1*AlnKL0Vj5ABxaBzdtKEaqA.jpeg" /></figure><ul><li>ansible.cfg contains our configuration settings</li></ul><pre>[defaults]<br>inventory=host-inventory<br>remote_user=ubuntu<br>host_key_checking=false<br><br>[privilege_escalation]<br>become=yes<br>become_method=sudo </pre><ul><li>host-inventory contains the list of hosts in which the modules and tasks in the playbook will operate</li></ul><pre>[local]<br>localhost</pre><ul><li>host_vars contains the variables associated with our host</li><li>roles is a directory that contains a collection of tasks, templates which makes it easier to organize and structure the playbook.</li><li>main.yml is where each of these individual roles are consolidated to make up the entire play.</li></ul><h4>Step One: Provisioning the VPC</h4><p>From the architecture diagram and project tree above, you will notice that you are creating a VPC with:</p><ul><li>A public and private subnet each in two availability zones</li><li>An Internet gateway</li><li>Two NAT gateways in each public subnet (this is ideal for high availability, although only one is created)</li><li>A route table to route the public subnet traffic</li><li>A route table to route the private subnet traffic</li><li>A security group for the Application load balancer</li><li>A security group for the instances.</li></ul><p><strong>Defining the VPC variables</strong></p><ul><li>Create your project directory</li><li>Create <strong>host_vars/localhost</strong> directory in the root of your project directory</li><li>Create a <strong>vars.yml</strong> where all the variables needed to run the playbook will be defined.</li></ul><pre>---<br># host_vars/localhost/vars.yml<br><br># VPC variables<br><br>vpc_name: &quot;assignment-VPC&quot;   <br>vpc_cidr: &quot;172.20.0.0/16&quot;                  #VPC CIDR block<br>aws_region: &quot;us-east-1&quot;<br>az_01: &quot;us-east-1a&quot;                        #Availability zone 1<br>az_02: &quot;us-east-1b&quot;                        #Availability zone 2<br>pub_cidr_01: &quot;172.20.1.0/24&quot;              #CIDR for the first public subnet<br>pub_cidr_02: &quot;172.20.2.0/24&quot;               #CIDR for the second public subnet<br>priv_cidr_01: &quot;172.20.3.0/24&quot;               #CIDR for the first private subnet<br>priv_cidr_02: &quot;172.20.4.0/24&quot;              #CIDR for the second private subnet<br>pub_subnet_name_01: &quot;Assignment-pub-01&quot;<br>pub_subnet_name_02: &quot;Assignment-pub-02&quot;<br>priv_subnet_name_01: &quot;Assignment-priv-01&quot;<br>priv_subnet_name_02: &quot;Assignment-priv-02&quot;<br>igw_name: &quot;Assignment-IGW&quot;                #Internet gateway name<br>igw_rt_name: &quot;Assignment-IGW-RT&quot;         #Internet gateway route table name<br>nat_name: &quot;Assignment-NAT&quot;                #NAT gateway name<br>nat_rt_name: &quot;Assignment-NAT-RT&quot;          #NAT gateway route table name<br>alb_sg: &quot;ALB-SG&quot;                 #Application load balancer security group name<br>instances_sg: &quot;Instance-SG&quot;          #EC2 security group name</pre><p><strong>Next you will move on to provisioning the VPC itself</strong></p><ol><li>Create the <strong>roles/</strong> directory in the root of your project directory.</li></ol><p>2. Create the <strong>provision_vpc/tasks</strong> subdirectory in the <strong>roles</strong> directory.</p><p>3. Create the <strong>vpc.yml</strong> file</p><pre>---<br>#roles/provision_vpc/tasks/vpc.yml<br><br># Provision the vpc<br><br>  - name: Create a VPC<br>    amazon.aws.ec2_vpc_net:<br>      name: &#39;{{ vpc_name }}&#39;      <br>      cidr_block: &#39;{{ vpc_cidr }}&#39;        <br>      region: &#39;{{ aws_region }}&#39;   <br>      dns_support: true            <br>      dns_hostnames: true          <br>      state: present<br>    register: new_vpc<br>    tags:<br>      - vpc<br>      - first<br><br>  - name: save the vpc id<br>    set_fact:<br>      vpc_id: &#39;{{ new_vpc.vpc.id }}&#39;<br>      cacheable: yes</pre><p>The <a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_vpc_net_module.html">amazon.aws.ec2_vpc_net</a> module creates a VPC with the following parameters listed:</p><ul><li>name — to specify the name of the VPC</li><li>cidr_block — to specify the cidr block for the VPC</li><li>region — to specify the region in which the VPC will be created</li><li>dns_support and dns_hostnames — to enable DNS support and DNS hostname support respectively</li><li>state — to specify the state of the VPC, use ‘present’ to create the VPC or ‘absent’ to delete it.</li></ul><p>You will use register to capture the output to the new_vpcvariable.</p><p>Theset_fact module allows you to set or create new variables and their values during the execution of the playbook. Here it stores the <strong>vpc id</strong> of the VPC parsed from new_vpc so that it can be used in other parts of the playbook.</p><p>4. Create the <strong>subnets.yml</strong> file</p><pre>---<br>#roles/provision_vpc/tasks/subnets.yml<br><br># Creates two public and two private subnets and stores their subnet ids<br><br><br>  - name: Create two public and two private subnets<br>    amazon.aws.ec2_vpc_subnet:<br>      az: &#39;{{ item.az }}&#39;<br>      cidr: &#39;{{ item.cidr }}&#39;<br>      vpc_id: &#39;{{ vpc_id }}&#39;<br>      region: &#39;{{ aws_region }}&#39;<br>      state: present<br>      resource_tags:<br>        Name: &#39;{{ item.name }}&#39;<br>    loop:<br>      - { az: &#39;{{ az_01 }}&#39;, cidr: &#39;{{ pub_cidr_01 }}&#39;, name: &#39;{{ pub_subnet_name_01 }}&#39; }<br>      - { az: &#39;{{ az_02 }}&#39;, cidr: &#39;{{ pub_cidr_02 }}&#39;, name: &#39;{{ pub_subnet_name_02 }}&#39; }<br>      - { az: &#39;{{ az_01 }}&#39;, cidr: &#39;{{ priv_cidr_01 }}&#39;, name: &#39;{{ priv_subnet_name_01 }}&#39; }<br>      - { az: &#39;{{ az_02 }}&#39;, cidr: &#39;{{ priv_cidr_02 }}&#39;, name: &#39;{{ priv_subnet_name_02 }}&#39; }<br> <br><br>  - name: Gather facts about the subnets<br>    amazon.aws.ec2_vpc_subnet_info:<br>      region: &#39;{{ aws_region }}&#39;<br>      filters:<br>        &quot;tag:Name&quot;:<br>          - &#39;{{ pub_subnet_name_01 }}&#39;<br>          - &#39;{{ pub_subnet_name_02 }}&#39;<br>          - &#39;{{ priv_subnet_name_01 }}&#39;<br>          - &#39;{{ priv_subnet_name_02 }}&#39;<br>    register: subnets_info<br>    tags: first<br><br>  - name: Get the subnet ids<br>    set_fact:<br>      priv_01_subnet_id: &quot;{{ subnets_info|community.general.json_query(priv_01)|join(\&quot;\&quot;) }}&quot;<br>      priv_02_subnet_id: &quot;{{ subnets_info|community.general.json_query(priv_02)|join(\&quot;\&quot;) }}&quot;<br>      pub_01_subnet_id: &quot;{{ subnets_info|community.general.json_query(pub_01)|join(\&quot;\&quot;) }}&quot;<br>      pub_02_subnet_id: &quot;{{ subnets_info|community.general.json_query(pub_02)|join(\&quot;\&quot;) }}&quot;<br>    vars:<br>      priv_01: &quot;subnets[?tags.Name == &#39;{{ priv_subnet_name_01 }}&#39;].id&quot;<br>      priv_02: &quot;subnets[?tags.Name == &#39;{{ priv_subnet_name_02 }}&#39;].id&quot;<br>      pub_01: &quot;subnets[?tags.Name == &#39;{{ pub_subnet_name_01 }}&#39;].id&quot;<br>      pub_02: &quot;subnets[?tags.Name == &#39;{{ pub_subnet_name_02 }}&#39;].id&quot;</pre><p>The<a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_vpc_subnet_module.html">amazon.aws.ec2_vpc_subnet</a> module creates subnets in the VPC with the following parameters:</p><ul><li>az — to specify the availability zone of each subnet</li><li>cidr — to specify a distinct CIDR in each subnet (It is important that the CIDR of the subnets do not overlap)</li><li>vpc_id — to specify the ID of the VPC in which the subnets are created. The variable {{vpc_id}}listed as the value is from the variable defined with the set_fact module in <strong>vpc.yml</strong></li><li>resource_tags — is used to specify tags that will be associated with the subnet such as the name of the subnet.</li></ul><p>The<a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_loops.html#iterating-over-a-list-of-hashes">loop</a> keyword is used to iterate over a <a href="https://pythonexamples.org/python-list-of-dictionaries/">list of dictionaries</a>(or hashes) where each item in the list contains subkeys that represent the availability zone, CIDR, and name of each of the four subnets to be created.</p><blockquote>In Ansible, list of dictionaries can also be written as:</blockquote><blockquote>loop: [{ key1:value1, key2:value2 }, { key3:value3, key4:value4 }]</blockquote><p>The<a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_vpc_subnet_info_module.html">amazon.aws.ec2_vpc_subnet_info</a> module is used to gather information on the subnets created.</p><ul><li>The filters parameter is used to filter the list of the subnets returned by the module based on specific criteria. In this case the criteria is the names of the individual subnets.</li></ul><p>Theset_fact module is setting four new variables, as seen above, for later use in the playbook. The <a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_filters.html#selecting-json-data-json-queries">community.general.json_query</a> filter — a form of data manipulation in Ansible used to extract specific values from a complex data structure in JSON format, is used to extract the specific subnet ids from the subnet_info variable that was used to capture the information gathered on the subnets. join(\&quot;\&quot;) is used to display the subnet id as a string.vars is used to make the filter more readable by defining the <a href="https://jmespath.org/tutorial.html">JMESPath</a> query separately.</p><p>5. Create the <strong>igw.yml</strong> file</p><pre>---<br>#roles/provision_vpc/tasks/igw.yml<br><br># Creates the Internet gateway<br><br>  - name: Create an internet gateway<br>    amazon.aws.ec2_vpc_igw:<br>      vpc_id: &#39;{{ vpc_id }}&#39;<br>      region: &#39;{{ aws_region }}&#39;<br>      state: present<br>      resource_tags:<br>        Name: &#39;{{ igw_name }}&#39;<br>    register: igw<br><br>  - name: store the gateway id<br>    set_fact: <br>      igw_gateway_id: &#39;{{ igw.gateway_id }}&#39;</pre><p>The<a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_vpc_igw_module.html">amazon.aws.ec2_vpc_igw</a> module creates an internet gateway with parameters that define the VPC ID, region, state, and name of the internet gateway.</p><p>The internet gateway ID is extracted from the <strong>igw</strong> variable defined with register and stored in the <strong>igw_gateway_id</strong> variable to be used later in the playbook.</p><p>6. Create the <strong>nat.yml</strong> file</p><pre>---<br>#roles/provision_vpc/tasks/nat.yml<br><br># Create NAT gateway and allocate an elastic ip<br><br>  - name: Create a NAT gateway and allocate an elastic IP<br>    amazon.aws.ec2_vpc_nat_gateway:<br>      state: present<br>      subnet_id: &#39;{{ pub_01_subnet_id }}&#39;<br>      if_exist_do_not_create: true<br>      wait: yes<br>      region: &#39;{{ aws_region }}&#39;<br>    register: nat_gateway<br>    tags: nat<br><br># making it cacheable in order to use it in the delete playbook<br>  - name: store nat gateway id<br>    set_fact:<br>      nat_gate_id: &#39;{{ nat_gateway.nat_gateway_id }}&#39;<br>      </pre><p>The<a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_vpc_nat_gateway_module.html">amazon.aws.ec2_vpc_nat_gateway</a> module creates the NAT gateway with the following parameters:</p><ul><li>state and region to specify the state and region in which the NAT gateway is created respectively.</li><li>subnet_id —to specify the subnet id to create the subnet in. The NAT gateway is usually created in a public subnet.</li><li>if_exist_do_not_create — to specify whether or not to create a NAT gateway if it already exists in a subnet id. This also allows <a href="https://www.linkedin.com/pulse/making-ansible-service-module-idempotent-ankita-jamdade/">idempotency.</a></li><li>wait — is used to control whether or not the module should wait for the NAT gateway to be created and be in the “available” state before continuing with the playbook.</li></ul><p>The NAT gateway ID is stored in the <strong>nat_gate_id</strong> variable for later use in the playbook.</p><p>7. Create the <strong>igw_rt.yml</strong> file</p><pre>---<br>#roles/provision_vpc/tasks/igw_rt.yml<br><br># Create a route table for the internet gateway and associates the public subnets<br><br>  - name: Create IGW route table and associate the public subnets<br>    amazon.aws.ec2_vpc_route_table:<br>      vpc_id: &#39;{{ vpc_id }}&#39;<br>      region: &#39;{{ aws_region }}&#39;<br>      subnets:<br>        - &#39;{{ pub_01_subnet_id }}&#39;<br>        - &#39;{{ pub_02_subnet_id }}&#39;<br>      routes:<br>        - dest: 0.0.0.0/0<br>          gateway_id: &quot;{{ igw_gateway_id }}&quot;<br>      resource_tags:<br>        Name: &#39;{{ igw_rt_name }}&#39;<br>    register: IGW_RT<br> </pre><p>The<a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_vpc_route_table_module.html">amazon.aws.ec2_vpc_route_table</a> module creates a route table for controlling the flow of network traffic within the VPC by using the following parameters:</p><ul><li>vpc_id and region to specify the VPC ID and region in which the route table is to be created.</li><li>subnets — to specify the subnets to be associated with the route table. Here the public subnets are associated.</li><li>routes — to specify the routes to be added to the route table. It is specified as a list of dictionaries representing a route. Each route dictionary should contain a dest and gateway_id key. dest being the IPV4 network range for the destination address of the route. gateway_id in this case will be the Internet gateway ID defined in the <strong>igw.yml </strong>file with the set_fact module since this is a public route table.</li></ul><p>8. Create the nat_rt.yml file</p><pre>---<br>#roles/provision_vpc/tasks/nat_rt.yml<br><br># Creates a NAT route table and associates the private subnets<br><br>  - name: Create NAT route table<br>    amazon.aws.ec2_vpc_route_table:<br>      vpc_id: &#39;{{ vpc_id }}&#39;<br>      region: &#39;{{ aws_region }}&#39;<br>      subnets:<br>        - &#39;{{ priv_01_subnet_id }}&#39;<br>        - &#39;{{ priv_02_subnet_id }}&#39;<br>      routes:<br>        - dest: 0.0.0.0/0<br>          gateway_id: &quot;{{ nat_gate_id }}&quot;<br>      resource_tags:<br>        Name: &#39;{{ nat_rt_name }}&#39;<br>    register: NAT_RT<br></pre><p>Here the private subnets are associated, and the NAT gateway ID is used for the gateway_id key in the routes parameter.</p><p>9. Create the <strong>sg.yml</strong> file</p><pre>---<br>#roles/provision_vpc/tasks/sg.yml<br><br># Creates security groups for the ec2 instances and the application load balancer<br><br>  - name: Load balancer security group<br>    amazon.aws.ec2_group:<br>      name: &#39;{{ alb_sg }}&#39;<br>      description: Security group for the ALB<br>      vpc_id: &#39;{{ vpc_id }}&#39;<br>      region: &#39;{{ aws_region }}&#39;<br>      rules:<br>        - proto: tcp<br>          ports:<br>            - 80<br>            - 443<br>          cidr_ip: 0.0.0.0/0<br>    register: alb_sg<br><br>  - name: store the alb security group name<br>    set_fact: <br>      alb_group_id: &#39;{{ alb_sg.group_id }}&#39;<br>      alb_group_name: &#39;{{ alb_sg.group_name }}&#39;<br><br>  - name: EC2 security group<br>    amazon.aws.ec2_group:<br>      name: &#39;{{ instances_sg }}&#39;<br>      description: Security group for the instances<br>      vpc_id: &#39;{{ vpc_id }}&#39;<br>      region: &#39;{{ aws_region }}&#39;<br>      rules:<br>        - proto: tcp<br>          ports:<br>            - 80<br>            - 443<br>          group_name: &#39;{{ alb_group_name }}&#39;<br>    register: instance_sg<br>    <br><br>  - name: store the instances security group id<br>    set_fact:<br>      instance_group_id: &#39;{{ instance_sg.group_id }}&#39;</pre><p>The<a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_group_module.html">amazon.aws.ec2_group</a> module creates security groups, which are a virtual firewall that controls inbound and outbound network traffic to and from your EC2 instances.</p><ul><li>name ,vpc_id , and region parameters specify the name of the security group, the VPC ID in which the security group is created and the region respectively.</li><li>rules — specifies the set of inbound rules to enforce in the security group. It is a list of dictionaries. Each rule dictionary should contain the following keys: proto which defines the protocol the rules applies to such as “tcp”. ports which defines the port traffic is going to such as “80” for HTTP, and “443” for HTTPS. cidr_ip which is the IPV4 CIDR range traffic is coming from and group_name which is the name of the Security group traffic is coming from.</li><li>description — specifies the description of the Security group.</li></ul><p>The application load balancer’s security group is first created, and the <strong>security group name</strong> and <strong>security group id</strong> are extracted and stored using the set_fact module. Then the EC2 instance security group is created, which allows ports 80 and 443 from the application load balancer’s security group.</p><p>10. Create the <strong>main.yml</strong> file</p><pre>---<br>#roles/provision_vpc/tasks/main.yml<br><br>- include_tasks: vpc.yml<br>- include_tasks: subnets.yml<br>- include_tasks: igw.yml<br>- include_tasks: nat.yml<br>- include_tasks: igw_rt.yml<br>- include_tasks: nat_rt.yml<br>- include_tasks: sg.yml</pre><p>The <strong>main.yml</strong> file consolidates all the individual tasks that have been created in the <strong>provision_vpc</strong> role. It makes use of the <a href="https://docs.ansible.com/ansible/2.9/modules/include_tasks_module.html">include_tasks</a> module to achieve this.</p><h4>Step Two: Create AutoScaling Group</h4><p>To create the autoscaling group, you have to create the launch template first. Launch templates are a way to store and version the launch parameters of an EC2 instance. AutoScaling groups automatically increase or decrease the number of instances in the group based on user-defined policies.</p><p><strong>First, you will update host_vars/localhost/vars.yml to include the AutoScaling group variables.</strong></p><pre>#host_vars/localhost/vars.yml<br><br># Autoscaling group variables<br><br>lt_name: &quot;Assignment-LT&quot;            #Launch template name<br>lt_key_name: &quot;exam&quot;                  #Launch template key name for SSH<br>lt_instance_type: &quot;t2.micro&quot;          #Launch template instance type<br>lt_image_id: &quot;ami-0b93ce03dcbcb10f6&quot;     #ubuntu20.04 <br>asg_name: &quot;Assignment-ASG&quot;            #Auto Scaling group name<br><br># User data variables<br><br>nginx_display_directory: /var/www/assignment<br>nginx_source_file: /etc/nginx/sites-available/assignment.conf<br>nginx_dest_file: /etc/nginx/sites-enabled<br>domain: yourdomainname</pre><p><strong>Next, you will move on to creating the launch template and autoscaling group.</strong></p><ol><li>Create the <strong>autoscaling/</strong> directory in the <strong>roles</strong> directory.</li><li>Create a <strong>tasks</strong> and <strong>templates</strong> directory in the <strong>autoscaling</strong> directory. The <strong>templates</strong> directory will store the user data script you will use to deploy the web application in Jinja2 format which can include variables defined in the variables file.</li><li>In the templates directory, create a user-data.sh.j2 file. The .j2 extension indicates that this file is a Jinja2 template and should be processed as such.</li></ol><pre>#!/bin/bash<br><br>#roles/autoscaling/templates/user-data.sh.j2<br><br># script to install nginx using user data<br><br>apt update -y<br>apt install nginx php7.4 php7.4-fpm -y<br>systemctl start nginx<br>mkdir -p {{ nginx_display_directory }}<br><br>echo &quot;?php echo &quot;Hello world! This is &#39;; echo gethostname(); ?&gt;&quot; | tee {{ nginx_display_directory }}/index.php<br><br>printf &quot;server {\n<br>       listen 80 default_server;\n<br>       listen [::]:80 default_server;\n<br>       server_name {{ domain }};\n<br>       root {{ nginx_display_directory }};\n<br>       index index.php;\n<br>       location / {\n<br>               try_files \$uri \$uri/ =404;\n<br>       }\n<br><br>       location ~ \.php$ {\n<br>           include snippets/fastcgi-php.conf;\n<br>           fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;\n<br>      }\n<br><br>}&quot; | tee -a {{ nginx_source_file }}<br>ln -s {{ nginx_source_file }} {{ nginx_dest_file }}<br>rm {{ nginx_dest_file }}/default<br>systemctl reload nginx</pre><p>The script above will install Nginx and PHP on our servers and then display the hostname of each of the servers.</p><p>4. Change directories to the <strong>tasks</strong> directory and create the <strong>launch_template.yml </strong>file.</p><pre>---<br>#roles/autoscaling/tasks/launch_template.yml<br><br># Creates the launch template<br><br>  - name: Create the launch template<br>    community.aws.ec2_launch_template:<br>      name: &#39;{{ lt_name }}&#39;<br>      image_id: &#39;{{ lt_image_id }}&#39;<br>      key_name: &#39;{{ lt_key_name }}&#39;<br>      instance_type: &#39;{{ lt_instance_type }}&#39;<br>      security_group_ids: &#39;{{ instance_group_id }}&#39;<br>      user_data: &quot;{{ lookup(&#39;ansible.builtin.template&#39;, &#39;user-data.sh.j2&#39;)| b64encode }}&quot;<br>      region: &#39;{{ aws_region }}&#39;<br>    register: launch_temp<br>    tags: lt<br><br>  - name: store the launch template id<br>    set_fact:<br>      launch_temp_id: &#39;{{ launch_temp.latest_template.launch_template_id }}&#39;<br>      </pre><p>The<a href="https://docs.ansible.com/ansible/latest/collections/community/aws/ec2_launch_template_module.html">community.aws.ec2_launch_template</a> module can create launch templates with the following parameters:</p><ul><li>name and region— specifies the name and region of the launch template respectively</li><li>image_id — specifies the image ID of the AMI to be used in the launch template</li><li>key_name — specifies the name of the SSH key for the launch template. There is no need to add the .pem extension</li><li>instance_type — specifies the instance type of the launch template such as “t2.micro”</li><li>security_group_ids — specifies the security group to be associated with the instances created from the launch template. The security group ID value is the <strong>instance_group_id</strong> variable created with the set_fact module in the <strong>sg.yml</strong> file</li><li>user_data — specifies the user data to be used by the instances. <a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/template_lookup.html">lookup</a> plugin is being used to retrieve the contents the user-data.sh.j2 file from the templates directory. The contents of this file are then passed through the ansible.builtin.template lookup plugin, which processes the Jinja2 template code, replacing any variables with their corresponding values. The resulting output is then passed through the b64encode filter, which encodes the data in base64 (the user data must be base64 encoded). The final output, which is the base64 encoded user data, is then passed as the value of the user_data parameter to the community.aws.ec2_launch_template module.</li></ul><p>The launch template ID from the output is stored in the <strong>launch_temp_id</strong> variable.</p><p>5. Create the <strong>asg.yml</strong> file</p><pre>---<br>#roles/autoscaling/tasks/asg.yml<br><br># Create an autoscaling group in the private subnets using the launch template<br><br>  - name: Create autoscaling group<br>    community.aws.ec2_asg:<br>      name: &#39;{{ asg_name }}&#39;<br>      availability_zones: [ &#39;{{ az_01 }}&#39;, &#39;{{ az_02 }}&#39; ]<br>      launch_template:<br>        launch_template_id: &quot;{{ launch_temp_id }}&quot;<br>      min_size: 2<br>      max_size: 5<br>      desired_capacity: 2<br>      vpc_zone_identifier: [ &#39;{{ priv_01_subnet_id }}&#39;, &#39;{{ priv_02_subnet_id }}&#39; ]<br>      health_check_type: ELB<br>      health_check_period: 120<br>      wait_for_instances: true<br>      region: &#39;{{ aws_region }}&#39;<br>    register: autosg<br><br><br>  - name: store the instance id<br>    set_fact:<br>      asg_instance_id: &#39;{{ autosg.instances }}&#39;<br>   </pre><p>( The community.aws.ec2_asg module redirects to the <a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/autoscaling_group_module.html">amazon.aws.autoscaling_group</a> module. If your ansible-core version is between 2.12.0 and 2.13.0, you can use community.aws.ec2_asg . If it is &gt;2.14.0, use amazon.aws.autoscaling_group instead ).</p><p>The community.aws.ec2_asgmodule creates auto-scaling groups with the following parameters:</p><ul><li>name and region specify the name and region of the AutoScaling group</li><li>availability_zones — specifies the availability zones of the instances in the AutoScaling group. From the architecture diagram, the AutoScaling group spans two availability zones.</li><li>launch_template — specifies the launch template to be used. It has a sub-parameter which specifies the launch template ID(which was stored in the <strong>launch_temp_id</strong> variable).</li><li>min_size — specifies the minimum number of instances that should be running in the AutoScaling group at any given time.</li><li>max_size — specifies the maximum number of instances that should be running in the AutoScaling group at any given time. The AutoScaling group will not launch more instances than this number.</li><li>desired_capacity — specifies the number of instances that the AutoScaling group should try to maintain.</li><li>vpc_zone_identifier — specifies which subnets the AutoScaling group instances should be launched in. It is a list of subnet IDs, where each subnet corresponds to a subnet in a VPC within a particular availability zone. The private subnet IDS are used.</li><li>health_check_type — specifies whether or not an instance is running and responding to requests. The AutoScaling group uses the “ELB” option to know whether the instance is healthy as reported by the Elastic load balancer</li><li>health_check_period — specifies the length of time, in seconds, that the Auto Scaling group should wait between checking the health status of an instance</li><li>wait_for_instances — specifies whether or not the module should wait for the instances to be in a ready state before continuing with the playbook.</li></ul><p>The instance IDs of the instances extracted are stored in the <strong>asg_instance_id</strong> variable (it is stored as a list since there are multiple instances).</p><p>6. Create the main.yml file</p><pre>---<br>#roles/autoscaling/tasks/main.yml<br><br>- include_tasks: launch_template.yml<br>- include_tasks: asg.yml</pre><h4>Step Three: Create the Application Load Balancer</h4><p>To create the application load balancer, you must first create a target group. Target groups are used in conjunction with load balancers to help route traffic to the resources in the group. These resources could be EC2 instances.</p><p><strong>To begin, you’ll update the host_vars/localhost/vars.yml file to include the load balancer variables</strong></p><pre>#host_vars/localhost/vars.yml<br><br># Application load balancer variables<br><br>tg_name: &quot;Assignment-TG&quot;<br>alb_name: &quot;Assignment-ALB&quot;</pre><p><strong>Next, you will move on to creating the target group and then the application load balancer.</strong></p><ol><li>In the <strong>roles/</strong> directory, create the <strong>load_balancer/tasks</strong> directory</li><li>cd into <strong>tasks/</strong> and create the target_groups.yml file</li></ol><pre>---<br>#roles/load_balancer/tasks/target_groups.yml<br><br># Creates a target group for the application load balancer<br><br>  - name: Create the target group for the load balancer<br>    community.aws.elb_target_group:<br>      name: &#39;{{ tg_name }}&#39;<br>      protocol: http<br>      port: 80<br>      vpc_id: &#39;{{ vpc_id }}&#39;<br>      health_check_protocol: http<br>      health_check_path: /<br>      successful_response_codes: &quot;200,250-260&quot;<br>      targets:<br>        - Id: &#39;{{ asg_instance_id[0] }}&#39;<br>          Port: 80<br>        - Id: &#39;{{ asg_instance_id[1] }}&#39;<br>          Port: 80<br>      state: present<br>      region: &#39;{{ aws_region }}&#39;<br>    register: tg<br>    <br>  - name: store the target group name<br>    set_fact:<br>      tg_group_name: &#39;{{ tg.target_group_name }}&#39;<br>      tg_group_arn: &#39;{{ tg.target_group_arn }}&#39;<br>      cacheable: yes</pre><p>The<a href="https://docs.ansible.com/ansible/latest/collections/community/aws/elb_target_group_module.html">community.aws.elb_target_group</a> module creates and manages target groups for the application load balancer with the following parameters:</p><ul><li>name and region — specifies the name and region in which the target group is created respectively</li><li>protocol and port—specifies the protocol that routes incoming traffic to the target group, and the port on which the target group should listen for incoming traffic respectively</li><li>health_check_protocol and health_check_path— specifies the protocol the load balancer uses to check the health of registered targets, and the path on the targets for the health checks respectively</li><li>successful_response_codes — specifies the HTTP response code that should be considered a success response from your targets</li><li>targets — specifies the targets (EC2 instances in this case) that should be registered with the target group. The value of this parameter is a list of dictionaries, where each dictionary represents a target. The subkey id defines the ID of the instances (you stored the instance ID from <strong>asg.yml</strong> output in a list <strong>asg_instance_id</strong>. You can reference each instance based on their index). The port value should match the port parameter defined earlier</li><li>state — specifies the state of the target group.</li></ul><p>The target group name and Amazon Resource Name (ARN) are stored for later use in the playbook using the set_fact module.</p><p>3. Create the <strong>alb.yml</strong> file</p><pre>---<br>#roles/load_balancer/tasks/alb.yml<br><br># Creates the application load balancer in the public subnets for the two availability zones in the vpc<br><br>  - name: Create the load balancer<br>    community.aws.elb_application_lb:<br>      name: &#39;{{ alb_name }}&#39;<br>      security_groups: &#39;{{ alb_group_id }}&#39;<br>      subnets:<br>        - &#39;{{ pub_01_subnet_id }}&#39;<br>        - &#39;{{ pub_02_subnet_id }}&#39;<br>      listeners:<br>        - Protocol: HTTP<br>          Port: 80<br>          DefaultActions:<br>            - Type: forward<br>              TargetGroupName: &#39;{{ tg_group_name }}&#39;<br>      state: present<br>      region: &#39;{{ aws_region }}&#39;<br>      wait: true<br>      wait_timeout: 300<br>    register: new_alb<br>    <br>  - name: store the alb dns name and hosted zone id<br>    set_fact:<br>      alb_dns: &#39;{{ new_alb.dns_name }}&#39;<br>      alb_hosted_zone_id: &#39;{{ new_alb.canonical_hosted_zone_id }}&#39;<br><br>  - name: Add the target group to the autoscaling group<br>    community.aws.ec2_asg:<br>      name: &#39;{{ asg_name }}&#39;<br>      target_group_arns: &#39;{{ tg_group_arn }}&#39;<br>      region: &#39;{{ aws_region }}&#39;<br>   </pre><p>( The community.aws.elb_application_lb module redirects to the <a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/elb_application_lb_module.html#ansible-collections-amazon-aws-elb-application-lb-module">amazon.aws.elb_application_lb</a> module. If your ansible-core version is between 2.12.0 and 2.13.0, you can use community.aws.elb_application_lb . If it is &gt;2.14.0, use amazon.aws.elb_application_lb instead).</p><p>Thecommunity.aws.elb_application_lb module creates and manages the application load balancer with the following parameters:</p><ul><li>name and region — specify the name and region in which the load balancer is created respectively</li><li>security_group — specifies the security group to be associated with the load balancer. The value references the security group ID you created in <strong>asg.yml</strong></li><li>subnets — specifies the subnet IDs in which the load balancer should be created in. The subnet IDs should span at least two availability zones to ensure high availability. The public subnet IDs are used</li><li>listeners — specifies the listeners to be associated with the Application load balancer. The value of this parameter is a list of dictionaries. The key-value pair of each dictionary is case sensitive.</li></ul><p>Use the set_fact module to store the dns_name and canonical_hosted_zone_id return values, which specify the DNS name and the Amazon Route 53 hosted zone ID you want to use as the alias target for the Application Loadthelancer.</p><p>4. Create the main.yml file</p><pre>---<br>#roles/load_balancer/tasks/main.yml<br><br>- include_tasks: target_groups.yml<br>- include_tasks: alb.yml</pre><h4>Step Four: Create Route53 Records</h4><p>AWS Route53 is a simple and reliable way for end users to access internet applications by translating domain names to the IP addresses of the servers hosting those applications.</p><p>To create a Route53 record for the Application load balancer, you must first create a hosted zone.</p><ol><li>In the <strong>roles/</strong> directory, create the <strong>route53/tasks/</strong> directory.</li><li>cd into the <strong>tasks/</strong> directory and create the <strong>main.yml</strong> file (yes, you can also explicitly define your tasks in the main.yml file instead of using the include_tasks module).</li></ol><pre>---<br>#roles/route53/tasks/main.yml<br><br># Create a route53 hosted zone and an A record for the application load balancer<br><br>  - name: Create route53 hosted zone<br>    community.aws.route53_zone:<br>      zone: &#39;{{ domain }}&#39;<br>    register: hosted_zone_name<br>    tags: route53<br><br>  - name: Create an alias record that points to the ALB<br>    community.aws.route53:<br>      state: present<br>      hosted_zone_id: &#39;{{ hosted_zone_name.zone_id }}&#39;<br>      record: &#39;{{ domain }}&#39;<br>      type: A<br>      overwrite: true<br>      value: &#39;{{ alb_dns }}&#39;<br>      alias: True<br>      alias_hosted_zone_id: &#39;{{ alb_hosted_zone_id }}&#39;<br><br>  - name: get nameservers<br>    community.aws.route53:<br>      zone: &#39;{{ domain }}&#39;<br>      record: &#39;{{ domain }}&#39;<br>      type: A<br>      state: get<br>    register: nameserver<br>    tags: route53<br><br>  - name: store the credentials <br>    set_fact:<br>      nameservers_1: &#39;{{ nameserver.nameservers }}&#39;<br><br>  - name: display nameservers<br>    debug:<br>      msg: &quot;Here are your nameservers: {{ nameservers_1 }}&quot;<br>    tags: route53</pre><p>( The community.aws.route53_zone and community.aws.route53 modules redirect to <a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/route53_zone_module.html#ansible-collections-amazon-aws-route53-zone-module">amazon.aws.route53_zone</a> and <a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/route53_module.html#ansible-collections-amazon-aws-route53-module">amazon.aws.route53</a> respectively. If your ansible-core version is &gt; 2.14.0, use amazon.aws.route53_zone and amazon.aws.route53 instead).</p><p>The community.aws.route53_zone creates the Route53 public hosted zone with the following parameters:</p><ul><li>zone — specifies the domain name of the hosted zone to be created.</li></ul><p>Theregister module captures the output of the module in order to use it with the community.aws.route53 module.</p><p>The community.aws.route53 creates the A record in your defined hosted zone with the following parameters:</p><ul><li>zone — specifies the domain name in which the record is created</li><li>record — specifies the full DNS record to create. The value can either be a subdomain or just the domain name itself</li><li>type — specifies the type of record to create. Yours is an “A” record</li><li>overwrite — specifies whether or not to overwrite an existing record. Initially, the value was set to false but running the playbook again resulted in an error because the record already existed so overwrite was set to true, it doesn’t really break anything since the record is still pointing to the same Application Load Balancer</li><li>alias — specifies whether or not this is an alias record. It is, since you’re not translating the domain name to an IP address but the load balancer’s DNS name</li><li>alias_hosted_zone_id — specifies the target hosted zone ID. We defined this in the <strong>alb.yml </strong>file using the set_fact module.</li></ul><p>The next task sets thestate parameter to “get” in order to capture the output from the hosted zone created. The nameservers are then saved using the set_fact module.</p><p>The last task displays the nameservers of the hosted zone. Use these nameservers to update your domain provider (e.g., Name.com), which will tell Internet service providers around the world to route traffic aimed at your domain name to AWS Route53’s nameservers. This process is known as <a href="https://www.namecheap.com/support/knowledgebase/article.aspx/9622/10/dns-propagation-explained/">DNS Propagation</a>.</p><figure><img alt="the nameservers of the route53 hosted zone" src="https://cdn-images-1.medium.com/max/1024/1*hz33d4822RkhqLAcdmIT6Q.jpeg" /><figcaption>nameservers</figcaption></figure><h4>Step Five: Request a Public Certificate from AWS Certificate Manager and update the Application Load Balancer Listeners</h4><p>AWS Certificate Manager (ACM) is a service that allows you to easily provision, manage, and deploy Secure Sockets Layer or Transport Layer Security (SSL or TLS) certificates for use with AWS services.</p><p>You have the option to either import a certificate or request a new public certificate from AWS. Since Ansible does not have a module for requesting a new public certificate using ACM at the time of writing, you’ll use the AWS CLI to request the certificate through Ansible’s shell module. Ensure that you have already configured the AWS CLI as stated in the prerequisites</p><ol><li>In the <strong>roles/</strong> directory, create the <strong>ssl/tasks/</strong> directory</li><li>In the <strong>tasks/</strong> directory, create the <strong>main.yml</strong> file</li></ol><pre>---<br>#roles/ssl/tasks/main.yml<br><br># Ansible does not have a module to request a public certificate from aws certificate manager, hence the use of the cli<br><br>  - name: Request a public certificate<br>    shell: |<br>      aws acm request-certificate \<br>      --domain-name &quot;{{ domain }}&quot; \<br>      --validation-method DNS \<br>      --region &quot;{{ aws_region }}&quot; \<br>      --domain-validation-options DomainName={{ domain }},ValidationDomain={{ domain }} \<br>      --idempotency-token prevail<br>    register: new_acm<br>  <br><br># Create a CNAME record while the certificate is still pending validation<br><br>  - name: Get the Certificate ARN<br>    set_fact:<br>certificate_arn: &quot;&quot; (new_acm.stdout|from_json). CertificateArn }}&#39;<br><br>  - name: Retrieve information about the requested certificate<br>    shell: |<br>      aws acm describe-certificate --certificate-arn &#39;{{ certificate_arn }}&#39; --region &#39;{{ aws_region }}&#39;<br>    register: certificate_details<br>    retries: 13<br>    delay: 10<br>    until: (certificate_details.stdout|from_json). (certificate_details.stdout|from_json). Certificate.DomainValidationOptions[0].ResourceRecord.Name is defined<br>    <br><br>  - name: Create DNS(CNAME) record in route 53<br>    community.aws.route53:<br>      state: present<br>      ttl: 120<br>zone:&#39;(certificate_details.stdout | from_json)&#39;{{ (certificate_details.stdout|from_json). (certificate_details.stdout|from_json). Certificate.DomainValidationOptions[0].ValidationDomain }}&#39;<br>record: &quot; (certificate_details.stdout|from_json). (certificate_details.stdout|from_json). Certificate.DomainValidationOptions[0].ResourceRecord.Name }}&#39;<br>type: &quot;&quot; (certificate_details.stdout|from_json). Certificate.DomainValidationOptions[0].ResourceRecord.Type }}&#39;<br>value: &quot;&quot; (certificate_details.stdout|from_json). Certificate.DomainValidationOptions[0].ResourceRecord.Value }}&#39;<br>      wait: yes<br>    <br><br>  - name: Wait for the certificate&#39;s status to become ISSUED<br>    shell: |<br>      aws acm describe-certificate --certificate-arn &#39;{{ certificate_arn }}&#39; --region &#39;{{ aws_region }}&#39;<br>    register: certificate_details<br>    retries: 85<br>    delay: 20<br>    until: (certificate_details.stdout|from_json). (certificate_details.stdout|from_json). Certificate.Status == &quot;ISSUED&quot;<br>    <br><br># Update the application load balancer with https listeners and add a rule that redirects http<br><br>  - name: Update the ALB to redirect http traffic to https<br>    community.aws.elb_application_lb:<br>      name: &#39;{{ alb_name }}&#39;<br>      region: &#39;{{ aws_region }}&#39;<br>      security_groups: &#39;{{ alb_group_id }}&#39;<br>      subnets:<br>        - &#39;{{ pub_01_subnet_id }}&#39;<br>        - &#39;{{ pub_02_subnet_id }}&#39;<br>      listeners:<br>        - Protocol: HTTPS<br>          Port: 443<br>          SslPolicy: ELBSecurityPolicy-2016-08<br>          DefaultActions:<br>            - Type: forward<br>              TargetGroupName: &#39;{{ tg_name }}&#39;<br>          Certificates:<br>            - CertificateArn: &#39;{{ certificate_arn }}&#39;<br>        - Protocol: HTTP<br>          Port: 80<br>          DefaultActions:<br>            - Type: forward<br>              TargetGroupName: &#39;{{ tg_name }}&#39;<br>          Rules:<br>            - Conditions:<br>                - Field: path-pattern<br>                  Values: <br>                    - &quot;/*&quot;<br>              Priority: &#39;1&#39;<br>              Actions:<br>                - Type: redirect<br>                  RedirectConfig:<br>                    Protocol: &quot;HTTPS&quot;<br>                    Port: &quot;443&quot;<br>                    Host: &quot;#{host}&quot;<br>                    Path: &quot;/#{path}&quot;<br>                    Query: &quot;#{query}&quot;<br>                    StatusCode: &quot;HTTP_301&quot;<br><br></pre><p>The <a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/shell_module.html">shell</a> module is used to run the aws acm commands.</p><p>The <a href="https://awscli.amazonaws.com/v2/documentation/api/latest/reference/acm/request-certificate.html">aws acm request-certificate</a> command requests a certificate for you from ACM with the following options:</p><ul><li>--domain_name — specifies the domain name to request the certificate for. This could be a subdomain or your domain itself</li><li>--validation-method — specifies the method to validate that the domain name belongs to you. You could choose either DNS or Email validation. DNS is a better choice</li><li>--region — specifies the region to request the certificate in</li><li>--domain-validation-options — specifies the options that are required in order to validate your domain name. The list includes the DomainName which is your domain name and ValidationDomain which could be the same as your domain name or a superdomain if you are requesting the certificate for a subdomain</li><li>--idempotency-token — specifies a token generated by you to ensure idempotency between request calls for a certificate. It typically times out after one hour.</li></ul><p>The register module captures the output from running the command so that it can be used in other parts of the playbook.</p><p>The Certificate’s Amazon Resource Name(ARN) is stored with the set_fact module from the standard output (stdout) of the variable defined with the register module.</p><p>Remember earlier you configured the AWS CLI to output in JSON format, hence why we’re using the <a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_filters.html#formatting-data-yaml-and-json">from_json</a> ansible filter to manipulate the data to extract the ARN.</p><p><strong>You will need to retrieve information about the just created certificate in order to create a DNS CNAME record in Route53.</strong></p><p>You will use the <a href="https://awscli.amazonaws.com/v2/documentation/api/latest/reference/acm/describe-certificate.html">aws acm describe-certificate</a> command in the ansible shell module to achieve this with the following options:</p><ul><li>--certificate-arn — specifies the ARN of the certificate created (which we saved with the set_fact module). This option has to be specified to run the command</li><li>--region — specifies the region in which the certificate was created in.</li></ul><p>The register module captures the output of the command.</p><p>The <a href="https://www.middlewareinventory.com/blog/ansible-retry-examples/">retries</a> module is used to retry a task until a condition is met. This module works hand-in-hand with delay which specifies the time, in seconds, the tasks is to be retried, and until which specifies the condition to be met.</p><p>The task is to be retried <strong>13 times</strong>, every <strong>10 seconds</strong> until the <strong>ResourceRecord Name</strong> in the output of the command is defined ( That’s 130 seconds = 2 minutes, 10 seconds).</p><p><strong>You will then create the DNS record in Route53</strong> using the community.aws.route53 module. The ttl parameter (time-to-live) specifies the time in seconds that a record should be cached by resolvers and other DNS clients. The zone , record , type , and value parameters are parsed from the output of the aws acm describe-certificate command captured with the register module.</p><p>Now you will wait for the certificate to become issued. You’ll use the aws acm describe-certificate along with the retries , delay , and until modules to wait for the output’s “status” to change from <strong>PENDING_VALIDATION</strong> to <strong>ISSUED.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/994/1*wbxrB7pGLxISKTM9GyKd_w.jpeg" /><figcaption>Status: PENDING_VALIDATION</figcaption></figure><p>The task is to be retried <strong>85 times</strong>, every <strong>20 seconds</strong> until the Status equals <strong>ISSUED. </strong>(That’s about 1700 seconds = 28 minutes 20 seconds)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bZVZMArZzJM0VPDkJQ27Eg.jpeg" /><figcaption>Status: ISSUED. The task was only tried twice before the condition was met</figcaption></figure><p><strong>Finally, you’ll update your Application Load Balancer to include a https listener and redirect http to https.</strong></p><p>You’ll have to specify the name , region , subnets , and security_groups parameters again in the community.aws.elb_application_lb module to enable Ansible recognise which Application Load Balancer you want to update.</p><p>In the first item in the listeners parameter list, the SslPolicy and Certificates subkeys are added along side the Protocol , Port , and DefaulActions subkeys.</p><ul><li>The <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-policy-table.html">SslPolicy</a> — specifies the security configurations that determine how the load balancer should handle SSL/TLS connections. AWS recommends <strong>ELBSecurityPolicy-2016–08</strong></li><li>Certificates — specifies the SSL certificate for an Application Load Balancer. We’ll use the ACM certificate ARN.</li></ul><p>In the second item in the listeners parameter list, you will specify the Protocol , Port , and DefaultActions subkeys of the HTTP listener that you want to redirect to HTTPS. You will use the Rules subkey to define a set of rules for the redirection.</p><p>The Rules subkey is defined with the following parameters:</p><ul><li><a href="http://ocs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#rule-condition-types">Conditions</a> — specifies the conditions to be met before the rules can be applied. The Field key specifies the criteria used to match incoming requests to the rule, such as the host header, path, or query string. You’re matching incoming requests to the “/*” <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions">path condition</a> which means that the rule will match any incoming request regardless of its path</li><li><a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#listener-rules">Priority</a> — specifies the order in which rules are evaluated, with the rule having the highest priority being evaluated first. Rules with the lowest value have the highest priority, while the default rule is always evaluated last. The priority value is set to ‘1’, hence this rule will be evaluated first.</li><li><a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#rule-action-types">Actions</a> — the action that should be taken when a request matches the conditions specified in the Conditions subkey. The Type of action is set to redirect . This basically redirects the HTTP traffic to the HTTPS traffic with the same Host , Path , and Query parameters. The StatusCode is set to &quot;HTTP_301&quot;, which is used for permanent redirects.</li></ul><h4>Step Six: Include the Roles in the main.yml file</h4><p>The last step in writing the playbook is to include all the roles in the main.yml file</p><pre>---<br>#main.yml<br><br>- name: Provisioning AWS resources using Ansible<br>  hosts: localhost<br>  connection: local<br>  gather_facts: False<br><br>  roles:<br>    - name: Create a logical network<br>      role: provision_vpc<br>    - name: Create an autoscaling group<br>      role: autoscaling<br>    - name: Create an application load balancer<br>      role: load_balancer<br>    - name: Link the load balancer to route53<br>      role: route53<br>    - name: Create a certificate manager, add it to the load balancer and update load balancer rules<br>      role: ssl</pre><p>Now that you have your complete playbook, the next thing is to run the playbook.</p><pre>ansible-playbook main.yml </pre><p>After running the playbook you should have all the necessary AWS resources provisioned and your domain name should be up and running.</p><figure><img alt="The domain name displaying “Hello world! This is ip-172–20–4–83”" src="https://cdn-images-1.medium.com/max/409/1*Ulev9yaHJjbP-kulqLbz9Q.jpeg" /><figcaption>example</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fee5567e5c72" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>