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

<channel>
	<title>Ansible &#8211; Blog of Kliment Andreev &#8211; A place so I won&#039;t forget things</title>
	<atom:link href="https://blog.andreev.it/tag/ansible/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.andreev.it</link>
	<description></description>
	<lastBuildDate>Mon, 08 Feb 2021 17:04:37 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>Ansible: Quick Start Guide for FreeBSD, CentOS and Ubuntu</title>
		<link>https://blog.andreev.it/2021/02/ansible-quick-start-guide-for-freebsd-centos-and-ubuntu/</link>
					<comments>https://blog.andreev.it/2021/02/ansible-quick-start-guide-for-freebsd-centos-and-ubuntu/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Mon, 08 Feb 2021 17:04:37 +0000</pubDate>
				<category><![CDATA[CentOS]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[FreeBSD]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Ansible]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[freebsd]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=8784</guid>

					<description><![CDATA[In this post/howto, I&#8217;ll explain how to install Ansible as control and managed node&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post/howto, I&#8217;ll explain how to install Ansible as control and managed node on FreeBSD 12, CentOS 8 and Ubuntu 18. Then, I&#8217;ll explain how to create SSH keys so the nodes can communicate and some basic tasks. Then, I&#8217;ll show you how to create a playbook to install the latest updates and also install an Apache server with the default settings. Finally, I&#8217;ll show an example of how to use variables. </p>
<h1>Control and managed nodes</h1>
<p>A control node is where you execute all of your Ansible commands and eventually keep your playbooks, configs, inventory etc. It&#8217;s pretty much your workstation. A managed node is where the actual playbooks are executed. <a href="https://docs.ansible.com/ansible/latest/network/getting_started/basic_concepts.html" rel="noopener" target="_blank">These </a>are the main concepts and the terminology.<br />
In my case, I have 4 VMs/instances. The main one which is the control node and 3 managed nodes.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2021/01/P153-01.png"><img decoding="async" src="https://blog.andreev.it/wp-content/uploads/2021/01/P153-01.png" alt="" width="309" height="113" class="aligncenter size-full wp-image-8786" srcset="https://blog.andreev.it/wp-content/uploads/2021/01/P153-01.png 309w, https://blog.andreev.it/wp-content/uploads/2021/01/P153-01-300x110.png 300w" sizes="(max-width: 309px) 100vw, 309px" /></a></p>
<h2>CentOS 8</h2>
<p>If you want to install Ansible on the control node, you have to install Python 3.x first. </p>
<pre class="brush: bash; title: ; notranslate">
sudo dnf install python3
</pre>
<p>This will also install <strong>pip</strong>. Type <strong>python3 </strong>to test, <strong>CTRL-D</strong> to exit and then install ansible.</p>
<pre class="brush: bash; title: ; notranslate">
sudo pip3 install ansible
</pre>
<p>Type <strong>ansible </strong>to test.</p>
<h2>Ubuntu</h2>
<p>Ubuntu comes with python installed, but not with pip. Install pip with:</p>
<pre class="brush: bash; title: ; notranslate">
sudo apt install python3-pip
</pre>
<p>Then install ansible with:</p>
<pre class="brush: bash; title: ; notranslate">
sudo pip3 install ansible
</pre>
<p>Type <strong>ansible </strong>to test.</p>
<h2>FreeBSD</h2>
<p>FreeBSD comes with python installed but pip is not. Check the version and then install the same pip version. </p>
<pre class="brush: bash; title: ; notranslate">
ls -l /usr/local/bin/python*
</pre>
<p>If your output is for example <strong>python37</strong>, install the same pip version.</p>
<pre class="brush: bash; title: ; notranslate">
pkg install py37-pip
</pre>
<p>Then install ansible.</p>
<pre class="brush: bash; title: ; notranslate">
pip install ansible
</pre>
<p>Type <strong>ansible </strong>to test.</p>
<h1>SSH keys</h1>
<p>While Ansible can use standard *nix username/password authentication, it&#8217;s recommended that you use SSH keys to communicate from control node to the managed nodes. For that, you&#8217;ll have to create your SSH keys. Let&#8217;s say you have an account on your control node and the username is admin. You also want to use the user ansible on the managed nodes. It really doesn&#8217;t matter what usernames you are going to choose. You can always override the keys to use, but in this case, I&#8217;ll create a key on the control node and send it to all managed nodes.<br />
On the control node, regardless of your OS, do:</p>
<pre class="brush: bash; title: ; notranslate">
ssh-keygen -b 4096
</pre>
<p>This will create a subfolder <strong>.ssh</strong> with two files: <strong>id_rsa</strong> and <strong>id_rsa.pub</strong>. The former is your private key and the later is your public key.<br />
Copy the key to your managed nodes.</p>
<pre class="brush: bash; title: ; notranslate">
ssh-copy-id ansible@nodeX.andreev.local
</pre>
<p>This will copy my key for the user admin to the node X under the ansible user. Then test the passwordless connection.</p>
<pre class="brush: bash; title: ; notranslate">
ssh ansible@nodeX.andreev.local
</pre>
<p>Mind that the use of FQDN (nodex.andreev.local) vs. hostname (nodex) is important. For SSH these two are different. Once you log to the managed node, the node will be added to the list of known hosts in the file <strong>.ssh/known_hosts</strong>. </p>
<h1>Inventory and the config file</h1>
<p>Ansible uses the inventory files to execute an action against using the options and parameters specified in the config file. The config file is <strong>/etc/ansible/ansible.cfg</strong> for CentOS and Ubuntu and <strong>/usr/local/etc/ansible/ansible.cfg </strong>for FreeBSD. You can also put the inventory file in the same directory and name it as you wish, but you have to specify the inventory as a parameter on the command line or an entry in the config file. In addition, you can have your config file in your current directory or under the <strong>.ansible</strong> directory in your home folder. <strong>ansible.cfg</strong> in the current directory has a precedence over <strong>.ansible.cfg</strong> in the home directory which has a precedence over <strong>/etc/ansible/ansible.cfg</strong>. Here is how that looks.<br />
Let&#8217;s list all the inventory.</p>
<pre class="brush: bash; title: ; notranslate">
ansible --list-hosts all
</pre>
<p>You&#8217;ll get a message that there is no inventory file. Let&#8217;s create one, we&#8217;ll name it <strong>inventory.txt</strong>.</p>
<pre class="brush: bash; title: ; notranslate">
&#x5B;freebsd]
node1.andreev.local
&#x5B;centos]
node2.andreev.local
&#x5B;ubuntu]
node3.andreev.local
&#x5B;bsd]
node1.andreev.local
&#x5B;linux]
node2.andreev.local
node3.andreev.local
</pre>
<p>If we specify the inventory file, we&#8217;ll get this.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
ansible --list-hosts all -i inventory.txt
  hosts (3):
    node1.andreev.local
    node2.andreev.local
    node3.andreev.local
</pre>
<p>If we create a config file, we can tell ansible where to look for the inventory. Create a file <strong>ansible.cfg</strong> in the same directory.</p>
<pre class="brush: bash; title: ; notranslate">
&#x5B;defaults]
inventory=/home/&lt;somewhere&gt;/inventory.txt
</pre>
<p>If you do <strong>ansible &#8211;list-hosts all</strong> now, you&#8217;ll get the same result as before, but without specifying the inventory file.<br />
Or something like this. </p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
ansible all -m shell -a &quot;uname -a&quot;
node1.andreev.local | CHANGED | rc=0 &gt;&gt;
FreeBSD node1.andreev.local 12.1-RELEASE FreeBSD 12.1-RELEASE r354233 GENERIC  amd64
node2.andreev.local | CHANGED | rc=0 &gt;&gt;
Linux node2.andreev.local 4.18.0-193.14.2.el8_2.x86_64 #1 SMP Sun Jul 26 03:54:29 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
node3.andreev.local | CHANGED | rc=0 &gt;&gt;
Linux node3.andreev.local 4.15.0-129-generic #132-Ubuntu SMP Thu Dec 10 14:02:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
</pre>
<p>By default, ansible runs on the managed node with the currently logged user that executes the playbook on the control node. If you get an error saying that the previous command cannot connect to the host, you have to specify the same user that you used when you test the connection with <em>ssh -user-@managednode.</em> So, edit <strong>ansible.cfg</strong> and add this line.</p>
<pre class="brush: bash; title: ; notranslate">
remote_user=&lt;user&gt;
</pre>
<p>We can target only the group linux which consists of Linux hosts only in the inventory file. </p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
ansible linux -m shell -a &quot;date&quot;
node3.andreev.local | CHANGED | rc=0 &gt;&gt;
Fri Jan  8 15:23:40 UTC 2021
node2.andreev.local | CHANGED | rc=0 &gt;&gt;
Fri Jan  8 10:23:40 EST 2021
</pre>
<h1>Playbooks</h1>
<p>The playbooks are the blueprints of the automation tasks. Instead of running the ansible command to execute each task separately, we combine these tasks in a YAML file and execute them sequentially. Here are 3 playbooks that update each of our managed nodes. There is no update module for FreeBSD, so we use the shell command.</p>
<pre class="brush: yaml; title: ; notranslate">
# freebsd-update.yml
---
  - hosts: freebsd
    become: yes
    tasks:
      - name: Fetch all packages
        shell: freebsd-update fetch
      - name: Install FreeBSD updates
        shell: freebsd-update install
      - name: Reboot
        reboot:
</pre>
<p>For CentOS we&#8217;ll use <strong>yum</strong>. </p>
<pre class="brush: yaml; title: ; notranslate">
# centos-update.yml
---
  - hosts: centos
    become: yes
    tasks:
      - name: Update all packages
        yum: name=* state=latest
      - name: Reboot
        reboot:
</pre>
<p>&#8230;and for Ubuntu we&#8217;ll use <strong>apt</strong>.</p>
<pre class="brush: yaml; title: ; notranslate">
# ubuntu-update.yml
---
  - hosts: ubuntu
    become: yes
    tasks:
      - name: Update all packages
        apt: name=* state=latest
      - name: Reboot
        reboot:
</pre>
<p>Save these files with a YAML extension and you can execute them with the following command.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-playbook &lt;filename&gt;
</pre>
<p>All of them will probably fail. That&#8217;s because your ansible user on the managed nodes is required a password when executing a <strong>sudo </strong>command. In order to fix that, you&#8217;ll have to add a line in the sudoers file. Edit this file using the <strong>visudo </strong>command.</p>
<pre class="brush: bash; title: ; notranslate">
visudo
</pre>
<p>&#8230;and then add this line right before the <strong>@includedir <...></strong> which is the last line in the file.</p>
<pre class="brush: bash; title: ; notranslate">
ansible ALL=(ALL) NOPASSWD:ALL
</pre>
<p>Where <strong>ansible </strong>is the user that runs the playbooks on the managed nodes. FreeBSD doesn&#8217;t come with sudo preinstalled, so you&#8217;ll have to install it first on the managed node.</p>
<pre class="brush: bash; title: ; notranslate">
pkg install sudo
</pre>
<p>These playbooks will update the OS and the packages for the Linux. For FreeBSD, it will update only the OS. Here is another example of playbooks that will install Apache server in a default configuration and change the <strong>ServerName </strong>and <strong>ServerAdmin </strong>lines. We&#8217;ll also install PHP and test our server.<br />
If you have a firewall enabled, make sure you open it up first on CentOS. Ubuntu and FreeBSD do not come with the firewall enabled. </p>
<pre class="brush: bash; title: ; notranslate">
firewall-cmd --zone=public --permanent --add-service=http
firewall-cmd --reload
</pre>
<p>For FreeBSD, the playbook looks like this. </p>
<pre class="brush: yaml; title: ; notranslate">
# freebsd-apache.yml
---
  - hosts: freebsd
    become: yes
    tasks:
      - name: Install apache and php
        pkgng:
          name:
            - apache24
            - php74
            - mod_php74
          state:  present
      - name: Start on reboot
        service: name=apache24 enabled=yes
      - name: Copy index.php
        copy:
          src: ../files/index.php
          dest: /usr/local/www/apache24/data
          mode: 0755
      - name: Copy mod_php.conf
        copy:
          src: ../files/mod_php.conf
          dest: /usr/local/etc/apache24/modules.d
          mode: 0755
      - name: Start apache now
        service: name=apache24 state=started
</pre>
<p>For CentOS, it looks like this.</p>
<pre class="brush: yaml; title: ; notranslate">
# centos-apache.yml
---
  - hosts: centos
    become: yes
    tasks:
      - name: Install apache and php
        yum:
          name:
            - httpd
            - php
          state:  present
      - name: Start apache now and on reboot
        service: name=httpd state=started enabled=yes
      - name: Copy index.php
        copy:
          src: ../files/index.php
          dest: /var/www/html
          mode: 0755
</pre>
<p>&#8230;and for Ubuntu it looks like this.</p>
<pre class="brush: yaml; title: ; notranslate">
# ubuntu-apache.yml
---
  - hosts: ubuntu
    become: yes
    tasks:
      - name: Install apache and php
        apt:
          name:
            - apache2
            - php
          state:  present
      - name: Start apache now and on reboot
        service: name=apache2 state=started enabled=yes
      - name: Copy index.php
        copy:
          src: ../files/index.php
          dest: /var/www/html
          mode: 0755
</pre>
<p>You will also need these two files in a directory called <strong>files</strong>. In my case it&#8217;s one level above the directory where I keep my playbooks.<br />
<strong>index.php</strong></p>
<pre class="brush: php; title: ; notranslate">
&lt;?php
  phpinfo();
?&gt;
</pre>
<p><strong>001_mod-php.conf</strong></p>
<pre class="brush: bash; title: ; notranslate">
&lt;IfModule dir_module&gt;
    DirectoryIndex index.php index.html
    &lt;FilesMatch &quot;\.php$&quot;&gt;
        SetHandler application/x-httpd-php
    &lt;/FilesMatch&gt;
    &lt;FilesMatch &quot;\.phps$&quot;&gt;
        SetHandler application/x-httpd-php-source
    &lt;/FilesMatch&gt;
&lt;/IfModule&gt;
</pre>
<p>The <strong>index.php</strong> file is the standard test file to test the PHP distributions and the <strong>001_mod-php.conf</strong> is needed for FreeBSD only. As you can see the playbooks differ quite a bit for these three OSes. Once you deploy the playbooks, you can test the result by going to <strong>http://[nodeX]/index.php</strong>.<br />
Looks like this.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2021/01/P153-02.png"><img fetchpriority="high" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2021/01/P153-02.png" alt="" width="983" height="227" class="aligncenter size-full wp-image-8802" srcset="https://blog.andreev.it/wp-content/uploads/2021/01/P153-02.png 983w, https://blog.andreev.it/wp-content/uploads/2021/01/P153-02-300x69.png 300w, https://blog.andreev.it/wp-content/uploads/2021/01/P153-02-768x177.png 768w, https://blog.andreev.it/wp-content/uploads/2021/01/P153-02-585x135.png 585w" sizes="(max-width: 983px) 100vw, 983px" /></a></p>
<h1>Service handlers</h1>
<p>Service handlers are used only when a change is made on the managed node. For example, we can restart a service only if the config file was changed. If the file is not changed, then there is no need to restart. Here is an example of a service handler. We&#8217;ll restart postfix service only if the main.cf file was changed.</p>
<pre class="brush: yaml; highlight: [11,14]; title: ; notranslate">
# service-handler.yml
---
- hosts: centos
  become: yes
  tasks:
  - name: Configure main.cf
    lineinfile:
      path: /etc/postfix/main.cf
      regexp: ^#mydomain
      line: 'mydomain = example.com'
    notify: restart postfix

  handlers:
  - name: restart postfix
    service: name=postfix state=restarted
</pre>
<p>Make sure that the name for the handler is the same (lines 11 and 14), so Ansible knows what service handlers is referred. </p>
<h1>Variables</h1>
<p>When ansible runs a playbook on a managed node, the first task is to gather info about the managed node. The info is a bunch of settings that we can use in our playbooks. For example, if you execute the following command, you can see the IP address, the CPU model, python version etc.</p>
<pre class="brush: bash; title: ; notranslate">
ansible -m setup &lt;node&gt;
</pre>
<p>We can use these settings and use them as variables if we need them. For example, this playbook displays the hostname and the IP.</p>
<pre class="brush: yaml; title: ; notranslate">
# showip.yml
---
  - hosts: freebsd
    become: yes
    tasks:
      - name: Show the IP address
        debug:
          msg: &quot;The hostname is {{ inventory_hostname}}  and the IP is {{ansible_default_ipv4.address }}&quot;
</pre>
<p>And if you run the playbook, you&#8217;ll see something like this.</p>
<pre class="brush: plain; title: ; notranslate">
TASK &#x5B;Show the IP address] *********************************************************************************************
ok: &#x5B;node1.andreev.local] =&gt; {
    &quot;msg&quot;: &quot;The hostname is node1.andreev.local  and the IP is 192.168.1.211&quot;
}
</pre>
<p>Here is another example of using variables. In this case, we&#8217;ll specify a file and change the ownership and the mode.</p>
<pre class="brush: yaml; title: ; notranslate">
# owner.yml
---
  - hosts: centos
    become: yes
    vars:
      filename: &quot;/var/www/html/index.php&quot;
    tasks:
      - name: Change the owner of the file
        file:
          path: &quot;{{ filename }}&quot;
          owner: apache
          group: apache
          mode: '0755'
</pre>
<p>In case we want to assign an output to a variable, we&#8217;ll have to use the keyword <strong>register</strong>. Here is an example of how to get the output from a command and print it on the screen with the keyword <strong>debug</strong>.</p>
<pre class="brush: yaml; title: ; notranslate">
# variables.yml
---
  - hosts: freebsd

    tasks:
    - name: Get the uptime manually
      command: uptime
      register: var_uptime

    - name: Print the uptime
      debug:
        msg: The uptime is &quot;{{ var_uptime }}&quot;
</pre>
<h1>Roles</h1>
<p>Roles let you automatically load related vars_files, tasks, handlers, and other Ansible artifacts based on a known file structure. Once you group your content in roles, you can easily reuse them and share them with other users. The idea is to separate the tasks, handlers and vars in different files. Let&#8217;s see this playbook for example. It changes a line in main.cf file, restarts postfix and copies a file under the postfix main directory.</p>
<pre class="brush: yaml; title: ; notranslate">
# roles.yml
---
  - hosts: centos
    become: yes

    vars:
      filevd: &quot;/etc/postfix/virtual_domains&quot;
      cfgpostfix: &quot;/etc/postfix/main.cf&quot;


    tasks:
    - name: Configure main.cf
      lineinfile:
        path: &quot;{{ cfgpostfix }}&quot;
        regexp: ^#mydomain
        line: 'mydomain = example.com'
      notify: restart postfix
    - name: Copy virtual_domains
      copy:
        src: ../files/virtual_domains
        dest: &quot;{{ filevd }}&quot;
        mode: 0755

    handlers:
    - name: restart postfix
      service: name=postfix state=restarted
</pre>
<p>We can rewrite this file by separating the tasks, variables, files and handlers. Run this command.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-galaxy role init postfix
</pre>
<p>If you look at the file/directory structure of the newly created directory postfix, it looks like this.</p>
<pre class="brush: plain; highlight: [1]; title: ; notranslate">
tree postfix
postfix
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml
</pre>
<p>Create a file <strong>virtual_domains</strong> under the <strong>files </strong>directory.</p>
<pre class="brush: bash; title: ; notranslate">
echo &quot;mydomain.com&quot; &gt; postfix/files/virtual_domains
</pre>
<p>Move the config for vars, handlers and tasks in the separate <strong>main.yml</strong> files. For example, this is how my files look like.<br />
<strong>postfix/vars/main.yml</strong></p>
<pre class="brush: yaml; title: ; notranslate">
---
# vars file for postfix

  filevd: &quot;/etc/postfix/virtual_domains&quot;
  cfgpostfix: &quot;/etc/postfix/main.cf&quot;
</pre>
<p>As you can see the keyword <strong>vars:</strong> does not exists. Ansible knows that this file is for <strong>vars </strong>so there is no need to enter the <strong>vars </strong>keyboard.<br />
<strong>postfix/tasks/main.yml</strong></p>
<pre class="brush: yaml; title: ; notranslate">
---
# tasks file for postfix

  - name: Configure main.cf
    lineinfile:
      path: &quot;{{ cfgpostfix }}&quot;
      regexp: ^#mydomain
      line: 'mydomain = example.com'
    notify: restart postfix
  - name: Copy virtual_domains
    copy:
      src: ../files/virtual_domains
      dest: &quot;{{ filevd }}&quot;
      mode: 0755
</pre>
<p><strong>postfix/handlers/main.yml</strong></p>
<pre class="brush: yaml; title: ; notranslate">
---
# handlers file for postfix

  - name: restart postfix
    service: name=postfix state=restarted
</pre>
<p>Finally, create a file called something.yml that will be your main file. This file has to be outside the postfix directory structure.<br />
In my case it looks like this.<br />
<strong>something.yml</strong></p>
<pre class="brush: yaml; title: ; notranslate">
# something.yml
---
  - hosts: centos
    become: yes
    roles:
      - postfix
</pre>
<p>Now, if you execute this playbook, ansible will automatically execute the rest of the dependant playbooks as well.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-playbook something.yml
</pre>
<h1>Error handling</h1>
<p>Sometimes we want certain changes to be ignored. Sometimes, we know the behavior of certain commands and we know that they might return non-zero code and we want that ignored. For example, consider this part of a playbook.</p>
<pre class="brush: yaml; title: ; notranslate">
- hosts: centos
  tasks: 
  - name: Type something that will fail
    command: thiscommanddoesntexist
    ignore_errors: yes

  - name: Run command remotely
    command: /usr/local/bin/somecommand
    register: cmd_result
    changed_when: cmd_result == 2
</pre>
<p>Ansible would report a task as changed as long as the command (or) script gives zero return code.<br />
In the first part, we know that the task will fail, but we decide to ignore it using the keyword <strong>ignore_errors</strong>. No matter what the command returns, <strong>ignore_errors: yes</strong> will never report to ansible that the command failed.<br />
In the second command we can ignore the error based on the output of the command. For example, if the output is 2, the the error will be ignored. If cmd_result is not equal to 2, the task will be marked as changed.<br />
So whenever this condition is true, the task will be marked as changed. </p>
<h1>Tags</h1>
<p>Tags are used when you have a playbook with several tasks and you need to run only specific parts of it instead of running the entire playbook. You use tags to execute or skip selected tasks. Let&#8217;s say we have this playbook that installs Docker on Centos and has multiple tasks. As you can notice in lines 12, 24, 37 and 45 we have a new line with a keyword <strong>tags</strong> that we use to tag certain tasks. The purpose of this is to include or exclude these tasks from the playbook.</p>
<pre class="brush: yaml; highlight: [12,24,37,45]; title: ; notranslate">
# centos-docker.yml
---
- name: Install docker
  hosts: centos
  become: true

  tasks:
    - name: Install yum utils
      yum:
        name: yum-utils
        state: latest
      tags: install

    - name: Install device-mapper-persistent-data
      yum:
        name: device-mapper-persistent-data
        state: latest
      tags: install

    - name: Install lvm2
      yum:
        name: lvm2
        state: latest
      tags: install

    - name: Add Docker repo
      get_url:
        url: https://download.docker.com/linux/centos/docker-ce.repo
        dest: /etc/yum.repos.d/docer-ce.repo
      become: yes

    - name: Install Docker
      package:
        name: docker-ce
        state: latest
      become: yes
      tags: install

    - name: Start Docker service
      service:
        name: docker
        state: started
        enabled: yes
      become: yes
      tags: start
</pre>
<p>Now, with the command below, we can execute the playbook and only the tasks tagged with <strong>install </strong>will be executed.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-playbook centos-docker.yaml --tags install
</pre>
<p>We can also tell ansible to NOT run those tasks tagged with <strong>install</strong>.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-playbook centos-docker.yaml --skip-tags install
</pre>
<p>You can add multiple tags per task, e.g.</p>
<pre class="brush: yaml; title: ; notranslate">
tags:
  - cleanup_app
  - cleanup_web
</pre>
<p>Ansible reserves two tag names for special behavior: <strong>always </strong>and <strong>never</strong>. If you assign the <strong>always </strong>tag to a task or play, Ansible will always run that task or play, unless you specifically skip it (<strong>&#8211;skip-tags always</strong>). If you assign the <strong>never </strong>tag to a task or play, Ansible will skip that task or play unless you specifically request it (<strong>&#8211;tags never</strong>).</p>
<h1>Ansible Vault</h1>
<p>Ansible Vault encrypts variables and files so you can protect sensitive content such as passwords or keys rather than leaving it visible as plaintext in playbooks or roles.<br />
First, you have to create a vaulted file where we&#8217;ll store the passwords. When you run this command it will ask you to create a password and then an empty file will show up.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-vault create secrets.yml
</pre>
<p>Add some passwords there and save the file.</p>
<pre class="brush: bash; title: ; notranslate">
mysql_pwd: &quot;DifficultPassword&quot;
ht_pwd: &quot;PasswordXYZ&quot;
</pre>
<p>If you look at the file now, you&#8217;ll see that it&#8217;s encrypted and you can&#8217;t see the passwords anymore.<br />
If you want to edit the file do <strong>ansible-vault edit secrets.yaml</strong> and enter the vault password.<br />
Create a small playbook that displays the password.</p>
<pre class="brush: yaml; title: ; notranslate">
# centos-vault.yml
---
- hosts: centos
  vars_files:
    - secrets.yml

  tasks:
  - name: Show mysql pwd
    debug:
      msg: &quot;{{ mysql_pwd }}&quot;
</pre>
<p>If you run the playbook now, ansible will throw an error saying ERROR! Attempting to decrypt but no vault secrets found. You have to specify the parameter <strong>&#8211;ask-vault-pass</strong> and enter the vault password when prompted.</p>
<h1>Prompts</h1>
<p>In case you need to pause the playbook execution and ask the user for some input such as confirmation or password, use prompts.<br />
If you want the output to echo, use <strong>private: no</strong>, otherwise what you type won&#8217;t show up on the screen. Here is an example of a playbook that asks you to confirm if a file needs to be copied to the node. If you type <strong>yes</strong> and hit enter, the file will be copied, otherwise it won&#8217;t.</p>
<pre class="brush: yaml; title: ; notranslate">
# centos-prompt.yml
---
- hosts: centos
  become: yes

  vars_prompt:
    name: upload
    private: no
    prompt: &quot;Do you want to upload xyz.txt?&quot;

  tasks:
  - name: Upload xyz.txt
    copy:
      src: xyz.txt
      dest: /var/log
    when: upload == &quot;yes&quot;
</pre>
<h1>Useful options</h1>
<pre class="brush: bash; title: ; notranslate">
ansible-playbook &lt;name&gt; --syntax-check # checks the syntax of the playbook
ansible-playbook &lt;name&gt; --check # does a dry run, reports the will-be changes, but the playbook is not executed
ansible-playbook &lt;name&gt; --step # ask to confirm each-step
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2021/02/ansible-quick-start-guide-for-freebsd-centos-and-ubuntu/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AWS: Deploy nginx HA cluster with Packer, Terraform and Ansible in a new VPC environment</title>
		<link>https://blog.andreev.it/2019/12/aws-deploy-nginx-ha-cluster-with-packer-terraform-and-ansible/</link>
					<comments>https://blog.andreev.it/2019/12/aws-deploy-nginx-ha-cluster-with-packer-terraform-and-ansible/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sun, 29 Dec 2019 23:36:48 +0000</pubDate>
				<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Ansible]]></category>
		<category><![CDATA[Packer]]></category>
		<category><![CDATA[Terraform]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=6372</guid>

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