Jekyll2023-11-16T11:12:54-08:00https://ansiblejunky.com/feed.xmlAnsible JunkyA platform to express my opinions on life and technology.John WadleighInstall Ansible Automation Platform on OpenShift Local2023-08-01T00:00:00-07:002023-08-01T00:00:00-07:00https://ansiblejunky.com/blog/ansible-platform-in-openshift-on-laptop<!--more-->
<p><img src="https://ansiblejunky.com/assets/images/ansible_platform.png" alt="Ansible" class="full" /></p>
<ul id="markdown-toc">
<li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
<li><a href="#requirements" id="markdown-toc-requirements">Requirements</a></li>
<li><a href="#openshift-crash-course" id="markdown-toc-openshift-crash-course">OpenShift Crash Course</a> <ul>
<li><a href="#platform-management" id="markdown-toc-platform-management">Platform Management</a></li>
<li><a href="#container-management" id="markdown-toc-container-management">Container Management</a></li>
<li><a href="#operations-management" id="markdown-toc-operations-management">Operations Management</a></li>
<li><a href="#image-management" id="markdown-toc-image-management">Image Management</a></li>
<li><a href="#secrets-management" id="markdown-toc-secrets-management">Secrets Management</a></li>
<li><a href="#application-delivery-management" id="markdown-toc-application-delivery-management">Application Delivery Management</a></li>
<li><a href="#logging-management" id="markdown-toc-logging-management">Logging Management</a></li>
<li><a href="#security-management" id="markdown-toc-security-management">Security Management</a></li>
<li><a href="#network-management" id="markdown-toc-network-management">Network Management</a></li>
<li><a href="#storage-management" id="markdown-toc-storage-management">Storage Management</a></li>
<li><a href="#application-management" id="markdown-toc-application-management">Application Management</a></li>
</ul>
</li>
<li><a href="#part-1---install-openshift" id="markdown-toc-part-1---install-openshift">Part 1 - Install OpenShift</a></li>
<li><a href="#part-2---install-operator" id="markdown-toc-part-2---install-operator">Part 2 - Install Operator</a></li>
<li><a href="#part-3---install-automation-controller" id="markdown-toc-part-3---install-automation-controller">Part 3 - Install Automation Controller</a> <ul>
<li><a href="#create-controller" id="markdown-toc-create-controller">Create Controller</a></li>
<li><a href="#login-to-controller" id="markdown-toc-login-to-controller">Login to Controller</a></li>
</ul>
</li>
<li><a href="#part-4---install-automation-hub" id="markdown-toc-part-4---install-automation-hub">Part 4 - Install Automation Hub</a> <ul>
<li><a href="#create-hub" id="markdown-toc-create-hub">Create Hub</a></li>
<li><a href="#login-to-hub" id="markdown-toc-login-to-hub">Login to Hub</a></li>
</ul>
</li>
<li><a href="#scaling" id="markdown-toc-scaling">Scaling</a></li>
<li><a href="#storage" id="markdown-toc-storage">Storage</a></li>
<li><a href="#load-balancer" id="markdown-toc-load-balancer">Load Balancer</a></li>
<li><a href="#topology" id="markdown-toc-topology">Topology</a></li>
<li><a href="#containers" id="markdown-toc-containers">Containers</a></li>
<li><a href="#delete-operator-controller-and-hub" id="markdown-toc-delete-operator-controller-and-hub">Delete Operator, Controller and Hub</a></li>
<li><a href="#automation-analytics" id="markdown-toc-automation-analytics">Automation Analytics</a></li>
<li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>
<h2 id="introduction">Introduction</h2>
<p><code class="language-plaintext highlighter-rouge">Red Hat Ansible Automation Platform</code> (AAP) is now supported on the <code class="language-plaintext highlighter-rouge">Red Hat OpenShift</code> platform. In order to get your hands dirty with the installation process and gain experience, follow these steps to install OpenShift and Ansible Automation Platform (AAP) on your laptop or workstation.</p>
<p>Note that some features are currently not available on the OpenShift platform:</p>
<ul>
<li>Automation Services Catalog is currently not available
<ul>
<li>Automation services catalog is a service within the Red Hat Ansible Automation Platform that allows customers to organize and govern product catalog sources on Ansible across various environments.</li>
<li>Automation Services Catalog: https://www.ansible.com/resources/webinars-training/automation-services-catalog</li>
<li>More information can be found <a href="https://www.ansible.com/blog/automation-services-catalog-a-deep-dive-into-whats-new-in-red-hat-ansible-automation-platform">here</a>.</li>
</ul>
</li>
<li>Automation Mesh is currently not available</li>
</ul>
<p>Even with these current drawbacks, it is still a great direction to consider installing AAP on your OpenShift platform. The missing features will be added in upcoming releases and if your organization already has experience with OpenShift and containers then it’s something to seriously consider instead of the traditional installation on virtual machines.</p>
<p>Instead of the traditional installation method using the tar ball and running <code class="language-plaintext highlighter-rouge">setup.sh</code> script, the situation is very different On OpenShift. We will be leveraging <code class="language-plaintext highlighter-rouge">Operators</code> that greatly simplifies the installation process. For more information on operators, read below.</p>
<h2 id="requirements">Requirements</h2>
<p>To run Red Hat OpenShift on your laptop we will need to use <code class="language-plaintext highlighter-rouge">Red Hat OpenShift Local</code> (previously known as CodeReady Containers) that greatly simplifies OpenShift architecture to a single virtual machine running on your laptop. The virtual machine is managed through the included <code class="language-plaintext highlighter-rouge">crc</code> binary and OpenShift is managed through the typical <code class="language-plaintext highlighter-rouge">oc</code> command line tool or its GUI interface.</p>
<ul>
<li>Uses <code class="language-plaintext highlighter-rouge">OpenShift Container Runtime</code> default preset as required by Ansible</li>
<li>Resources
<ul>
<li>Minimum 4 physical CPU cores; 9 GB of free memory</li>
<li>Recommended 5 vCPUs and 15 GB RAM (to accommodate for Ansible)</li>
<li>35 GB of storage</li>
</ul>
</li>
<li>Operating Systems
<ul>
<li>Microsoft Windows: Windows 10 Fall Creators Update (version 1709) or newer</li>
<li>MacOS: MacOS 11 Big Sur or later</li>
<li>Linux: Red Hat Enterprise Linux 7.5, CentOS 7.5, or latest two stable Fedora releases</li>
</ul>
</li>
<li>Uses non-customizable cluster settings
<ul>
<li>Requires the <code class="language-plaintext highlighter-rouge">*.crc.testing</code> domain.</li>
<li>Requires the <code class="language-plaintext highlighter-rouge">172</code> address range for internal cluster communication. This can cause issues when, for example, a proxy is run in the same address space.</li>
</ul>
</li>
</ul>
<h2 id="openshift-crash-course">OpenShift Crash Course</h2>
<p>Historically, Ansible Automation Platform has been installed on VMs. So it’s necessary to introduce some basic OpenShift terminology. This certainly is not trying to cover everything but rather give the reader some understanding of the new landscape in OpenShift. We need to become familiar with these concepts in order to install, manage, and upgrade the Ansible Automation Platform.</p>
<h3 id="platform-management">Platform Management</h3>
<p>Achieved through the use of the following major concepts:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">OpenShift Container Platform</code> is based on Kubernetes and therefore shares the same technology however it includes enterprise-ready enhancements such as hybrid cloud deployments, integrated Red Hat technology, and an open source development model.</li>
<li><code class="language-plaintext highlighter-rouge">Users</code> are Kubernetes objects that represents an actor which may be granted permissions in the system</li>
<li><code class="language-plaintext highlighter-rouge">Project</code> is a Kubernetes namespace with additional annotations, and manages access to resources for regular users. A project allows a community of users to organize and manage their content in isolation from other communities. Users must be given access to projects by administrators, or if allowed to create projects, automatically have access to their own projects. Most objects in the system are scoped by namespace, but some are excepted and have no namespace, including nodes and users. For the purposes of the OpenShift platform, <code class="language-plaintext highlighter-rouge">project</code> and <code class="language-plaintext highlighter-rouge">namespace</code> are interchangable.</li>
<li><code class="language-plaintext highlighter-rouge">Groups</code> are useful when managing authorization policies to grant permissions to multiple users at once, for example allowing access to objects within a project, versus granting them to users individually. A user can be assigned to one or more groups, each of which represent a certain set of users.</li>
<li><code class="language-plaintext highlighter-rouge">Authentication layer</code> can be configured by a cluster administrator to control access and ensure only approved users access the cluster. For users to interact with OpenShift Container Platform, they must first authenticate to the cluster. The authentication layer identifies the user associated with requests to the OpenShift Container Platform API.</li>
<li><code class="language-plaintext highlighter-rouge">Authorization layer</code> determines whether the identified user has permissions to perform the requested action by following the defined RBAC</li>
<li><code class="language-plaintext highlighter-rouge">Role-based access control (RBAC)</code> objects such as rules, roles and bindings determine whether a user is allowed to perform a given action within a project.</li>
</ul>
<h3 id="container-management">Container Management</h3>
<p><img src="https://ansiblejunky.com/assets/images/aap-on-openshift-architecture.png" alt="Red Hat Console" width="500px" /></p>
<p>Achieved through the use of the following architectural concepts that has a Kubernetes foundation:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">OpenShift Platform</code> represents a single <code class="language-plaintext highlighter-rouge">cluster</code></li>
<li>Each cluster has one or more <code class="language-plaintext highlighter-rouge">nodes</code>, which are virtual or bare-metal machines that provide the runtime environments</li>
<li>A typical cluster contains the following:
<ul>
<li><code class="language-plaintext highlighter-rouge">control plane</code> is a logical grouping of nodes containing exactly 3 <code class="language-plaintext highlighter-rouge">master nodes</code></li>
<li><code class="language-plaintext highlighter-rouge">execution plane</code> is a logical grouping of nodes containing at least 2 <code class="language-plaintext highlighter-rouge">worker nodes</code>, which handle various workloads</li>
<li>Optional <code class="language-plaintext highlighter-rouge">infra nodes</code> are worker nodes marked with the infra label meant to segregate functionality (such as routing, logging, monitoring) for the purpose of cost savings</li>
</ul>
</li>
<li>Each node typically runs on <code class="language-plaintext highlighter-rouge">Red Hat Enterprise Linux CoreOS</code> (RHCOS), which is a container optimized operating system</li>
<li>Each node runs one or more <code class="language-plaintext highlighter-rouge">pods</code></li>
<li>Each pod is an object that is defined, immutable, and runs one or more <code class="language-plaintext highlighter-rouge">containers</code> on one node. Each pod is allocated its own internal IP address, therefore owning its entire port space, and containers within pods can share their local storage and networking. Pods have the following lifecycle:
<ul>
<li>Pending</li>
<li>Running, if at least one of its primary containers starts</li>
<li>Succeeded, if all containers terminated successfully</li>
<li>Terminated, if at least one container terminated in failure</li>
<li>Unknown, can occur at any time when state of pod cannot be obtained</li>
</ul>
</li>
<li>Each pod is configured with specific <code class="language-plaintext highlighter-rouge">CPU</code> and <code class="language-plaintext highlighter-rouge">Memory</code> to ensure proper performance.
<ul>
<li>CPU is measured in units called <code class="language-plaintext highlighter-rouge">millicores</code>. Each node in a cluster inspects the operating system to determine the amount of CPU cores on the node, then multiplies that value by 1000 to express its total capacity. For example, if a node has 2 cores, the node’s CPU capacity would be represented as 2000m. If you wanted to use 1/10 of a single core, it would be represented as 100m.</li>
<li>Memory is measured in <code class="language-plaintext highlighter-rouge">bytes</code>. In addition, it may be used with SI suffices (E, P, T, G, M, K) or their power-of-two-equivalents (Ei, Pi, Ti, Gi, Mi, Ki).</li>
<li>More information on memory and cpu resource units in OpenShift can be found <a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes">here</a>.</li>
</ul>
</li>
<li>Each container represents a basic unit of an OpenShift <code class="language-plaintext highlighter-rouge">application</code> that comprises the application code packaged along with its dependencies, libraries, and binaries</li>
<li>Each container consumes <code class="language-plaintext highlighter-rouge">compute resources</code> from the node they run on, which are measurable quantities that can be requested, allocated, limited and consumed.</li>
</ul>
<h3 id="operations-management">Operations Management</h3>
<p>Achieved through the use of:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Operators</code> are pieces of software that ease the operational complexity of running another piece of software. They are among the most important components of the OpenShift Container Platform. Operators are the preferred method of packaging, deploying, and managing services on the control plane. They can also provide advantages to applications that users run. They provide the means of monitoring applications, performing health checks, managing over-the-air (OTA) updates, and ensuring that applications remain in your specified state.</li>
<li><code class="language-plaintext highlighter-rouge">OperatorHub</code> is a web console for cluster administrators to discover and select Operators to install on their cluster. It is deployed by default in OpenShift Container Platform.</li>
<li>The level of sophistication of the management logic encapsulated within an Operator can vary. This logic is also in general highly dependent on the type of the service represented by the Operator.</li>
</ul>
<h3 id="image-management">Image Management</h3>
<p>Achieved through the use of:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Images</code> are a packaging format for software, supporting dependencies, and operating system. Images are immutable - they cannot be changed once they are built. If you modify the software running on the image, you must build an entirely new image and replace the old one.</li>
<li>OpenShift Container Platform can build images from your source code, deploy them, and manage their lifecycle. It provides an internal, integrated container <code class="language-plaintext highlighter-rouge">image registry</code> that can be deployed in your OpenShift Container Platform environment to locally manage images. The image registry will contain all versions of an image.</li>
</ul>
<h3 id="secrets-management">Secrets Management</h3>
<p>Achieved through the use of:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Secrets</code> provide a mechanism to hold sensitive information such as passwords</li>
</ul>
<h3 id="application-delivery-management">Application Delivery Management</h3>
<p>Achieved through the use of:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">configmaps</code> allow you to decouple configuration from images</li>
<li><code class="language-plaintext highlighter-rouge">ReplicationControllers</code> and <code class="language-plaintext highlighter-rouge">ReplicaSets</code> ensure a specified number of pods are running at any given time</li>
<li><code class="language-plaintext highlighter-rouge">Deployments</code> and <code class="language-plaintext highlighter-rouge">DeploymentConfigurations</code> define how to roll out new versions of pods</li>
</ul>
<h3 id="logging-management">Logging Management</h3>
<p>Achieved through the use of:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Logging subsystem</code> contains both the OpenShift Elasticsearch Operator and Red Hat OpenShift Logging Operator</li>
</ul>
<h3 id="security-management">Security Management</h3>
<p>Achieved through the use of:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Auditing</code> provides a security-relevant chronological set of records documenting the sequence of activities that have affected the system by individual users, administrators, or other components of the system</li>
<li><code class="language-plaintext highlighter-rouge">Certificates</code> are used by various components to validate access to the cluster</li>
<li><code class="language-plaintext highlighter-rouge">Encrypting data</code> for your cluster to provide an additional layer of data security.</li>
<li><code class="language-plaintext highlighter-rouge">Vulnerability scanning</code> using the Red Hat Quay Container Security Operator to run vulnerability scans on container images used in active pods on the cluster</li>
<li><code class="language-plaintext highlighter-rouge">Compliance checking</code> using the Compliance Operator to run compliance scans and recommend remediations for any issues found</li>
<li><code class="language-plaintext highlighter-rouge">File integrity checking</code> using the File Integrity Operator to continually run checks on cluster nodes and provide a log of modified files</li>
</ul>
<h3 id="network-management">Network Management</h3>
<p>Achieved through the use of:</p>
<ul>
<li>Each pod is allocated an internal IP address. This means that pods can be treated like physical hosts or virtual machines in terms of port allocation, networking, naming, service discovery, load balancing, application configuration, and migration.</li>
<li>Pods and their containers can network and containers in the same pod share the same network space, but clients outside the cluster do not have networking access. This ensures all containers within the pod behave as if they were on the same host.</li>
<li><code class="language-plaintext highlighter-rouge">routes</code> make services accessible to clients outside the environment via real-world urls</li>
<li><code class="language-plaintext highlighter-rouge">services</code> provide internal load-balancing and service discovery across pods; apps can talk to each other via services</li>
</ul>
<h3 id="storage-management">Storage Management</h3>
<p>Achieved through the use of two major storage mechanisms:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Ephemeral storage</code>, as pods and containers are ephemeral or transient in nature and designed for stateless applications.</li>
<li><code class="language-plaintext highlighter-rouge">Persistent storage</code>, as stateful applications deployed in containers require persistent storage. <code class="language-plaintext highlighter-rouge">Persistent Volumes</code> are pre-provisioned storage frameworks to allow cluster administrators to provision persistent storage. The data inside these volumes can exist beyond the lifecycle of an individual pod. <code class="language-plaintext highlighter-rouge">Persistent Volume Claims</code> are objects that claim a portion of the defined persistent volume. When applications are deployed, OpenShift automatically connects the real storage into the container as specified by the end user. As applications move around in the cluster, the storage automatically follows them. Many different storage types are supported, from raw to block to file, both read-write-once and read-write-many.</li>
</ul>
<h3 id="application-management">Application Management</h3>
<p>OpenShift performs health checks on running Applications through the use of <code class="language-plaintext highlighter-rouge">probes</code> that are defined with the pod. Probes are powerful ways to increase the reliability and availability of applications in OpenShift. OpenShift includes two types of probes for evaluating the status of applications.</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">liveness probes</code> determine if a container is still running. If the probe fails due to a condition such as a deadlock, it kills the container and the pod then responds based on its restart policy.</li>
<li><code class="language-plaintext highlighter-rouge">readiness probes</code> determine if a container is ready to accept service requests. If the readiness probe fails for a container, it removes the pod from the list of available service endpoints. After a failure, the probe continues to examine the pod. If the pod becomes available, it adds the pod to the list of available service endpoints.</li>
<li><code class="language-plaintext highlighter-rouge">startup probes</code> determine whether the application within a container is started. All other probes are disabled until the startup succeeds. If the startup probe does not succeed within a specified time period, it kills the container, and the pod responds based on its restart policy.</li>
</ul>
<h2 id="part-1---install-openshift">Part 1 - Install OpenShift</h2>
<p>For the first part, let’s prepare and install <code class="language-plaintext highlighter-rouge">OpenShift Local</code> on your laptop.</p>
<p><img src="https://ansiblejunky.com/assets/images/aap-on-openshift-download.png" alt="Red Hat Console" class="full" /></p>
<ul>
<li>Navigate to <a href="https://console.redhat.com/openshift/create/local">Red Hat Console</a></li>
<li>Download the <code class="language-plaintext highlighter-rouge">pull secret</code> to a local file</li>
<li>For Linux laptops:
<ul>
<li>Download and extract the <code class="language-plaintext highlighter-rouge">OpenShift Local</code> archive</li>
<li>Place the binary file to a location in your <code class="language-plaintext highlighter-rouge">$PATH</code></li>
</ul>
</li>
<li>For Mac laptops:
<ul>
<li>Download and open the <code class="language-plaintext highlighter-rouge">OpenShift Local</code> file to begin the step-by-step installation</li>
<li>Run <code class="language-plaintext highlighter-rouge">crc</code> from command line, Mac security will prevent it from running, click OK</li>
<li>Go to System Prefs -> Security -> General and you’ll notice a warning about <code class="language-plaintext highlighter-rouge">crc</code>; click <code class="language-plaintext highlighter-rouge">Allow</code></li>
<li>Run <code class="language-plaintext highlighter-rouge">crc</code> from command line again and window pops up warning again but allowing you to “Open” the app</li>
</ul>
</li>
<li>Setup the CRC environment. This will create the <em>~/.crc</em> directory if it does not already exist.<br />
<code class="language-plaintext highlighter-rouge">crc setup</code></li>
<li>Configure the virtual machine using recommended configuration described above.
<ul>
<li>Set the CPUs<br />
<code class="language-plaintext highlighter-rouge">crc config set cpus 5</code></li>
<li>Set the Memory<br />
<code class="language-plaintext highlighter-rouge">crc config set memory 14305</code></li>
<li>Set the pull secret:<br />
<code class="language-plaintext highlighter-rouge">crc config set pull-secret-file <pull-secret.txt></code></li>
</ul>
</li>
<li>Review configuration settings. We can <em>only</em> configure the resources <em>before</em> we start a new OpenShift cluster. If you wish to change them later, you must first delete the existing cluster and then change the configuration.<br />
<code class="language-plaintext highlighter-rouge">crc config view</code></li>
<li>Start the installation<br />
<code class="language-plaintext highlighter-rouge">crc start</code></li>
<li>Get a cup of coffee!</li>
<li>When it’s completed, it will provide the web console URL and the credentials for both <code class="language-plaintext highlighter-rouge">developer</code> and <code class="language-plaintext highlighter-rouge">kubeadmin</code> users.</li>
<li>Login using command line
<ul>
<li>Prepare your environment
<code class="language-plaintext highlighter-rouge">eval $(crc oc-env)</code></li>
<li>Login using <code class="language-plaintext highlighter-rouge">kubeadmin</code> credentials
<code class="language-plaintext highlighter-rouge">oc login -u kubeadmin https://api.crc.testing:6443</code></li>
</ul>
</li>
<li>Login using web console
<code class="language-plaintext highlighter-rouge">crc console</code></li>
</ul>
<p>For more details on <code class="language-plaintext highlighter-rouge">Red Hat OpenShift Local</code> use the <a href="https://access.redhat.com/documentation/en-us/red_hat_openshift_local/">Getting Started Guide</a>.</p>
<h2 id="part-2---install-operator">Part 2 - Install Operator</h2>
<ul>
<li>Ensure you are logged in using the administrator account <code class="language-plaintext highlighter-rouge">kubeadmin</code></li>
<li>Navigate within OpenShift web console on the left panel to <code class="language-plaintext highlighter-rouge">Operators -> OperatorHub</code></li>
<li>In the search field type <code class="language-plaintext highlighter-rouge">aap</code> to find the Operator</li>
<li>Select the <code class="language-plaintext highlighter-rouge">Ansible Automation Platform</code> Operator</li>
<li>Select the blue <code class="language-plaintext highlighter-rouge">Install</code> button which gives you the following installation options
<ul>
<li>Update Channel</li>
<li>Installation Mode</li>
<li>Installed Namespace - The installation creates a default namespace <code class="language-plaintext highlighter-rouge">ansible-automation-platform</code> and place all objects under that namespace</li>
<li>Update Approval</li>
</ul>
</li>
<li>Leave the default options and click <code class="language-plaintext highlighter-rouge">Install</code> button</li>
<li>Wait for the <code class="language-plaintext highlighter-rouge">Installing Operator</code> message to change to<br />
<code class="language-plaintext highlighter-rouge">Installed operator - ready for use</code></li>
<li>You can watch the pods using the following <code class="language-plaintext highlighter-rouge">oc</code> commands:<br />
<code class="language-plaintext highlighter-rouge">oc get pods -n ansible-automation-platform</code></li>
</ul>
<p><img src="https://ansiblejunky.com/assets/images/aap-on-openshift-install-operator.png" alt="Create Automation Controller" class="full" /></p>
<h2 id="part-3---install-automation-controller">Part 3 - Install Automation Controller</h2>
<p>By default, when using the Red Hat Ansible Automation Platform Operator to create the Automation Controller, it creates and configures a managed PostgreSQL pod in the <em>same</em> namespace as your Ansible Automation Platform deployment. A user may instead choose to use an external database if they prefer (1) to use a dedicated node to ensure dedicated resources or (2) to manually manage backups, upgrades, or performance tweaks. For the purposes of this article, the assumption is to leverage a managed PostgreSQL instance.</p>
<p>The recommendation is to leverage the defaults using the Operator unless you specifically need it different.</p>
<h3 id="create-controller">Create Controller</h3>
<ul>
<li>Ensure you are logged in using the administrator account <code class="language-plaintext highlighter-rouge">kubeadmin</code></li>
<li>Navigate within OpenShift web console to <code class="language-plaintext highlighter-rouge">Operators -> Installed Operators</code></li>
<li>Navigate to the <code class="language-plaintext highlighter-rouge">Automation Controller</code> tab</li>
<li>Click the blue button <code class="language-plaintext highlighter-rouge">Create Automation Controller</code> on the right side</li>
<li>Set the following fields as you wish, leveraging defaults in all other cases
<ul>
<li><code class="language-plaintext highlighter-rouge">Name</code> - Set a custom name, for example <code class="language-plaintext highlighter-rouge">aap-controller</code></li>
<li><code class="language-plaintext highlighter-rouge">Replicas</code> - Defaults to 1 instance of Automation Controller, increase as you wish. For more info see <a href="#scaling">Scaling</a> section.</li>
<li><code class="language-plaintext highlighter-rouge">Remove used secrets on instance removal?</code> - Set to true to cleanup everything when you delete an instance of Automation Controller</li>
<li><code class="language-plaintext highlighter-rouge">Preload instance with data upon creation?</code> - Set to false if you do not want the default objects created (such as “Default” organization)</li>
<li><code class="language-plaintext highlighter-rouge">Enable persistence for /var/lib/projects directory?</code></li>
<li>Ignore the Advanced Configuration section, as we will use default values</li>
</ul>
</li>
<li>Click the blue button <code class="language-plaintext highlighter-rouge">Create</code> to begin creation</li>
</ul>
<p>You should see the following pods created and running:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aap-controller-565bb7dc58-rmf4z 4/4 Running 4 37m
aap-controller-postgres-13-0 1/1 Running 0 38m
</code></pre></div></div>
<p><img src="https://ansiblejunky.com/assets/images/aap-on-openshift-create-controller.png" alt="Create Automation Controller" class="full" /></p>
<h3 id="login-to-controller">Login to Controller</h3>
<p>Before we can login to the Automation Controller, we need the following items:</p>
<ul>
<li>The <code class="language-plaintext highlighter-rouge">admin</code> password generated by the Operator</li>
<li>The network <code class="language-plaintext highlighter-rouge">route</code> generated by the Operator</li>
<li>Red Hat subscription account credentials</li>
</ul>
<p>We can get this information using either the OpenShift GUI or via command line.</p>
<ul>
<li>Using OpenShift GUI:
<ul>
<li>Open the OpenShift console: <code class="language-plaintext highlighter-rouge">crc console</code></li>
<li>Login using <code class="language-plaintext highlighter-rouge">kubeadmin</code> administrator (if you forgot the kubeadmin password, use <code class="language-plaintext highlighter-rouge">crc setup</code> command to display it again)</li>
<li>Navigate to <code class="language-plaintext highlighter-rouge">Networking -> Routes</code></li>
<li>Ensure the project <code class="language-plaintext highlighter-rouge">ansible-automation-platform</code> is selected</li>
<li>Navigate to the URL shown in the <code class="language-plaintext highlighter-rouge">Location</code> column and drilldown on it</li>
<li>New browser tab opens up with Ansible</li>
</ul>
</li>
<li>Using Command Line
<ul>
<li>Change current project: <code class="language-plaintext highlighter-rouge">oc project ansible-automation-platform</code></li>
<li>Get routes: <code class="language-plaintext highlighter-rouge">oc get routes</code></li>
<li>Use the value under the <code class="language-plaintext highlighter-rouge">HOST/PORT</code> column as the URL for Ansible</li>
<li>Navigate to the URL in your browser</li>
<li>List secrets for admin accounts: <code class="language-plaintext highlighter-rouge">oc get secrets | grep admin</code></li>
<li>Get <code class="language-plaintext highlighter-rouge">admin</code> password using the following command (replace the secret): <code class="language-plaintext highlighter-rouge">oc get secret aap-controller-admin-password -o yaml |grep password:|awk '{print $2}'|base64 -d</code></li>
</ul>
</li>
<li>Login using the <code class="language-plaintext highlighter-rouge">admin</code> credentials</li>
<li>The first time you login, you will need to provide your Red Hat subscription credentials</li>
<li>Start having fun with Automation Controller</li>
</ul>
<h2 id="part-4---install-automation-hub">Part 4 - Install Automation Hub</h2>
<p>By default, when using the Red Hat Ansible Automation Platform Operator to create the Automation Hub, it creates and configures a managed PostgreSQL pod in the <em>same</em> namespace as your Ansible Automation Platform deployment.</p>
<p>The recommendation is to leverage the defaults using the Operator unless you specifically need it different.</p>
<h3 id="create-hub">Create Hub</h3>
<ul>
<li>Ensure you are logged in using the administrator account <code class="language-plaintext highlighter-rouge">kubeadmin</code></li>
<li>Navigate within OpenShift web console to <code class="language-plaintext highlighter-rouge">Operators -> Installed Operators</code></li>
<li>Navigate to the <code class="language-plaintext highlighter-rouge">Automation Hub</code> tab</li>
<li>Click the blue button <code class="language-plaintext highlighter-rouge">Create Automation Hub</code> on the right side and set the following fields
<ul>
<li><code class="language-plaintext highlighter-rouge">Name</code> - Set a custom name, for example <code class="language-plaintext highlighter-rouge">aap-hub</code></li>
<li><code class="language-plaintext highlighter-rouge">Storage Type</code> - set to <code class="language-plaintext highlighter-rouge">File</code></li>
<li>Ignore the Advanced Configuration section, as we will use default values</li>
</ul>
</li>
<li>Click the blue button <code class="language-plaintext highlighter-rouge">Create</code> to begin creation</li>
</ul>
<p>You should see the following pods created and running:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aap-hub-api-7555475ccd-7qn66 1/1 Running 0 8m37s
aap-hub-content-657cbb64bb-4pfc8 1/1 Running 0 8m31s
aap-hub-content-657cbb64bb-ssh92 1/1 Running 0 8m31s
aap-hub-postgres-13-0 1/1 Running 0 9m51s
aap-hub-redis-5b7dcff876-fgsp7 1/1 Running 0 9m8s
aap-hub-web-84485b9644-ch27t 1/1 Running 3 9m37s
aap-hub-worker-d9c9f86-qcgr2 1/1 Running 0 8m28s
aap-hub-worker-d9c9f86-v9sdb 1/1 Running 0 8m28s
</code></pre></div></div>
<p><img src="https://ansiblejunky.com/assets/images/aap-on-openshift-create-hub.png" alt="Create Automation Hub" class="full" /></p>
<h3 id="login-to-hub">Login to Hub</h3>
<p>Before we can login to the Automation Hub, we need the following items:</p>
<ul>
<li>The <code class="language-plaintext highlighter-rouge">admin</code> password generated by the Operator</li>
<li>The network <code class="language-plaintext highlighter-rouge">route</code> generated by the Operator</li>
<li>Red Hat subscription account credentials</li>
</ul>
<p>We can get this information using either the OpenShift GUI or via command line.</p>
<ul>
<li>Using OpenShift GUI:
<ul>
<li>Open the OpenShift console: <code class="language-plaintext highlighter-rouge">crc console</code></li>
<li>Login using <code class="language-plaintext highlighter-rouge">kubeadmin</code> administrator (if you forgot the kubeadmin password, use <code class="language-plaintext highlighter-rouge">crc setup</code> command to display it again)</li>
<li>Navigate to <code class="language-plaintext highlighter-rouge">Networking -> Routes</code></li>
<li>Ensure the project <code class="language-plaintext highlighter-rouge">ansible-automation-platform</code> is selected</li>
<li>Navigate to the URL shown in the <code class="language-plaintext highlighter-rouge">Location</code> column and drilldown on it</li>
<li>New browser tab opens up with Ansible</li>
</ul>
</li>
<li>Using Command Line
<ul>
<li>Change current project: <code class="language-plaintext highlighter-rouge">oc project ansible-automation-platform</code></li>
<li>Get routes: <code class="language-plaintext highlighter-rouge">oc get routes</code></li>
<li>Use the value under the <code class="language-plaintext highlighter-rouge">HOST/PORT</code> column as the URL for Ansible</li>
<li>Navigate to the URL in your browser</li>
<li>List secrets for admin accounts: <code class="language-plaintext highlighter-rouge">oc get secrets | grep admin</code></li>
<li>Get <code class="language-plaintext highlighter-rouge">admin</code> password using the following command (replace the secret): <code class="language-plaintext highlighter-rouge">oc get secret aap-hub-admin-password -o yaml |grep password:|awk '{print $2}'|base64 -d</code></li>
</ul>
</li>
<li>Login using the <code class="language-plaintext highlighter-rouge">admin</code> credentials</li>
<li>The first time you login, you will need to provide your Red Hat subscription credentials</li>
<li>Start having fun with Automation Hub</li>
</ul>
<h2 id="scaling">Scaling</h2>
<p>You can scale the Automation Controller up or down by leveraging the <code class="language-plaintext highlighter-rouge">Replicas</code> property in the Automation Controller that is managed by the Operator. Do NOT try to change this anywhere else as it currently is not supported. For example, if you try to scale up editing the <code class="language-plaintext highlighter-rouge">Deployment</code> then those pods will start but soon terminated as the Operator is managing these pods and believes there should only be 1 replica.</p>
<ul>
<li>Navigate to <code class="language-plaintext highlighter-rouge">Operators -> Installed Operator</code></li>
<li>Select <code class="language-plaintext highlighter-rouge">Ansible Automation Platform</code> operator</li>
<li>Select the <code class="language-plaintext highlighter-rouge">Automation Controller</code> tab</li>
<li>Click on the 3-dots on the right hand side of the listed Automation Controller</li>
<li>Click on <code class="language-plaintext highlighter-rouge">Edit Automation Controller</code></li>
<li>In the YAML, scroll to the <code class="language-plaintext highlighter-rouge">spec</code> section for property <code class="language-plaintext highlighter-rouge">replicas</code></li>
<li>Change the number to scale up or down</li>
<li>Click blue <code class="language-plaintext highlighter-rouge">Save</code> button</li>
<li>Navigate to <code class="language-plaintext highlighter-rouge">Workloads -> Pods</code> and watch the Operator scale up/down your Automation Controller pods</li>
</ul>
<p>Notice that the Operator will generate unique pod names based on the name you provided the Automation Controller and it will still only create one instance of Postgres pod.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aap-controller-565bb7dc58-kl7dg 4/4 Running 1 <span class="o">(</span>10s ago<span class="o">)</span> 2m23s
aap-controller-565bb7dc58-r2bpk 4/4 Running 1 <span class="o">(</span>10s ago<span class="o">)</span> 2m23s
aap-controller-565bb7dc58-rmf4z 4/4 Running 1 <span class="o">(</span>13s ago<span class="o">)</span> 2m23s
aap-controller-postgres-13-0 1/1 Running 0 3m8s
</code></pre></div></div>
<p><img src="https://ansiblejunky.com/assets/images/aap-on-openshift-scaling.png" alt="Scaling Automation Controller" class="full" /></p>
<h2 id="storage">Storage</h2>
<p>To view the related storage for Ansible Automation Platform, navigate to <code class="language-plaintext highlighter-rouge">Storage -> PersistentVolumeClaims</code> to list the PVCs created to manage data for Automation Controller and Automation Hub.</p>
<p>Within OpenShift you can easily <a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/#expanding-persistent-volumes-claims">expand persistent volume claims</a> so keep things simple and go with the default sizing options for the PostgreSQL database for the Ansible Automation Platform and expand it later as needed.</p>
<p>Consider using the <a href="https://github.com/redhat-cop/volume-expander-operator">Volume Expander Operator</a> that is provided by the Red Hat Community of Practice. The purpose of the operator is to expand volumes when they are running out of space. This is achieved by using the <a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/#expanding-persistent-volumes-claims">volume expansion feature</a> mentioned above.</p>
<h2 id="load-balancer">Load Balancer</h2>
<p>Traditionally Ansible Automation Platform architecture includes configuration of a front Load Balancer to help manage the load over a cluster of virtual machine nodes that serve the various requests. In this case AAP on OpenShift is running one or more Automation Controller pods and a single <code class="language-plaintext highlighter-rouge">route</code> object has been defined to reach and use those pods. The <code class="language-plaintext highlighter-rouge">route</code> object is a load balancer (haproxy) and handles the load using either round robin or least connection methods (depending on how it has been configured, least connection being the default). For more information review the OpenShift online documentation section <code class="language-plaintext highlighter-rouge">Networking -> Configuring Routes</code>.</p>
<h2 id="topology">Topology</h2>
<p>The <code class="language-plaintext highlighter-rouge">Topology</code> view in the <code class="language-plaintext highlighter-rouge">Developer</code> perspective of the web console provides a visual representation of all the applications within a project, their build status, and the components and services associated with them. It’s a very useful tool to become familiar with as you learn about AAP on OCP. Ensure you switch to <code class="language-plaintext highlighter-rouge">Developer</code> perspective (instead of <code class="language-plaintext highlighter-rouge">Administrator</code> that we have used so far) and you set the <code class="language-plaintext highlighter-rouge">Project</code> to <code class="language-plaintext highlighter-rouge">ansible-automation-platform</code>.</p>
<p><img src="https://ansiblejunky.com/assets/images/aap-on-openshift-topology.png" alt="Topology" class="full" /></p>
<h2 id="containers">Containers</h2>
<p>If you wish to take a look under the hood of the Ansible Automation Platform, go to the web console and navigate to <code class="language-plaintext highlighter-rouge">Workloads -> Pods</code>. Then drill down on a pod link and then scroll down to the <code class="language-plaintext highlighter-rouge">Containers</code> section to see the list of containers within that pod. Drill down on one of the containers to learn more information about things like Ports, Mounted Volumes, Environment Variables, CPU/Memory Resource Requests, Image source, and Pod IP Address.</p>
<p>Take it a step further by establishing a remote shell session to a container using these commands:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get list of containers for all pods in the ansible namespace</span>
<span class="nv">$ </span>oc get pods <span class="nt">-n</span> ansible-automation-platform <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{range .items[*]}{"POD: "}{.metadata.name}{"\n containers: "}{.spec.containers[*].name}{"\n\n"}{end}'</span>
POD: aap-controller-565bb7dc58-kl7dg
containers: redis aap-controller-web aap-controller-task aap-controller-ee
POD: aap-controller-565bb7dc58-r2bpk
containers: redis aap-controller-web aap-controller-task aap-controller-ee
POD: aap-controller-565bb7dc58-rmf4z
containers: redis aap-controller-web aap-controller-task aap-controller-ee
POD: aap-controller-postgres-13-0
containers: postgres
POD: aap-hub-api-7555475ccd-7qn66
containers: api
POD: aap-hub-content-657cbb64bb-4pfc8
containers: content
POD: aap-hub-content-657cbb64bb-ssh92
containers: content
POD: aap-hub-postgres-13-0
containers: postgres
POD: aap-hub-redis-5b7dcff876-fgsp7
containers: redis
POD: aap-hub-web-84485b9644-ch27t
containers: web
POD: aap-hub-worker-d9c9f86-qcgr2
containers: worker
POD: aap-hub-worker-d9c9f86-v9sdb
containers: worker
POD: automation-controller-operator-controller-manager-c88f859f768lw
containers: kube-rbac-proxy automation-controller-manager
POD: automation-hub-operator-controller-manager-6fc656c8b-6bt5h
containers: kube-rbac-proxy automation-hub-manager
POD: resource-operator-controller-manager-c75999d8b-7vhqt
containers: kube-rbac-proxy platform-resource-manager
<span class="c"># Remote shell into a container: oc rsh -c <container> <pod></span>
<span class="nv">$ </span>oc rsh <span class="nt">-c</span> aap-controller-task aap-controller-565bb7dc58-r2bpk
sh-4.2<span class="err">$</span>
</code></pre></div></div>
<h2 id="delete-operator-controller-and-hub">Delete Operator, Controller and Hub</h2>
<p>In some cases you might want to recreate the Automation Controller or Automation Hub instance and the good news is this is made incredibly simple. Follow these steps for truly any object created by the Operator.</p>
<p>If you additionally wish to delete the Operator itself, do so in the <code class="language-plaintext highlighter-rouge">Operators -> Installed Operators</code> area but ensure all objects created by the Operator are deleted first.</p>
<ul>
<li>Navigate within OpenShift web console to <code class="language-plaintext highlighter-rouge">Operators -> Installed Operators</code></li>
<li>Navigate to the proper tab for the object you want to delete, for example <code class="language-plaintext highlighter-rouge">Automation Hub</code> tab</li>
<li>Existing instances are listed</li>
<li>Select the 3-dots on the right side of the instance to expose a popup menu</li>
<li>Select the <code class="language-plaintext highlighter-rouge">Delete</code> option</li>
<li>Notice related objects in OpenShift (pods, secrets, pvc, etc) have been shutdown, destroyed and removed</li>
</ul>
<h2 id="automation-analytics">Automation Analytics</h2>
<p>Red Hat also offers Automation Analytics from their <a href="https://console.redhat.com/ansible/ansible-dashboard">cloud console</a> site. For more information on how you can leverage this as well as Red Hat Insights for reporting purposes read the <a href="https://www.ansible.com/blog/red-hat-insights-for-ansible-automation-platform-new-report-modules">following blog</a>.</p>
<h2 id="references">References</h2>
<p>Some extra information and resources leveraged in this article.</p>
<p>For OpenShift:</p>
<ul>
<li><a href="https://access.redhat.com/documentation/en-us/red_hat_openshift_local/">Product Documentation for Red Hat OpenShift Local</a></li>
<li><a href="https://access.redhat.com/documentation/en-us/red_hat_ansible_automation_platform/">Product Documentation for Red Hat Ansible Automation Platform</a></li>
<li><a href="https://github.com/code-ready/crc">Upstream Source Code</a></li>
<li><a href="https://access.redhat.com/articles/2988581">OpenShift Admins Guide to jsonpath</a></li>
<li><a href="https://freedomben.medium.com/introduction-to-oc-the-openshift-command-line-power-tool-cdcd399b4048">Introduction to “oc” — the OpenShift Command Line Power Tool</a></li>
<li><a href="https://opensource.com/article/22/5/guide-containers-images">opensource.com - A hands-on guide to images and containers for developers</a></li>
<li><a href="https://docs.openshift.com/container-platform/4.10/operators/understanding/olm-what-operators-are.html#olm-maturity-model_olm-what-operators-are">Operator Maturity Model</a></li>
<li><a href="https://access.redhat.com/solutions/5034771">Infrastructure Nodes in OpenShift 4</a></li>
<li><a href="https://www.ansible.com/blog/on-demand-execution-with-red-hat-openshift">On-Demand execution with Red Hat OpenShift</a></li>
<li><a href="https://www.redhat.com/en/technologies/cloud-computing/openshift/what-are-openshift-operators">What are Red Hat OpenShift Operators?</a></li>
<li><a href="https://cloud.redhat.com/blog/self-hosted-load-balancer-for-openshift-an-operator-based-approach">Self-hosted Load Balancer for OpenShift: an Operator Based Approach by Raffaele Spazzoli</a></li>
<li><a href="https://github.com/redhat-cop/keepalived-operator">Keepalived operator</a></li>
</ul>
<p>For Ansible Platform:</p>
<ul>
<li><a href="https://www.ansible.com/blog/whats-new-in-ansible-automation-platform-2.2">What’s new in Ansible Automation Platform 2.2</a></li>
<li><a href="https://www.ansible.com/blog/using-ansible-automation-platform-gitlab-ce-and-webhooks-to-deploy-iis-website?hsLang=en-us">Using Ansible Automation Platform, GitLab CE and webhooks to deploy IIS website</a></li>
<li><a href="https://www.ansible.com/blog/on-demand-execution-with-red-hat-openshift">On-Demand execution with Red Hat OpenShift</a></li>
<li><a href="https://www.ansible.com/blog/8-private-automation-hub-features-about-automation-execution-environments">8 private automation hub features about automation execution environments</a></li>
</ul>John WadleighThe simple guide to get you started with Ansible Automation Platform on Openshift with just your laptopVDI Best Practices2022-06-02T00:00:00-07:002022-06-02T00:00:00-07:00https://ansiblejunky.com/blog/vdi-best-practices<p><img src="https://ansiblejunky.com/assets/images/vdi.png" alt="Ansible" class="full" /></p>
<ul id="markdown-toc">
<li><a href="#meetings" id="markdown-toc-meetings">Meetings</a></li>
<li><a href="#development" id="markdown-toc-development">Development</a> <ul>
<li><a href="#ssh-client" id="markdown-toc-ssh-client">SSH Client</a></li>
<li><a href="#code-editor" id="markdown-toc-code-editor">Code Editor</a></li>
</ul>
</li>
</ul>
<p>Managing your own company emails, meetings, applications and then additionally those within the VDI (Virtual Desktop Infrastructure) session for the customer you are working with can be a struggle.</p>
<p>Below are some tips and tricks while using a VDI session.</p>
<h2 id="meetings">Meetings</h2>
<p>Configure forwarding of meetings to your main email address and calendar system. This ensures one central view of your meetings.</p>
<ul>
<li>Open <code class="language-plaintext highlighter-rouge">Outlook</code></li>
<li>Navigate to <code class="language-plaintext highlighter-rouge">File -> Manage Rules & Alerts</code> menu</li>
<li>Select <code class="language-plaintext highlighter-rouge">New Rule ...</code></li>
<li>Select <code class="language-plaintext highlighter-rouge">Apply rule on messages | receive</code> and click NEXT</li>
<li>Select <code class="language-plaintext highlighter-rouge">which is a meeting invitation or update</code> and click NEXT</li>
<li>Select <code class="language-plaintext highlighter-rouge">Yes</code> to confirm</li>
<li>Select <code class="language-plaintext highlighter-rouge">forward it to "people or public group"</code>, and then click the hyperlink <code class="language-plaintext highlighter-rouge">people or public group</code> in the box below and add provide target email address in the <code class="language-plaintext highlighter-rouge">TO</code> field and click OK</li>
<li>Click NEXT</li>
<li>Click NEXT</li>
<li>Specify the rule name, for example “Forward to Red Hat”</li>
<li>Click FINISH</li>
</ul>
<h2 id="development">Development</h2>
<p>If you intend to do some development work within a VDI session, you’ll most likely need to setup SSH, code editor, and ultimately be able to clone a repository from a version control system to begin your work.</p>
<p>Most of these instructions can be completed even if you do not have <code class="language-plaintext highlighter-rouge">administrator access</code> within your VDI environment. Simply install apps into a shared folder. Some folders will be emptied upon restart of your VDI session so be sure you find the right folder to use.</p>
<h3 id="ssh-client">SSH Client</h3>
<p>Install the Home Edition (free) of <a href="https://mobaxterm.mobatek.net/">MobaXTerm</a>. It is a multi window SSH client that is really easy to use.</p>
<p>Note that you might need to install the <code class="language-plaintext highlighter-rouge">Portable Edition</code> which has the software inside a single folder and does not require administrator access to the machine.</p>
<h3 id="code-editor">Code Editor</h3>
<p>My editor of choice currently is <code class="language-plaintext highlighter-rouge">Visual Studio Code</code>. Here’s how to install it and configure it to pull code from a repository using either HTTPS or SSH authentication.</p>
<ul>
<li>Desktop
<ul>
<li>Install <a href="https://gitforwindows.org/">Git for Windows</a>, with all default installation options</li>
<li>Install <a href="https://code.visualstudio.com/download">Visual Studio Code</a>, with all default options</li>
<li>Start <code class="language-plaintext highlighter-rouge">Git Bash</code> command window</li>
<li>Run <code class="language-plaintext highlighter-rouge">ssh-keygen</code> with default values to create your private key file</li>
<li>Run <code class="language-plaintext highlighter-rouge">cat .ssh/id_rsa.pub</code> to show the public key</li>
</ul>
</li>
<li>BitBucket
<ul>
<li>Navigate to BitBucket using your browser</li>
<li>Navigate to top-right icon and click on <code class="language-plaintext highlighter-rouge">Personal Settings</code> in BitBucket</li>
<li>Navigate to <code class="language-plaintext highlighter-rouge">SSH Keys</code> section, add the public key in there</li>
<li>Navigate to an exiting repository in BitBucket you want to clone</li>
<li>Copy either the SSH url or HTTPS url</li>
</ul>
</li>
<li>Configure Git
<ul>
<li>Start <code class="language-plaintext highlighter-rouge">Git Bash</code> command window</li>
<li>Run <code class="language-plaintext highlighter-rouge">git config --global http.sslVerify false</code> (required to connect via HTTPS instead of SSH)</li>
<li>Run <code class="language-plaintext highlighter-rouge">git config --global user.name "First Lastname"</code></li>
<li>Run <code class="language-plaintext highlighter-rouge">git config --global user.email "email@this.com"</code></li>
<li>Run <code class="language-plaintext highlighter-rouge">cd; mkdir repositories</code> to prepare a subfolder for just your git repos</li>
<li>Run <code class="language-plaintext highlighter-rouge">cd repositories</code></li>
<li>Clone existing repo by running <code class="language-plaintext highlighter-rouge">git clone <repo></code></li>
<li>Clone any other repos you wish to work on</li>
<li>Run <code class="language-plaintext highlighter-rouge">code .</code> to launch Visual Studio Code window with all the existing repo folders showing up for you</li>
</ul>
</li>
<li>Last Steps
<ul>
<li>Add/install VSCode extensions (Ansible, Jinja, etc)</li>
<li>Restart VSCode</li>
</ul>
</li>
<li>Done!</li>
</ul>John WadleighRecommendations for email, meetings, and general setup for maximizing VDI experienceCreate completely offline Ansible Tower bundle installation2021-03-25T00:00:00-07:002021-03-25T00:00:00-07:00https://ansiblejunky.com/blog/create-complete-offline-ansible-tower-bundle<p><img src="https://ansiblejunky.com/assets/images/ansible-short.png" alt="Ansible" class="full" /></p>
<blockquote>
<p>Updated on March, 2021 as Ansible Tower installation has changed since this blog was originally posted. This blog was updated using Ansible Tower 3.8.x but the next iteration of Ansible Tower will be changing again. So please read the warning in this blog!</p>
</blockquote>
<p>Ansible Tower installation package exists in two forms: (1) online setup, and (2) offline bundled setup. However the <em>bundled</em> package, still depends on OS-based repositories for specific OS dependencies. These packages cannot be included into the bundle because it would have to include all packages for each and every OS version (RHEL 8.0, 8.1, 8.3, etc). That would be redundant to duplicate all of that information. The assumption is that you have a mirror of these repositories that are accessible from your server infrastructure. Optimally, this is handled by Red Hat Satellite!</p>
<p>However, I have still received a number of requests for packaging a true offline Ansible Tower install. Some customers still have specific situations that for various reasons have their servers in complete “offline” mode with no access to internet <strong>and</strong> no access to an intranet mirror repository.</p>
<p>This article shows you how to prepare a true “offline” Ansible Tower installation that includes every dependent package for your specific OS version.</p>
<blockquote>
<p>WARNING: Ansible Tower installation package has changed since this blog was originally written. It is now much more complicated to attempt to gather all package dependencies. In fact I would argue it’s truly not worth it. In this day and age, it should always be possible to connect your servers to official OS repositories (RHEL for example) as these repos have been highly vetted and are used worldwide. If you do not have access to these repositories, you can still download the ISO and mount it to the servers. Therefore, this blog has been changed as a reflection of this opinion.</p>
</blockquote>
<h2 id="why">Why?</h2>
<p>It’s an important question to ask yourself! As stated above, Ansible Tower can be easily installed as long as you have the standard required repositories available. For example, for RHEL8:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> yum repolist
Updating Subscription Management repositories.
repo <span class="nb">id </span>repo name
rhel-8-for-x86_64-appstream-rpms Red Hat Enterprise Linux 8 <span class="k">for </span>x86_64 - AppStream <span class="o">(</span>RPMs<span class="o">)</span>
rhel-8-for-x86_64-baseos-rpms Red Hat Enterprise Linux 8 <span class="k">for </span>x86_64 - BaseOS <span class="o">(</span>RPMs<span class="o">)</span>
</code></pre></div></div>
<p>Again, it is not recommended anymore to prepare a completely offline installation package. However, below are some tips if you wish to attempt this.</p>
<blockquote>
<p>Warning… these steps are not complete and not guaranteed.</p>
</blockquote>
<h2 id="prepare-machine">Prepare Machine</h2>
<p>I use <a href="https://www.vagrantup.com/">Vagrant</a> for this since it’s the quickest way to startup a machine for testing purposes.</p>
<p>The following commands gets you started quickly:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Initialize vagrant box</span>
vagrant init generic/rhel8
<span class="c"># Ensure you have the latest box version</span>
vagrant box upgrade
<span class="c"># Start the machine</span>
vagrant up
<span class="c"># Login to the machine</span>
vagrant ssh
<span class="c"># Escalate to root access</span>
<span class="nb">sudo </span>su -
<span class="c"># Download latest version of Ansible Tower bundle package</span>
wget https://releases.ansible.com/ansible-tower/setup-bundle/ansible-tower-setup-bundle-latest.tar.gz
</code></pre></div></div>
<h2 id="prepare-repositories">Prepare Repositories</h2>
<p>As stated, the bundled package still needs access to OS specific repositories. The Vagrant box includes them as a result of registering <code class="language-plaintext highlighter-rouge">subscription-manager</code>. These are the required repositories:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> yum repolist
Updating Subscription Management repositories.
repo <span class="nb">id </span>repo name
rhel-8-for-x86_64-appstream-rpms Red Hat Enterprise Linux 8 <span class="k">for </span>x86_64 - AppStream <span class="o">(</span>RPMs<span class="o">)</span>
rhel-8-for-x86_64-baseos-rpms Red Hat Enterprise Linux 8 <span class="k">for </span>x86_64 - BaseOS <span class="o">(</span>RPMs<span class="o">)</span>
</code></pre></div></div>
<p>If you have these repositories, then you are good and can move on to the next step.</p>
<p>If you do not have access to these repositories for some reason, you will need to download the ISO for your OS version, push it to the vagrant box and mount it so you can create the repositories to point to the ISO content.</p>
<ul>
<li>Download ISO from Red Hat</li>
<li>
<p>Copy ISO file to machine using <code class="language-plaintext highlighter-rouge">scp</code> or <a href="https://github.com/invernizzi/vagrant-scp">vagrant-scp plugin</a></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c"># Copy ISO to machine</span>
vagrant scp rhel-8.3-x86_64-dvd.iso default:/tmp/rhel-8.3-x86_64-dvd.iso
</code></pre></div> </div>
</li>
<li>
<p>Mount and create repositories using similar instructions as noted in <a href="https://access.redhat.com/solutions/1355683">this solution</a></p>
<p>Note that for RHEL 8 ISO, there are actually two repositories (not just one) for AppStream and BaseOS so you would need to create two repo definitions under <code class="language-plaintext highlighter-rouge">/etc/yum.repos.d/</code> path.</p>
</li>
</ul>
<h2 id="prepare-tower">Prepare Tower</h2>
<p>Now let’s prepare the Ansible Tower installation folder.</p>
<ul>
<li>
<p>Unpack the Ansible Tower bundle</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">cd</span> ~
<span class="nb">tar</span> <span class="nt">-xvf</span> ansible-tower-setup-bundle-<span class="k">*</span>.tar.gz
</code></pre></div> </div>
</li>
<li>
<p>Modify the Inventory (optional)</p>
<p>We only need to set the passwords and nothing else! Set them to <code class="language-plaintext highlighter-rouge">password</code> because you will eventually package this folder and use it for future installs.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c"># Edit the inventory</span>
<span class="nb">cd </span>ansible-tower-setup-bundle-<span class="k">*</span>
vi inventory
<span class="c"># Set the following to 'password'</span>
<span class="nv">admin_password</span><span class="o">=</span><span class="s1">'password'</span>
<span class="nv">pg_password</span><span class="o">=</span><span class="s1">'password'</span>
<span class="nv">automationhub_admin_password</span><span class="o">=</span><span class="s1">'password'</span>
<span class="nv">automationhub_pg_password</span><span class="o">=</span><span class="s1">'password'</span>
</code></pre></div> </div>
</li>
</ul>
<h2 id="download-packages">Download Packages</h2>
<p>The packages we will need are listed in the <code class="language-plaintext highlighter-rouge">ansible-tower-setup-bundle-*/bundle/el8/base_packages.txt</code> file located within the Ansible Tower installation folder. Note that the file exists under both <code class="language-plaintext highlighter-rouge">el8</code> and <code class="language-plaintext highlighter-rouge">el7</code> folders as they require different packages.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> find <span class="nb">.</span> <span class="nt">-name</span> <span class="s2">"base_packages.txt"</span>
./bundle/el7/base_packages.txt
./bundle/el8/base_packages.txt
</code></pre></div></div>
<p>However the package names use <code class="language-plaintext highlighter-rouge">nevra</code> (name-epoch:version-release.architecture) format - see below example. While yum/dnf supports installing packages using the nevra format, we may not have all of these exact same versions available in our specific repositories.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="nb">tail</span> ./bundle/el8/base_packages.txt
xmlsec1-0:1.2.25-4.el8.x86_64
xmlsec1-openssl-0:1.2.25-4.el8.i686
xmlsec1-openssl-0:1.2.25-4.el8.x86_64
xorg-x11-font-utils-1:7.5-40.el8.x86_64
xorg-x11-server-utils-0:7.7-27.el8.x86_64
xz-0:5.2.4-3.el8.x86_64
xz-libs-0:5.2.4-3.el8.i686
xz-libs-0:5.2.4-3.el8.x86_64
zlib-0:1.2.11-17.el8.i686
zlib-0:1.2.11-17.el8.x86_64
</code></pre></div></div>
<p>So we will use a small Python script to help us extract the package name element of the nevra format and create a new package file that can be used by <code class="language-plaintext highlighter-rouge">yumdownloader</code> tool.</p>
<p>Let us first prepare the folder where the extra packages will be downloaded. Since I’m dealing with a RHEL8 system, I navigate inside the EL8 folder - you might need EL7.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Navigate again inside the tower bundle folder</span>
<span class="nb">cd</span> ~/ansible-tower-setup-bundle-<span class="k">*</span>
<span class="c"># Create new repo folder inside the bundle repos folder</span>
<span class="nb">mkdir </span>bundle/el8/repos/others
<span class="c"># Navigate to the new folder</span>
<span class="nb">cd </span>bundle/el8/repos/others
</code></pre></div></div>
<p>Save the following Python script as <code class="language-plaintext highlighter-rouge">convert.py</code> inside the <code class="language-plaintext highlighter-rouge">others</code> folder. It basically takes a filename as argument and outputs the names of the packages. It will help us convert the <code class="language-plaintext highlighter-rouge">base_packages.txt</code> to something usable for <code class="language-plaintext highlighter-rouge">yumdownloader</code>. (You can also elect to use <a href="https://github.com/ihiji/version_utils">version_utils</a>)</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span><span class="o"><</span><span class="mi">2</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"No argument provided. Exiting."</span><span class="p">)</span>
<span class="nb">exit</span><span class="p">()</span>
<span class="n">filename</span><span class="o">=</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
<span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">f</span><span class="p">:</span>
<span class="n">pkgname</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">'-[0-9]*:'</span><span class="p">,</span> <span class="n">line</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">pkgname</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
</code></pre></div></div>
<p>Now we just run it all and magically we will be downloading all the necessary packages into our <code class="language-plaintext highlighter-rouge">others</code> folder.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Convert base_packages.txt to base.txt with only package name</span>
<span class="nb">cd</span> ~/ansible-tower-setup-bundle-<span class="k">*</span>/bundle/el8/repos/others
<span class="c"># Convert the base packages using Python script</span>
python3 convert.py ../../base_packages.txt <span class="o">></span> base.txt
<span class="c"># Remove exception with `dejavu-lgc-sans-fonts` and `sshpass` packages not available in the repositories</span>
<span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"dejavu-lgc-sans-fonts"</span> base.txt | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"sshpass"</span> <span class="o">></span> base_clean.txt
<span class="c"># Install packages to help us download locally and create a local repo</span>
yum <span class="nb">install </span>yum-utils createrepo
<span class="c"># Download packages to local folder</span>
yumdownloader <span class="si">$(</span><span class="nb">cat</span> ../base_clean.txt<span class="si">)</span>
<span class="c"># WARNING - See warning statement below...</span>
<span class="c"># Prepare local folder repository with metadata using `createrepo` tool</span>
createrepo <span class="nb">.</span>
</code></pre></div></div>
<blockquote>
<p>WARNING: In addition to the <code class="language-plaintext highlighter-rouge">base_packages.txt</code> and the downloading all dependencies for them, you may need to additionally download dependencies for the packages inside the <code class="language-plaintext highlighter-rouge">ansible-tower-dependencies</code> folder (try using <code class="language-plaintext highlighter-rouge">repoquery --resolve --requires *.rpm</code>). I noticed the <code class="language-plaintext highlighter-rouge">setup.sh</code> installer script creates a temporary yum repo that points to this folder to be able to install the Ansible engine required for installing Ansible Tower. Additionally, the <code class="language-plaintext highlighter-rouge">setup.sh</code> calls the installation playbook that performs a couple commands to enable dnf modules (nginx and postgresql) and these also required access to the OS repositories. So this is where the problems start happening and why I marked this blog with a big warning in the beginning! It gets complex and will only get worse in the following versions. So save yourself the trouble and simply attach the OS repositories. Do not attempt to find and download all package dependencies!</p>
</blockquote>
<p>Now that the local repo has been prepared, we can have the Ansible Tower installer reference it during installation by adding it to the repo template file which is used to generate a yum repo file for use during installation.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ansible-tower-setup-bundle-<span class="k">*</span>
<span class="nb">cat</span> <span class="o"><<</span> <span class="no">EOF</span><span class="sh"> >> ./roles/repos_el/templates/tower_bundle.j2
[others]
name=Others
baseurl=file://{{ bundle_install_folder }}/others
enabled=1
gpgcheck=0
</span><span class="no">EOF
</span></code></pre></div></div>
<h2 id="re-package-the-bundle">Re-package the bundle</h2>
<p>That’s it! Now you can re-package your new bundle and bring it to your Data Center or wherever and simply install Ansible Tower in a completely offline situation.</p>
<blockquote>
<p>Remember that this bundle will only work with the specific OS and version you used in this process! For any other OS version you will need to do this again.</p>
</blockquote>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># navigate to home folder</span>
<span class="nb">cd</span> ~
<span class="c"># create tar ball of the new bundle</span>
<span class="nb">tar</span> <span class="nt">-czvf</span> ansible-tower-bundle-offline.tar.gz ansible-tower-setup-bundle-<span class="k">*</span>
</code></pre></div></div>
<p>If you want to test the installation, just disable all of your repos before attempting the installation. You can additionally pre-install the downloaded packages.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Disable all repositories</span>
yum-config-manager <span class="nt">--disable</span> <span class="se">\*</span>
<span class="c"># Optional - install the packages manually on target machine</span>
<span class="nb">cd </span>bundle/el8/repos/other/
yum <span class="nt">--disablerepo</span><span class="o">=</span><span class="k">*</span> localinstall <span class="nt">--skip-broken</span> <span class="k">*</span>.rpm
</code></pre></div></div>
<p>However, for a proper test, you should destroy your <code class="language-plaintext highlighter-rouge">vagrant</code> machine and recreate it and use only your new bundled package to install Tower.</p>
<p>Good luck!</p>John WadleighPrepare an offline Ansible Tower installation.Ansible 101 - Standards2021-02-24T00:00:00-08:002021-02-24T00:00:00-08:00https://ansiblejunky.com/blog/ansible-101-standards<p>Collection of Ansible standards based on my experience.</p>
<!--more-->
<p><img src="https://ansiblejunky.com/assets/images/ansible-short.png" alt="Ansible" class="full" /></p>
<p>As with most languages there should be some recommended standards to live by. This blog provides a list of the common standards or best practices to live by when writing Ansible code. This is an unofficial list of standards based on my personal experience with hundreds of customers and my own development practices I have used over the years. I have a masters degree in Computation from Oxford University working with programming theory and in the last 20 years in the technology industry I have seen my fair share of code that lacks any standards at all.</p>
<p>In other words, these standards are tried and trusted - they should work well as a starting point. Feel free to adapt them to your own situation. Standards are commonly adapted to specific situations.</p>
<ul id="markdown-toc">
<li><a href="#general" id="markdown-toc-general">General</a></li>
<li><a href="#projects" id="markdown-toc-projects">Projects</a></li>
<li><a href="#inventory" id="markdown-toc-inventory">Inventory</a></li>
<li><a href="#development" id="markdown-toc-development">Development</a></li>
<li><a href="#playbooks" id="markdown-toc-playbooks">Playbooks</a></li>
<li><a href="#roles" id="markdown-toc-roles">Roles</a></li>
<li><a href="#variables" id="markdown-toc-variables">Variables</a></li>
<li><a href="#conditionals-and-return-status" id="markdown-toc-conditionals-and-return-status">Conditionals and Return Status</a></li>
<li><a href="#formatting" id="markdown-toc-formatting">Formatting</a></li>
<li><a href="#collections" id="markdown-toc-collections">Collections</a></li>
</ul>
<h2 id="general">General</h2>
<blockquote>
<p>Complexity Kills Productivity</p>
</blockquote>
<p>Ansible tools have always been built around this concept. The tools have simplified complex operations by hiding the complexity (underlying Python code) from the end user! Strive for a similar approach with your Ansible Playbooks and Roles by simplifying what you automate.</p>
<p>Follow the UNIX principle of <a href="https://en.wikipedia.org/wiki/Unix_philosophy#:~:text=The%20Unix%20philosophy%20is%20documented,by%20adding%20new%20%22features%22.">do one thing, and one thing well</a>.</p>
<blockquote>
<p>Optimize for Readability</p>
</blockquote>
<p>If done properly, it can be the documentation of your workflow automation.</p>
<blockquote>
<p>Think Declaritively</p>
</blockquote>
<p>Ansible is a desired state engine by design. If you’re trying to “write code” in your plays and roles, you’re setting yourself up for failure. Playbooks were never meant to be for programming.</p>
<blockquote>
<p>Version Control Everything</p>
</blockquote>
<p>Always use a version control system (VCS) with your Ansible content</p>
<blockquote>
<p>Start Simple and Build as Needed</p>
</blockquote>
<ul>
<li>Start with a basic playbook and static inventory</li>
<li>Refactor and modularize (using Roles, etc) later</li>
<li>Just because you can create a complex structure doesn’t mean you should</li>
<li>Focus on testing a simple concept first</li>
</ul>
<blockquote>
<p>Forget YAML Document Markers</p>
</blockquote>
<p>You might have heard that YAML documents should begin with <code class="language-plaintext highlighter-rouge">---</code> and end with <code class="language-plaintext highlighter-rouge">...</code>. No, they don’t! Just forget these document markers and move on. Your YAML files will work perfectly without them. More than likely they will only cause your repositories to have some YAML files with the start marker <code class="language-plaintext highlighter-rouge">---</code>, some with both start and end, and some with neither because multiple developers have worked on that repository.</p>
<p>These document markers are part of the YAML 1.2 Schema definition and are <em>only</em> needed when you either want to define directives (not needed with Ansible ever!) or you want to define multiple YAML documents within the same file (also never needed by Ansible ever!).</p>
<p>For more information reference <a href="https://yaml.org/spec/1.2/spec.html">YAML 1.2 Schema Specifications</a>.</p>
<blockquote>
<p>Consistent naming standards</p>
</blockquote>
<p>Have <em>consistent</em> naming standards that can easily be followed. These apply to files, folders, inventory group names, variable names, role names, repository names, and objects defined within Ansible Automation Platform. The following rules are a good recommendation to start with.</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">*.yml</code> as the file extension for YAML files</li>
<li><code class="language-plaintext highlighter-rouge">*.j2</code> as the file extension for Jinja files</li>
<li>Use lowercase <code class="language-plaintext highlighter-rouge">a-z</code></li>
<li>Use underscore <code class="language-plaintext highlighter-rouge">_</code> as a separator, not dashes</li>
<li>Do not use whitespace as a separator</li>
<li>Terse, one word if possible, using underscores if necessary</li>
<li>Human-meaningful</li>
<li>Variables should be prefixed with the related application, Ansible Role, etc, to bring context and avoid with the name of the role. For example:</li>
</ul>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apache_port</span><span class="pi">:</span> <span class="m">80</span>
<span class="na">apache_path</span><span class="pi">:</span> <span class="s">/opt/apache</span>
</code></pre></div></div>
<ul>
<li>Use appropriate naming standards for files</li>
</ul>
<p>Files are typically sorted alphabetically within IDE tools and simply from the terminal shell and having a good naming standard can help developers read and organize them. For example, name your files using the <em>noun</em> as the prefix and <em>action</em> as the suffix: <code class="language-plaintext highlighter-rouge"><noun>_<action>.<extension></code></p>
<p>This ensures the files are listed and organized by product/application first and then by the action that is supported. Keeping your files organized in this manner makes it much easier to scan a Project/Role/etc to determine the product and all supported actions. For example:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apache_download.yml
apache_install.yml
apache_manage.yml
nginx_install.yml
nginx_manage.yml
</code></pre></div></div>
<ul>
<li>Name repositories based on their content</li>
</ul>
<p>The name of the version control repository should be short but answers the following two high level questions: (1) Is this containing an Ansible Project or Ansible Role or Ansible Collection? (2) What is the general purpose of the content?</p>
<p>For example an Ansible Role repository for managing Apache software installations could be named: <code class="language-plaintext highlighter-rouge">ansible_role_apache</code>. Notice we still only use lowercase/underscore in the name of the repository but we also try to answer the 2 questions. This helps developers search for and use the appropriate repository for their work. Reference hundreds of examples on Ansible Galaxy website.</p>
<p>Similarly, an Ansible Project repository that handles infrastructure provisioning on Azure platform could be named <code class="language-plaintext highlighter-rouge">ansible_project_azure_provisioning</code>.</p>
<p>Do NOT name your repositories with a mix of underscore and dashes such as <code class="language-plaintext highlighter-rouge">ansible-project_azure_provisioning</code>, as this is just simply confusing and frustrating when developers start to clone and reference repositories.</p>
<blockquote>
<p>Always name Tasks</p>
</blockquote>
<p>Tasks should <strong>always</strong> be named using the <code class="language-plaintext highlighter-rouge">name:</code> parameter, no exceptions. In its output, Ansible shows you the name of each task it runs. The goal of writing Ansible is that it’s readable, not only for the developers but for those reading the output of a specific run.</p>
<p>When naming a Task it should be an English sentence describing the purpose of the Task. It should be terse, descriptive, with spaces allowed. Do not use comments as a replacement for the actual name of the task!</p>
<p>An acceptable example using comments and naming:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># This is an temporary workaround and should be replaced soon.</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Ensure file exists</span>
<span class="na">file</span><span class="pi">:</span>
<span class="na">path</span><span class="pi">:</span> <span class="s">/opt/test</span>
<span class="na">state</span><span class="pi">:</span> <span class="s">touch</span>
</code></pre></div></div>
<blockquote>
<p>Always mention the state</p>
</blockquote>
<p>For many modules, the <code class="language-plaintext highlighter-rouge">state</code> parameter is optional. Different modules have different default settings for <code class="language-plaintext highlighter-rouge">state</code>, and some modules support several <code class="language-plaintext highlighter-rouge">state</code> settings. Explicitly setting <code class="language-plaintext highlighter-rouge">state: present</code> or <code class="language-plaintext highlighter-rouge">state: absent</code> brings consistency and clarity to your Playbooks and Roles.</p>
<blockquote>
<p>Clean up your debugging tasks</p>
</blockquote>
<p>Make them optional with the verbosity parameter so they’re only displayed when they are wanted.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Show information about the current state</span>
<span class="na">debug</span><span class="pi">:</span>
<span class="na">msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">This</span><span class="nv"> </span><span class="s">always</span><span class="nv"> </span><span class="s">displays"</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Debug statement that conditionally shows</span>
<span class="na">debug</span><span class="pi">:</span>
<span class="na">msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">This</span><span class="nv"> </span><span class="s">only</span><span class="nv"> </span><span class="s">displays</span><span class="nv"> </span><span class="s">with</span><span class="nv"> </span><span class="s">ansible-playbook</span><span class="nv"> </span><span class="s">-vv+"</span>
<span class="na">verbosity</span><span class="pi">:</span> <span class="m">2</span>
</code></pre></div></div>
<blockquote>
<p>Define testing strategies to increase reliability and reduce costs</p>
</blockquote>
<p>Testing increases reliability and reduces costs as it can catch problems early. Consider implementing various testing strategies but do not attempt to build too much too soon - simply introduce tests as needed. The following are some of the common testing strategies:</p>
<ul>
<li>smoke testing</li>
<li>sanity testing</li>
<li>unit testing</li>
<li>integration testing</li>
<li>syntax testing</li>
<li>runtime testing</li>
</ul>
<blockquote>
<p>Do something today that gets you closer to your long term goal</p>
</blockquote>
<p>There are two common mistakes people make when automating:</p>
<ul>
<li><strong>Attempt to do everything at once</strong>. Always focus on creating smaller and manageable pieces of work that produce perhaps a portion of the total functionality or feature you want to implement. This allows something to still be delivered to the end user but without the long time commitment. The smaller the delivery, the easier it is to manage it, reduce errors, and produce constant stability and trust in the pipeline and end product.</li>
<li><strong>Expect that it needs to be perfect</strong>. Everyone gets caught saying “We don’t have time to do that right now” but really they mean “We don’t have time to do [all of] that right now”. Often you have time to do something small right now that gets you a little closer to “all of that”.</li>
</ul>
<blockquote>
<p>Use smoke tests when starting services</p>
</blockquote>
<p>Don’t just start the services - test them to ensure they are ready and available. Smoke tests can be performed using various methods such as with the <code class="language-plaintext highlighter-rouge">uri</code> module.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">check for proper response</span>
<span class="na">uri</span><span class="pi">:</span>
<span class="na">url</span><span class="pi">:</span> <span class="s">http://localhost/myapp</span>
<span class="na">return_content</span><span class="pi">:</span> <span class="s">yes</span>
<span class="na">register</span><span class="pi">:</span> <span class="s">result</span>
<span class="na">until</span><span class="pi">:</span> <span class="s1">'</span><span class="s">"Hello</span><span class="nv"> </span><span class="s">World"</span><span class="nv"> </span><span class="s">in</span><span class="nv"> </span><span class="s">result.content'</span>
<span class="na">retries</span><span class="pi">:</span> <span class="m">10</span>
<span class="na">delay</span><span class="pi">:</span> <span class="m">1</span>
</code></pre></div></div>
<blockquote>
<p>Refrain from creating arbitrary delays between tasks</p>
</blockquote>
<p>The <code class="language-plaintext highlighter-rouge">pause</code> and <code class="language-plaintext highlighter-rouge">wait_for</code> modules are commonly used to create an arbitrary delay between tasks. It is always better to wait for a specific condition to happen (file exists, file or folder does not exist, registry entry created, etc) than to wait for an arbitrary amount. The problem with selecting an arbitrary time for an operation to complete is that the total time required is typically not a fixed amount of time as processes are dependent on many things (CPU, memory, dependent processes, etc) and something that completed in 30 seconds today may require 60 seconds or more when executed on Production environment or perhaps simply next year on the same server. As a result of this factor, often developers select an extrordanarily large amount of time to compenate for the possible variance. This becomes a different problem now that we are delaying the execution and therefore completion of tasks by a much large amount of time than necessary. Additionally, this is only against a single target server - if we scale this up and run our Playbook against 1000 servers the impact is much greater. Of course using more Ansible forks can reduce that problem but each fork demands more memory. It simply is not a great solution and considered a “lazy” workaround to a problem.</p>
<p>Examples of arbitrary delays:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Pause for 5 minutes to build app cache</span>
<span class="na">pause</span><span class="pi">:</span>
<span class="na">minutes</span><span class="pi">:</span> <span class="m">5</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Sleep for 300 seconds and continue with play</span>
<span class="na">wait_for</span><span class="pi">:</span>
<span class="na">timeout</span><span class="pi">:</span> <span class="m">300</span>
<span class="na">delegate_to</span><span class="pi">:</span> <span class="s">localhost</span>
</code></pre></div></div>
<blockquote>
<p>Use <code class="language-plaintext highlighter-rouge">command</code> modules sparingly</p>
</blockquote>
<ul>
<li>Always seek out a module first</li>
<li>Use the run command modules like shell and command as a last resort</li>
<li>The command module is generally safer</li>
<li>The shell module should only be used for I/O redirect</li>
</ul>
<p>For example, here we could be lazy and use the command module:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">add user</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">useradd appuser</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">install apache</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">yum install httpd</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">start apache</span>
<span class="na">shell</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s">service httpd start && chkconfig httpd on</span>
</code></pre></div></div>
<p>However, a better approach is using the available modules:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">add user</span>
<span class="na">user</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">appuser</span>
<span class="na">state</span><span class="pi">:</span> <span class="s">present</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">install apache</span>
<span class="na">yum</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">httpd</span>
<span class="na">state</span><span class="pi">:</span> <span class="s">latest</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">start apache</span>
<span class="na">service</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">httpd</span>
<span class="na">state</span><span class="pi">:</span> <span class="s">started</span>
<span class="na">enabled</span><span class="pi">:</span> <span class="s">yes</span>
</code></pre></div></div>
<blockquote>
<p>Create internal landing site for Ansible Automation</p>
</blockquote>
<ul>
<li>Create a style guide for developers</li>
<li>Use <code class="language-plaintext highlighter-rouge">mkdocs</code> for a super simple way to spin up a landing site</li>
<li>Include some of the following concepts:
<ul>
<li>Ansible standards</li>
<li>Onboarding instructions for other teams</li>
<li>Release process</li>
<li>Vision, goals, responsibilities</li>
</ul>
</li>
<li>Produces consistency in things like
<ul>
<li>Naming of Tasks, Plays, Variables and Roles</li>
<li>Directory Layouts</li>
<li>Whitespace</li>
<li>Tagging</li>
</ul>
</li>
</ul>
<blockquote>
<p>Enforce the style using tools</p>
</blockquote>
<p>Style guides are great but they don’t ensure that your developers actually follow these rules. In fact, it’s guaranteed they won’t! So use linting tools such as <code class="language-plaintext highlighter-rouge">ansible-lint</code>, a command-line static analysis tool that checks for behaviour that could be improved.</p>
<h2 id="projects">Projects</h2>
<blockquote>
<p>Standard Project Structure</p>
</blockquote>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">ansible_project_myapp</span>
<span class="s">├── collections</span>
<span class="s">│ └── requirements.yml</span>
<span class="s">├── roles</span>
<span class="s">│ └── requirements.yml</span>
<span class="s">│ ├── myapp</span>
<span class="s">│ │ ├── tasks</span>
<span class="s">│ │ │ └── main.yml</span>
<span class="s">│ │ └── ...</span>
<span class="s">│ ├── nginx</span>
<span class="s">│ │ └── ...</span>
<span class="s">│ └── proxy</span>
<span class="s">│ └── ...</span>
<span class="s">├── inventory</span>
<span class="s">│ ├── group_vars</span>
<span class="s">│ │ └── web.yml</span>
<span class="s">│ ├── host_vars</span>
<span class="s">│ │ └── db1.yml</span>
<span class="s">│ └── hosts</span>
<span class="s">├── myapp_configure.yml</span>
<span class="s">├── myapp_provision.yml</span>
<span class="s">└── site.yml</span>
</code></pre></div></div>
<blockquote>
<p>Separate provisioning from deployment and configuration</p>
</blockquote>
<p>Build separate Playbooks to handle each of these major operations and string them together using the <code class="language-plaintext highlighter-rouge">site.yml</code> Playbook.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">$ cat site.yml</span>
<span class="nn">---</span>
<span class="pi">-</span> <span class="na">import_playbook</span><span class="pi">:</span> <span class="s">myapp_provision.yml</span>
<span class="pi">-</span> <span class="na">import_playbook</span><span class="pi">:</span> <span class="s">myapp_configure.yml</span>
</code></pre></div></div>
<h2 id="inventory">Inventory</h2>
<blockquote>
<p>Use dynamic inventory with clouds</p>
</blockquote>
<p>With cloud providers and other systems that maintain canonical lists of your infrastructure, use <a href="https://docs.ansible.com/ansible/latest/user_guide/intro_dynamic_inventory.html#intro-dynamic-inventory">dynamic inventory</a> to retrieve those lists instead of manually updating static inventory files. With cloud resources, you can use tags to differentiate production and staging environments.</p>
<blockquote>
<p>Group hosts for easier inventory selection and less conditional tasks – the more groups the better.</p>
</blockquote>
<p>Groups are powerful in the Ansible inventory and a Playbook can easily target a group of hosts using the group name which can be freely defined. Consider using naming standards when creating group names so there is some level of consistency across your entire inventory. Try to use human-meaningful group names. Use the <code class="language-plaintext highlighter-rouge">group_vars</code> folder to define the variables specific to each of the groups. This allows for host names to change, but group names can stay consistent which means your Playbooks do not need to be changed. Reducing code changes means more stability.</p>
<blockquote>
<p>Build single source of truth</p>
</blockquote>
<p>Use a single source of truth if you have it – even if you have multiple sources, Ansible can unify them. The power of the Inventory structure is that its simply a folder structure that manages multiple platforms, environments, host groups, etc. Additionally with Dynamic Inventory and using Inventory Plugins for various platforms (VMware, AWS, Azure, Google, etc) we can build a single source of truth across your entire hybrid cloud landscape.</p>
<blockquote>
<p>Create separate repository for your Inventory</p>
</blockquote>
<p>Similar to the advantages of creating an Ansible Role in a separate repository (instead of inside the Project repository in the <code class="language-plaintext highlighter-rouge">roles/</code> folder) consider moving your Inventory to a separate repository!</p>
<p>It’s not required to keep the Inventory in the same repository as the Project that contains your Playbooks.</p>
<p>Advantages:</p>
<ul>
<li>Manage your inventory separate from your Playbooks, Roles, etc.</li>
<li>Inventory structure can have its own life cycle in a version control system
<ul>
<li>Changes to a Playbook or Role does not affect your Inventory since it sits in a different repository</li>
</ul>
</li>
<li>Single source of truth for your inventory in one repository</li>
<li>Control access to the repository content separately from other Ansible content</li>
<li>Easily load the inventory within Ansible Tower however you want</li>
</ul>
<blockquote>
<p>Only use vault encrypted strings</p>
</blockquote>
<p>Never encrypt an entire file within the inventory. It makes it difficult to understand or search the contents of the file using typical text editors or develpment tools. To use encrypted strings in your inventory file it will need to be in YAML format instead of the traditional INI format.</p>
<blockquote>
<p>Consider using symbolic links to manage your shared variables</p>
</blockquote>
<p>Read through <a href="https://www.digitalocean.com/community/tutorials/how-to-manage-multistage-environments-with-ansible">this article from Digital Ocean</a> that talks about why symbolic links are useful as an organizational tool for handling shared variables across platforms, environments, and so on. Do not be afraid to use symlinks within your repository.</p>
<p><strong>Advanced Tip</strong>: Also consider using <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_advanced_syntax.html#yaml-anchors-and-aliases-sharing-variable-values">anchors/aliases</a> as yet another tool for managing your inventory variables.</p>
<blockquote>
<p>Remember, files can be folders too.</p>
</blockquote>
<p>Ansible reads the <code class="language-plaintext highlighter-rouge">group-vars/</code> folder contents and allows files to also be folders. This means the <code class="language-plaintext highlighter-rouge">web.yml</code> group file can also be a folder with the same name <code class="language-plaintext highlighter-rouge">web</code>. In this case Ansible loads <em>all</em> of the files in that group folder. This is useful when doing more advanced inventory management techniques.</p>
<blockquote>
<p>Avoid defining host variables</p>
</blockquote>
<p>Always define variables for a group. There may be exceptions that highlight the fact that the host is more of a unicorn host and not aligned to any standard operating environment.</p>
<h2 id="development">Development</h2>
<blockquote>
<p>Reconsider Developing Plugins and Modules</p>
</blockquote>
<ul>
<li>Remember, complexity kills productivity!</li>
<li>Just because you <em>can</em>, doesn’t mean you <em>should</em></li>
<li>Ansible modules such as <em>uri</em> help reduce the need for developing custom modules</li>
<li>It will always be easier to maintain only a YAML solution</li>
<li>Adding a Python module adds complexity, increases maintanence costs, increases technical debt</li>
<li>Consider building a focused Ansible Role instead of a custom module</li>
</ul>
<blockquote>
<p>Good modules are user-centric</p>
</blockquote>
<p>If you must create a custom module, please use these guidelines.</p>
<ul>
<li>Modules are how Ansible balances simple and powerful</li>
<li>They implement common automation tasks for a user</li>
<li>They make easy tasks easier and complicated tasks possible</li>
<li>They abstract users from having to know the details to get things done</li>
<li>They are <em>not</em> one-to-one mapping of an API or command line tool interface. This is why you should not auto-generate your modules</li>
<li>They are <em>not</em> monolithic “does everything” modules that are hard to understand and complicated to use correctly</li>
</ul>
<h2 id="playbooks">Playbooks</h2>
<blockquote>
<p>Plays should do nothing more than include a list of roles</p>
</blockquote>
<p>Plays attempt to answer the <code class="language-plaintext highlighter-rouge">Where?</code> and the <code class="language-plaintext highlighter-rouge">What?</code>. We answer the <code class="language-plaintext highlighter-rouge">Where?</code> question by defining the managed nodes that we are targeting using the <code class="language-plaintext highlighter-rouge">hosts:</code>. We answer the <code class="language-plaintext highlighter-rouge">What?</code> question by providing the workflow of roles which state what we are trying to do. It should be simple and readable, so just a workflow of Roles in a specific order.</p>
<p>Try to prevent writing Tasks in your Plays, if it’s not the <code class="language-plaintext highlighter-rouge">include_role</code> task. This logic or functionality should be moved to an appropriate Role.</p>
<blockquote>
<p>Use Playbooks instead of Workflow Templates when possible</p>
</blockquote>
<p>Since a Playbook can contain one or more Plays, it’s literally defining a workflow. A Playbook represents a workflow in code form and allows full flexibility (conditionals, tasks, tags, pre-tasks, etc). However an Ansible Tower Workflow Job Template is a graphical form of the workflow with only success/fail paths from any single node. If you are focused on “Everything as Code”, you should try to always write Playbooks. Of course one can write code that creates the Ansible Tower Job Templates for each node and then the Workflow Job Template with all required information, but why do all of this when it can be a nice beautiful Playbook that is readable and easily usable.</p>
<p>There is a good exception to this rule when you need the graphical representation for demonstration purposes or other visual needs. Some have argued to use the Workflow Job Template because they already have the Job Templates created and tested and they only want to create an orchestration of these Job Templates using a Workflow Job Template.</p>
<blockquote>
<p>Use <code class="language-plaintext highlighter-rouge">include_role</code> instead of the traditional <code class="language-plaintext highlighter-rouge">roles:</code> section</p>
</blockquote>
<p>Traditionally a Playbook contains a <code class="language-plaintext highlighter-rouge">roles:</code> section to list the dependent roles and explicitly load them in that same order. Using the <code class="language-plaintext highlighter-rouge">tasks:</code> section and defining <code class="language-plaintext highlighter-rouge">include_role</code> tasks allows for more control with the order, ability to add <code class="language-plaintext highlighter-rouge">when</code> clauses, etc.</p>
<blockquote>
<p>Consider using module defaults groups when heavily using cloud modules</p>
</blockquote>
<p><a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_module_defaults.html#module-defaults-groups">Module defaults groups</a> is a feature that was added in Ansible 2.7 that groups together modules that share common sets of parameters. This makes it easier to author playbooks making heavy use of API-based modules such as cloud modules.</p>
<p>For example, in a playbook you can set module defaults for whole groups of modules, such as setting a common AWS region.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span> <span class="s">localhost</span>
<span class="na">module_defaults</span><span class="pi">:</span>
<span class="s">group/aws</span><span class="pi">:</span>
<span class="na">region</span><span class="pi">:</span> <span class="s">us-west-2</span>
<span class="na">tasks</span><span class="pi">:</span>
<span class="c1"># Required parameter 'region' is defaulted</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Get info</span>
<span class="na">aws_s3_bucket_info</span><span class="pi">:</span>
<span class="c1"># Required parameter 'region' is defaulted</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Get info</span>
<span class="na">ec2_ami_info</span><span class="pi">:</span>
<span class="na">filters</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">RHEL*7.5*'</span>
</code></pre></div></div>
<h2 id="roles">Roles</h2>
<blockquote>
<p>Roles should answer the question <code class="language-plaintext highlighter-rouge">How?</code></p>
</blockquote>
<p>Recall that a Playbook answers the questions of <code class="language-plaintext highlighter-rouge">Where?</code> and <code class="language-plaintext highlighter-rouge">What?</code>, but it’s the Role that answers the question of <code class="language-plaintext highlighter-rouge">How?</code>. How are we implementing this specific functionality? The answer is in the code written in the Task files, Variable files, Template files, and so on. Normally a new user of the Role should not ever need to care about <em>how</em> the Role was implemented, but rather they want to know <em>how</em> to use it. Make the Role easy to use and consume, by answering <em>how</em> to use it, and ensuring the complex bits are somewhere else in the Role (available to those that are curious but not in the way).</p>
<blockquote>
<p>Keep roles self contained and focused</p>
</blockquote>
<ul>
<li>Think about the full life-cycle of a service, microservice or container — not a whole stack or environment</li>
<li>Roles should be loosely-coupled — limit hard dependencies on other roles or external variables</li>
<li>Roles should avoid including tasks from other roles when possible</li>
<li>Roles should NOT perform <code class="language-plaintext highlighter-rouge">include_role</code> to include another external role</li>
<li>Variables needed by the Role should all be defined within the Role (using <code class="language-plaintext highlighter-rouge">defaults/main.yml</code> or <code class="language-plaintext highlighter-rouge">vars/main.yml</code>)</li>
<li>Pass variables to the role when calling <code class="language-plaintext highlighter-rouge">include_role:</code></li>
</ul>
<blockquote>
<p>Roles should be environment independent</p>
</blockquote>
<p>Do not place environment specific variables or values inside an Ansible Role, as the purpose of a Role is to be usable against any environment or even platform or possibly distribution. Flexibility is the key to a successful Role that can be easily used. Remember, environment specific values are loaded from your Inventory structure and these values are passed into the Role to set or override the default values.</p>
<blockquote>
<p>Roles should be more than a single task</p>
</blockquote>
<p>It’s common to find an implementation where there are over 20 Roles defined in separate repositories and each Role has only the <code class="language-plaintext highlighter-rouge">tasks/main.yml</code> defined and often containing only 1 task. This is an overly complex solution that could be simplified.</p>
<p>Before creating a Role, think about the overall design and purpose. Try to group similar functionality (install, configure, etc) into a single Role when it makes sense to do so. For example, decide whether you should create one Role to handle the install and configuration or two Roles (<code class="language-plaintext highlighter-rouge">apache_install</code> and <code class="language-plaintext highlighter-rouge">apache_configure</code>). When Roles have to manage similar variables they should be considered as one single Role using multiple Task files (<code class="language-plaintext highlighter-rouge">tasks/apache_install.yml</code> and <code class="language-plaintext highlighter-rouge">tasks/apache_configure</code>).</p>
<blockquote>
<p>Automate the testing of your roles</p>
</blockquote>
<p>Use <a href="https://molecule.readthedocs.io/en/latest/">Molecule</a>, a testing framework designed to aid in the development and testing of Ansible Roles. Note that <code class="language-plaintext highlighter-rouge">ansible-lint</code> can be run as part of your Molecule test runs.</p>
<blockquote>
<p>Roles should never contain vaulted information</p>
</blockquote>
<p>As Roles should be environment-independent, vaulted information tends to be environment-specific. Often Roles are built and then tested against target machines which can result in the vaulted information being used as default values for variables. Instead, set variable defaults to cleartext such as ‘password’ and allow the true vaulted secret to override these defaults when passed from the command line or using Ansible Tower credentials.</p>
<blockquote>
<p>Roles should define public variables using <code class="language-plaintext highlighter-rouge">defaults/main.yml</code></p>
</blockquote>
<p>For those variables that can or should be overridden by the user of the Role, they should be defined with default values in the <code class="language-plaintext highlighter-rouge">defaults/main.yml</code> file. This is generally where new users should look in order to quickly understand what can be overrriden or customized with a Role. When variables are defined in here, they have the lowest precedence and are easily overridden by other variables defined elsewhere - that is the intention as they are only defining the default value.</p>
<blockquote>
<p>Roles should define private variables using <code class="language-plaintext highlighter-rouge">vars/main.yml</code></p>
</blockquote>
<p>Any variables that are needed by the Role to perform the tasks and are <strong>not</strong> needed by anyone outside the Role should be defined using <code class="language-plaintext highlighter-rouge">vars/main.yml</code>. Remember, we are trying to hide the details of <em>how</em> to do the tasks. The typical user of the Role does not need to know or understand these details and variables so we place them in a different location.</p>
<blockquote>
<p>Roles should use variables instead of hardcoded strings</p>
</blockquote>
<p>Even if the string is referenced once, it is still better to define a variable and have that variable added to the <code class="language-plaintext highlighter-rouge">vars/main.yml</code> or <code class="language-plaintext highlighter-rouge">defaults/main.yml</code> as it’s much easier to scan and maintain these variables in a central location. More often one forgets about the hardcoded strings and it causes a runtime error or misconfiguration.</p>
<blockquote>
<p>Roles do not always need the <code class="language-plaintext highlighter-rouge">tasks/main.yml</code> tasks file</p>
</blockquote>
<p>A default Role file structure contains the <code class="language-plaintext highlighter-rouge">tasks/main.yml</code> as the common entry point for a Role. This is used when referencing a Role using <code class="language-plaintext highlighter-rouge">roles:</code> section in a Playbook or by simply including a Role using <code class="language-plaintext highlighter-rouge">include_role:</code> or <code class="language-plaintext highlighter-rouge">import_role:</code> tasks. However in some cases it might not make sense to have a “default” behavior for a Role. For example, the Role might support “install” and “configure”, in which case you can create the Role this way:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defaults/
main.yml
vars/
main.yml
tasks/
configure.yml
install.yml
</code></pre></div></div>
<p>And then call the Role using a specific task:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">include_role</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">apache</span>
<span class="na">tasks_from</span><span class="pi">:</span> <span class="s">configure.yml</span>
</code></pre></div></div>
<p>This is perfectly acceptable and in fact it forces the user of the Role to select the function (using the task file) instead of guessing or checking what behavior will happen by default. In many cases, I prefer this style for Roles. It’s good to know you have the option to design it this way as well.</p>
<blockquote>
<p>Consider using module defaults when using a specific module heavily</p>
</blockquote>
<p>Using <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_module_defaults.html">module defaults</a> can greatly simplify your code by eliminating duplication of module parameters. A great example is when building a Role that heavily uses the <code class="language-plaintext highlighter-rouge">uri</code> module to make REST API calls to an external service. Instead of defining the same parameters for each Task, set the defaults and then override or set whatever you need in each specific Task.</p>
<h2 id="variables">Variables</h2>
<blockquote>
<p>Separate logic (tasks) from variables to reduce repetitive patterns and provide flexibility.</p>
</blockquote>
<p>The code here shows embedded parameter values and a repetitive home directory value pattern in multiple places. This works but could be more flexible and maintainable.</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Clone student lesson app for a user</span>
<span class="na">host</span><span class="pi">:</span> <span class="s">nodes</span>
<span class="na">tasks</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create ssh dir</span>
<span class="na">file</span><span class="pi">:</span>
<span class="na">state</span><span class="pi">:</span> <span class="s">directory</span>
<span class="na">path</span><span class="pi">:</span> <span class="s">/home/{{ username }}/.ssh</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set Deployment Key</span>
<span class="na">copy</span><span class="pi">:</span>
<span class="na">src</span><span class="pi">:</span> <span class="s">files/deploy_key</span>
<span class="na">dest</span><span class="pi">:</span> <span class="s">/home/{{ username }}/.ssh/id_rsa</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Clone repo</span>
<span class="na">git</span><span class="pi">:</span>
<span class="na">accept_hostkey</span><span class="pi">:</span> <span class="s">yes</span>
<span class="na">clone</span><span class="pi">:</span> <span class="s">yes</span>
<span class="na">dest</span><span class="pi">:</span> <span class="s">/home/{{ username }}/exampleapp</span>
<span class="na">key_file</span><span class="pi">:</span> <span class="s">/home/{{ username }}/.ssh/id_rsa</span>
<span class="na">repo</span><span class="pi">:</span> <span class="s">git@github.com:example/apprepo.git</span>
</code></pre></div></div>
<p>Here we see the improved version, where parameter values are set separately from the tasks and can be easily overridden. Human meaningful variables “document” what’s getting plugged into the task parameter. This can now be more easily refactored into a Role.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Clone student lesson app for a user</span>
<span class="na">host</span><span class="pi">:</span> <span class="s">nodes</span>
<span class="na">vars</span><span class="pi">:</span>
<span class="na">user_home_dir</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/home/{{</span><span class="nv"> </span><span class="s">username</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">user_ssh_dir</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">user_home_dir</span><span class="nv"> </span><span class="s">}}/.ssh"</span>
<span class="na">deploy_key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">user_ssh_dir</span><span class="nv"> </span><span class="s">}}/id_rsa"</span>
<span class="na">app_dir</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">user_home_dir</span><span class="nv"> </span><span class="s">}}/exampleapp"</span>
<span class="na">tasks</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create ssh dir</span>
<span class="na">file</span><span class="pi">:</span>
<span class="na">state</span><span class="pi">:</span> <span class="s">directory</span>
<span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">user_ssh_dir</span><span class="nv"> </span><span class="s">}}"</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set Deployment Key</span>
<span class="na">copy</span><span class="pi">:</span>
<span class="na">src</span><span class="pi">:</span> <span class="s">files/deploy_key</span>
<span class="na">dest</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">deploy_key</span><span class="nv"> </span><span class="s">}}"</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Clone repo</span>
<span class="na">git</span><span class="pi">:</span>
<span class="na">dest</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">app_dir</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">key_file</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">deploy_key</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">repo</span><span class="pi">:</span> <span class="s">git@github.com:example/exampleapp.git</span>
<span class="na">accept_hostkey</span><span class="pi">:</span> <span class="s">yes</span>
<span class="na">clone</span><span class="pi">:</span> <span class="s">yes</span>
</code></pre></div></div>
<blockquote>
<p>Define temporary variables using unique prefix</p>
</blockquote>
<p>Registered variables and facts set using <code class="language-plaintext highlighter-rouge">set_fact</code> module are often defined for temporary usage. In order to avoid variable collision and make it clear to the reader that these variables are only for temporary usage, it is good practice to use a unique prefix. For example, you could use <code class="language-plaintext highlighter-rouge">r_</code> for registered variables and <code class="language-plaintext highlighter-rouge">f_</code> for facts. Another possibility is simply using <code class="language-plaintext highlighter-rouge">_</code> underscore as the prefix for any temporary variables: <code class="language-plaintext highlighter-rouge">_results</code> or <code class="language-plaintext highlighter-rouge">_myfact</code>.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Collect information from external system</span>
<span class="na">uri</span><span class="pi">:</span>
<span class="na">url</span><span class="pi">:</span> <span class="s">http://www.example.com</span>
<span class="na">return_content</span><span class="pi">:</span> <span class="s">yes</span>
<span class="na">register</span><span class="pi">:</span> <span class="s">r_results</span>
<span class="na">failed_when</span><span class="pi">:</span> <span class="s2">"</span><span class="s">'AWESOME'</span><span class="nv"> </span><span class="s">not</span><span class="nv"> </span><span class="s">in</span><span class="nv"> </span><span class="s">r_results.content"</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set some facts for temporary usage</span>
<span class="na">set_fact</span><span class="pi">:</span>
<span class="na">f_username</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">r_results.username</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div></div>
<blockquote>
<p>Define paths without trailing slash</p>
</blockquote>
<p>Variables that define paths should never have a trailing slash. Also when concatenating paths, follow the same convention. For example:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># yes</span>
<span class="na">app_root</span><span class="pi">:</span> <span class="s">/foo</span>
<span class="na">app_bar</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">app_root</span><span class="nv"> </span><span class="s">}}/bar"</span>
<span class="c1"># no</span>
<span class="na">app_root</span><span class="pi">:</span> <span class="s">/foo/</span>
<span class="na">app_bar</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">app_root</span><span class="nv"> </span><span class="s">}}bar"</span>
</code></pre></div></div>
<blockquote>
<p>Keep dictionaries simple</p>
</blockquote>
<p>Name the dictionary using the standard naming conventions described above, and name the keys defined inside the dictionary without repeating the same prefix. For example:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apache_options</span><span class="pi">:</span>
<span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
<span class="na">path</span><span class="pi">:</span> <span class="s">/opt/apache</span>
<span class="na">version</span><span class="pi">:</span> <span class="m">1</span>
</code></pre></div></div>
<h2 id="conditionals-and-return-status">Conditionals and Return Status</h2>
<blockquote>
<p>Place the <code class="language-plaintext highlighter-rouge">when</code> clause on Tasks after the <code class="language-plaintext highlighter-rouge">name</code> clause (always start with <code class="language-plaintext highlighter-rouge">name:</code>)</p>
</blockquote>
<p>Keep your Ansible readable by maintaining a consistent style. By always using <code class="language-plaintext highlighter-rouge">name:</code> as the first clause in a Task it helps the reader understand what you are trying to achieve in this task and makes it easier to scan a set of tasks defined in a task file.</p>
<p>Some examples of good and bad style:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># yes</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Print the dictionary</span>
<span class="na">when</span><span class="pi">:</span> <span class="s">my_dict is defined</span>
<span class="na">debug</span><span class="pi">:</span>
<span class="na">msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">dictionary</span><span class="nv"> </span><span class="s">values:</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">my_dict</span><span class="nv"> </span><span class="s">}}"</span>
<span class="c1"># no</span>
<span class="pi">-</span> <span class="na">when</span><span class="pi">:</span> <span class="s">my_dict is defined</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Print the dictionary</span>
<span class="na">debug</span><span class="pi">:</span>
<span class="na">msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">dictionary</span><span class="nv"> </span><span class="s">values:</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">my_dict</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div></div>
<blockquote>
<p>Ensure variables are defined before you reference them</p>
</blockquote>
<p>You can use the <code class="language-plaintext highlighter-rouge">assert</code> module or a <code class="language-plaintext highlighter-rouge">when</code> clause to ensure they are defined. Some example tasks:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Ensure variable is defined</span>
<span class="na">assert</span><span class="pi">:</span>
<span class="na">that</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">my_var is defined</span>
<span class="pi">-</span> <span class="s">my_var >= </span><span class="m">0</span>
<span class="pi">-</span> <span class="s">my_var <= </span><span class="m">100</span>
<span class="na">fail_msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">'my_var'</span><span class="nv"> </span><span class="s">must</span><span class="nv"> </span><span class="s">be</span><span class="nv"> </span><span class="s">between</span><span class="nv"> </span><span class="s">0</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">100"</span>
<span class="na">success_msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">'my_var'</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">between</span><span class="nv"> </span><span class="s">0</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">100"</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Output my custom message</span>
<span class="na">debug</span><span class="pi">:</span>
<span class="na">msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">The</span><span class="nv"> </span><span class="s">value</span><span class="nv"> </span><span class="s">is:</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">my_var</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">when</span><span class="pi">:</span> <span class="s">my_var is defined</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Ensure variable is defined</span>
<span class="na">fail</span><span class="pi">:</span>
<span class="na">msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Variable</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">not</span><span class="nv"> </span><span class="s">defined"</span>
<span class="na">when</span><span class="pi">:</span> <span class="s">my_var is not defined</span>
</code></pre></div></div>
<blockquote>
<p>Verify return status using <code class="language-plaintext highlighter-rouge">failed</code> Jinja conditional filter function</p>
</blockquote>
<p>For example:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check for something</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">/bin/false</span>
<span class="na">register</span><span class="pi">:</span> <span class="s">my_result</span>
<span class="na">ignore_errors</span><span class="pi">:</span> <span class="s">True</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Failed to do something</span>
<span class="na">debug</span><span class="pi">:</span> <span class="s">msg="task failed"</span>
<span class="na">when</span><span class="pi">:</span> <span class="s">my_result | failed</span>
</code></pre></div></div>
<h2 id="formatting">Formatting</h2>
<blockquote>
<p>Use spaces around Jinja variable names</p>
</blockquote>
<p>For example, use <code class="language-plaintext highlighter-rouge">{{ var }}</code> and not <code class="language-plaintext highlighter-rouge">{{var}}</code>.</p>
<blockquote>
<p>Use native YAML syntax to maximize readability</p>
</blockquote>
<p>Vertical reading is easier and it supports complex parameter values. Additionally this format works better with syntax highlighting in editors. It also works much better with a version control system, since a change to a specific line of code often means one parameter change, but when it’s all on one line then it’s more difficult to determine what exactly changed. For example:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># yes</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Ensure text file exists</span>
<span class="na">file</span><span class="pi">:</span>
<span class="na">dest</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">test</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">src</span><span class="pi">:</span> <span class="s2">"</span><span class="s">./foo.txt"</span>
<span class="na">mode</span><span class="pi">:</span> <span class="m">0770</span>
<span class="na">state</span><span class="pi">:</span> <span class="s">present</span>
<span class="na">user</span><span class="pi">:</span> <span class="s2">"</span><span class="s">root"</span>
<span class="na">group</span><span class="pi">:</span> <span class="s2">"</span><span class="s">wheel"</span>
<span class="c1"># no</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Ensure text file exists</span>
<span class="na">file</span><span class="pi">:</span> <span class="pi">></span>
<span class="s">dest={{ test }} src=./foo.txt mode=0770</span>
<span class="s">state=present user=root group=wheel</span>
</code></pre></div></div>
<blockquote>
<p>Break long lines using YAML line continuation</p>
</blockquote>
<p>One often defines key/value pairs in Ansible and the value can be quite a long string. This is common when using the <code class="language-plaintext highlighter-rouge">shell</code> or <code class="language-plaintext highlighter-rouge">command</code> modules to execute something that cannot be done using an Ansible module. In these cases, use the line continuation mechanisms that are available to improve readability. Again, it also helps a lot with version control systems since a change to a specific line of code that is broken into multiple lines is easier to detect the change.</p>
<p>There are many types of multiline string mechanisms - learn about them <a href="https://yaml-multiline.info/">here</a>.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">shell</span><span class="pi">:</span> <span class="pi">></span>
<span class="s">python a very long command --with=very --long-options=foo</span>
<span class="s">--and-even=more_options --like-these</span>
</code></pre></div></div>
<h2 id="collections">Collections</h2>
<p>As of Ansible 2.10, collections are required. So here’s some style guides.</p>
<blockquote>
<p>Just because you can, doesn’t mean you should</p>
</blockquote>
<p>An Ansible Collection represents a set of Ansible Roles, Modules, etc that all reside within the same repository and therefore have the same life cycle - they are released together. While you might have a bunch of Ansible Roles in one repository or across multiple repositories and thinking of consolidating and creating an Ansible Collection repository, it may not be the right direction. If the Ansible Roles should be released together for a single purpose then perhaps it’s a good candidate. Otherwise, if the Ansible Roles and related Modules can live in separate repositories then users of those Roles can leverage whatever release from whatever Ansible Role they need for their purposes.</p>
<p>The simple way to look at it is by reviewing existing certified content in the Ansible Collections that are offered. For example, AWS, VMware, general community, etc. These collections contain various modules and Ansible Roles that collectively (intentional use here) serve the end user and must (emphasis on this word) collectively work together, be tested together and be released together.</p>John WadleighCollection of Ansible standards based on my experience.Install Ansible Tower on OpenShift 4.x on your Laptop2020-09-21T00:00:00-07:002020-09-21T00:00:00-07:00https://ansiblejunky.com/blog/ansible-tower-in-openshift-on-laptop<ul id="markdown-toc">
<li><a href="#requirements" id="markdown-toc-requirements">Requirements</a></li>
<li><a href="#openshift-resources" id="markdown-toc-openshift-resources">OpenShift Resources</a></li>
<li><a href="#part-1---install-openshift" id="markdown-toc-part-1---install-openshift">Part 1 - Install Openshift</a></li>
<li><a href="#part-2---install-ansible-tower" id="markdown-toc-part-2---install-ansible-tower">Part 2 - Install Ansible Tower</a> <ul>
<li><a href="#download" id="markdown-toc-download">Download</a></li>
<li><a href="#inventory" id="markdown-toc-inventory">Inventory</a></li>
<li><a href="#option-1-externally-managed-database" id="markdown-toc-option-1-externally-managed-database">Option 1: Externally Managed Database</a></li>
<li><a href="#option-2a-tower-managed-database-with-persistent-data" id="markdown-toc-option-2a-tower-managed-database-with-persistent-data">Option 2a: Tower Managed Database, with Persistent Data</a></li>
<li><a href="#option-2b-tower-managed-database-with-non-persistent-data" id="markdown-toc-option-2b-tower-managed-database-with-non-persistent-data">Option 2b: Tower Managed Database, with Non-Persistent Data</a></li>
<li><a href="#install" id="markdown-toc-install">Install</a></li>
</ul>
</li>
<li><a href="#part-3---access-ansible-tower" id="markdown-toc-part-3---access-ansible-tower">Part 3 - Access Ansible Tower</a></li>
<li><a href="#adjusting-ansible-tower-resource-requirements" id="markdown-toc-adjusting-ansible-tower-resource-requirements">Adjusting Ansible Tower Resource Requirements</a></li>
<li><a href="#remote-shell" id="markdown-toc-remote-shell">Remote Shell</a></li>
<li><a href="#extras" id="markdown-toc-extras">Extras</a></li>
<li><a href="#upgrading" id="markdown-toc-upgrading">Upgrading</a></li>
<li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>
<p><code class="language-plaintext highlighter-rouge">Red Hat Ansible Tower</code> is now supported on the <code class="language-plaintext highlighter-rouge">Red Hat OpenShift</code> platform. In order to get your hands dirty with the installation process and gain experience, follow these steps to install OpenShift on your laptop or workstation and then additionally install Ansible Tower.</p>
<p>Previously, with OpenShift 3.x we would use a product called <a href="https://github.com/minishift/minishift">Minishift</a> to install OpenShift on our laptop however this is not available for OpenShift 4.x.</p>
<p>To run Red Hat OpenShift 4.x on your laptop we will need to use a new product called <code class="language-plaintext highlighter-rouge">Red Hat CodeReady Containers</code> that uses a single node configuration on your laptop. Simply put, it uses hypervisor software to spin up a virtual machine containing a single node OpenShift cluster. The virtual machine is managed solely through the <code class="language-plaintext highlighter-rouge">crc</code> command line tool. The OpenShift cluster is managed through the standard <code class="language-plaintext highlighter-rouge">oc</code> command line tool as well as the GUI interface.</p>
<p><code class="language-plaintext highlighter-rouge">Red Hat CodeReady Containers</code> creates a regular OpenShift installation with the following notable differences:</p>
<ul>
<li>The CodeReady Containers OpenShift cluster is <em>ephemeral</em> and is not intended for production use.</li>
<li>It uses a single node which behaves as both a master and worker node.</li>
<li>It disables the machine-config and monitoring Operators by default.
<ul>
<li>These disabled Operators cause the corresponding parts of the web console to be non-functional.</li>
<li>For the same reason, there is no upgrade path to newer OpenShift versions.</li>
</ul>
</li>
<li>The OpenShift instance runs in a virtual machine. This may cause other differences, particularly with external networking.</li>
</ul>
<h2 id="requirements">Requirements</h2>
<p><code class="language-plaintext highlighter-rouge">CodeReady Containers (CRC)</code> has the following minimum requirements:</p>
<ul>
<li>4 vCPUs</li>
<li>9 GB RAM (9216 MiB)</li>
<li>35 GB of storage</li>
<li>Operating Systems
<ul>
<li>Microsoft Windows: Windows 10 Fall Creators Update (version 1709) or newer</li>
<li>MacOS: 10.12 Sierra or newer</li>
<li>Linux: Red Hat Enterprise Linux 7.5, CentOS 7.5, or latest two stable Fedora releases</li>
</ul>
</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">Ansible Tower</code> has the following minimum requirements:</p>
<ul>
<li>3 vCPUs (per pod)</li>
<li>6 GB RAM (per pod)</li>
<li>20 GB of storage</li>
<li>OpenShift 3.11 or higher</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">PostgreSQL</code> has the following minimum requirements:</p>
<ul>
<li>1 vCPUs</li>
<li>500 MiB RAM</li>
<li>20 GB of storage (due to Ansible Tower requirements)</li>
<li>OpenShift 3.11 or higher</li>
</ul>
<p>CRC creates a virtual machine with an OpenShift installation that already consumes a lot of the available resources. As a result, it is not possible to install Ansible Tower and PostgreSQL within the OpenShift cluster using the default resource settings (cpu and memory) as they were insufficient. With the default settings OpenShift cannot start the <code class="language-plaintext highlighter-rouge">tower</code> pod and shows a <code class="language-plaintext highlighter-rouge">Pending</code> status with the reason stating “0/1 nodes are available: 1 Insufficient cpu, 1 Insufficient memory.” To check this on your laptop, after Tower installer is completed use the following commands:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oc project tower
oc get pods
oc describe pod ansible-tower | <span class="nb">grep </span>Warning
</code></pre></div></div>
<p>Therefore, the recommended minimum resource settings for CRC to handle the Ansible Tower installation are the following:</p>
<ul>
<li>5 vCPUs</li>
<li>15 GB RAM (14305 MiB)</li>
</ul>
<p>We will use these settings going forward but feel free to increase them to handle even more demands from Ansible Tower and PostreSQL. This was the minimum needed to get the Ansible Tower login and be able to navigate around the application. One way to avoid resource demands might be to spin up PostgreSQL outside of OpenShift using a Docker image or external system. However, the PostgreSQL pod is not really very demanding on resources. It is the Ansible Tower pod that has the most demand on the resources. Therefore, I would recommend scaling down the base OpenShift installation so that it releases more resources for Ansible Tower.</p>
<h2 id="openshift-resources">OpenShift Resources</h2>
<p>For those that may not understand how OpenShift resources work, the folowing explanation might help. Skip this section if you already have an understanding.</p>
<p>OpenShift clusters can have one or more nodes, but in this case with CodeReady Containers we only have one node. Each node runs one or more pods that are running one or more containers inside them. Each container consumes compute resources from that node, which are measurable quantities that can be requested, allocated, and consumed.</p>
<p>When authoring a pod configuration file, you can optionally specify how much CPU and memory (RAM) each container needs in order to better schedule pods in the cluster and ensure satisfactory performance.</p>
<ul>
<li>CPU is measured in units called millicores. Each node in a cluster inspects the operating system to determine the amount of CPU cores on the node, then multiplies that value by 1000 to express its total capacity. For example, if a node has 2 cores, the node’s CPU capacity would be represented as 2000m. If you wanted to use 1/10 of a single core, it would be represented as 100m.</li>
<li>Memory is measured in bytes. In addition, it may be used with SI suffices (E, P, T, G, M, K) or their power-of-two-equivalents (Ei, Pi, Ti, Gi, Mi, Ki).</li>
</ul>
<p>To help you understand better, the meaning of memory and cpu resource units in OpenShift can be found <a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes">here</a>.</p>
<h2 id="part-1---install-openshift">Part 1 - Install Openshift</h2>
<p>For the first part, let’s prepare and install OpenShift on your laptop using the CRC tool.</p>
<p><img src="https://ansiblejunky.com/assets/images/ansible-tower-on-openshift-download.png" alt="Red Hat Developer" class="full" /></p>
<ul>
<li>Download the <code class="language-plaintext highlighter-rouge">CRC tool</code> and the <code class="language-plaintext highlighter-rouge">pull secret</code> from the <a href="https://developers.redhat.com/products/codeready-containers/download">Red Hat Developer</a> website. It is important to download it from here (as opposed to other mirrors) as you will also need the “pull secret” file to perform the installation.</li>
<li>Extract the contents of the archive to reveal the <code class="language-plaintext highlighter-rouge">crc</code> binary file</li>
<li>Move the <code class="language-plaintext highlighter-rouge">crc</code> binary file to a location in your <code class="language-plaintext highlighter-rouge">PATH</code></li>
<li>For Mac laptops:
<ul>
<li>Run <code class="language-plaintext highlighter-rouge">crc</code> from command line, Mac security will prevent it from running, click OK</li>
<li>Go to System Prefs -> Security -> General and you’ll notice a warning about <code class="language-plaintext highlighter-rouge">crc</code>; click <code class="language-plaintext highlighter-rouge">Allow</code></li>
<li>Run <code class="language-plaintext highlighter-rouge">crc</code> from command line again and window pops up warning again but allowing you to “Open” the app</li>
</ul>
</li>
<li>Setup the CRC environment. This will create the <em>~/.crc</em> directory if it does not already exist.<br />
<code class="language-plaintext highlighter-rouge">crc setup</code></li>
<li>Configure the virtual machine, as described above.
<ul>
<li>Set the CPUs<br />
<code class="language-plaintext highlighter-rouge">crc config set cpus 5</code></li>
<li>Set the Memory<br />
<code class="language-plaintext highlighter-rouge">crc config set memory 14305</code></li>
<li>Set the pull secret:<br />
<code class="language-plaintext highlighter-rouge">crc config set pull-secret-file <pull-secret.txt></code></li>
</ul>
</li>
<li>Review configuration settings. We can <em>only</em> configure the resources <em>before</em> we start a new OpenShift cluster. If you wish to change them later, you must first delete the existing cluster and then change the configuration.<br />
<code class="language-plaintext highlighter-rouge">crc config view</code></li>
<li>Start the installation<br />
<code class="language-plaintext highlighter-rouge">crc start</code></li>
<li>Get a cup of coffee!</li>
<li>Prepare your environment<br />
<code class="language-plaintext highlighter-rouge">eval $(crc oc-env)</code></li>
</ul>
<blockquote>
<p><i class="fas fa-info-circle"></i> Optionally, you can access the OpenShift console at this point by running <code class="language-plaintext highlighter-rouge">crc console</code> and then login to OpenShift by choosing <code class="language-plaintext highlighter-rouge">kube:admin</code> and using the provided <code class="language-plaintext highlighter-rouge">kubeadmin</code> credentials.</p>
</blockquote>
<h2 id="part-2---install-ansible-tower">Part 2 - Install Ansible Tower</h2>
<p>Before you begin the Ansible Tower installation, be aware that Ansible Tower installation requires and depends on Ansible being installed locally. If you do not have Ansible locally installed, the Ansible Tower installer will attempt to install the bundled version of Ansible for you.</p>
<p>My recommendation is to prepare your local Ansible installation yourself (use perhaps the <code class="language-plaintext highlighter-rouge">pyenv</code> tool and create a clean Python virtual environment with Ansible).</p>
<p>For further info see the <a href="https://docs.ansible.com/ansible-tower/latest/html/installandreference/requirements_refguide.html#ansible-software-requirements">Ansible Tower software requirements</a>.</p>
<h3 id="download">Download</h3>
<p>Let’s start by getting the latest Ansible Tower setup bundle.</p>
<ul>
<li>Download the latest version of the Ansible Tower installation bundle for OpenShift<br />
<code class="language-plaintext highlighter-rouge">wget https://releases.ansible.com/ansible-tower/setup_openshift/ansible-tower-openshift-setup-latest.tar.gz</code></li>
<li>Extract the contents<br />
<code class="language-plaintext highlighter-rouge">tar xvf ansible-tower-openshift-setup-latest.tar.gz</code></li>
<li>Change directory into installation folder<br />
<code class="language-plaintext highlighter-rouge">cd ansible-tower-setup-*</code></li>
</ul>
<h3 id="inventory">Inventory</h3>
<p>We now need to edit the <code class="language-plaintext highlighter-rouge">inventory</code> file variables to configure the installation.</p>
<p>Here is the overview of the variables that need to be changed. The <code class="language-plaintext highlighter-rouge">RED</code> marked variables are required and the <code class="language-plaintext highlighter-rouge">BLUE</code> marked variables are only required for a specific install option described below.</p>
<p><img src="https://ansiblejunky.com/assets/images/ansible-tower-on-openshift-inventory.png" alt="Inventory Settings" class="full" /></p>
<p>Let’s set the required variables in <code class="language-plaintext highlighter-rouge">RED</code>:</p>
<ul>
<li>Ansible Tower Credentials
<ul>
<li><code class="language-plaintext highlighter-rouge">admin_password</code> is the password for the Tower admin user</li>
<li><code class="language-plaintext highlighter-rouge">secret_key</code> is a manually generated key string of your choice</li>
</ul>
</li>
<li>PostgreSQL Credentials
<ul>
<li><code class="language-plaintext highlighter-rouge">pg_username</code> is the manually set username for postgres</li>
<li><code class="language-plaintext highlighter-rouge">pg_password</code> is the manually generated password for postgres</li>
</ul>
</li>
<li>OpenShift
<ul>
<li><code class="language-plaintext highlighter-rouge">openshift_host</code> is the OpenShift host that is created by the <code class="language-plaintext highlighter-rouge">crc</code> tool, typically <code class="language-plaintext highlighter-rouge">https://api.crc.testing:6443</code></li>
<li><code class="language-plaintext highlighter-rouge">openshift_skip_tls_verify</code> should be set to True in our case since <code class="language-plaintext highlighter-rouge">crc</code> tool creates self-signed certificates</li>
<li><code class="language-plaintext highlighter-rouge">openshift_user</code> should be set to <code class="language-plaintext highlighter-rouge">kubeadmin</code> or another admin user you created</li>
</ul>
</li>
</ul>
<p>The installation of Ansible Tower consists of <em>two</em> pods: Ansible Tower and PostgreSQL. You only need to decide on how PostgreSQL is installed. For the purposes of simply installing Tower for dev/test reasons you should use <code class="language-plaintext highlighter-rouge">Option 2a</code> (see below) that will quickly get you started.</p>
<h3 id="option-1-externally-managed-database">Option 1: Externally Managed Database</h3>
<p>With this option, PostgreSQL is <em>not</em> installed by the Tower installer, but rather it is installed separately and before the Tower install. It can be inside <em>or</em> outside of the OpenShift cluster. Tower installer is simply configured to point at the PostgreSQL instance. For typical installations this is the recommended option.</p>
<p>For this option, you need to set the following variables in the <code class="language-plaintext highlighter-rouge">inventory</code> file:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">pg_hostname</code> should be set with the name of the postgres hostname</li>
</ul>
<p>Ensure that <code class="language-plaintext highlighter-rouge">pg_username</code> and <code class="language-plaintext highlighter-rouge">pg_password</code> are set to the existing user and password on your PostgreSQL instance.</p>
<h3 id="option-2a-tower-managed-database-with-persistent-data">Option 2a: Tower Managed Database, with Persistent Data</h3>
<p>With this option, PostgreSQL is installed by the Tower installer in OpenShift using a manually pre-created Persistent Volume Claim (PVC).</p>
<p>For this option, you need perform the following tasks:</p>
<ul>
<li>Login as admin<br />
<code class="language-plaintext highlighter-rouge">oc login -u kubeadmin -p abc-123-password https://api.crc.testing:6443</code></li>
<li>Create a new project<br />
<code class="language-plaintext highlighter-rouge">oc new-project tower</code></li>
<li>
<p>Define our Persistant Volume Claim (PVC) of 20 Gi by creating <code class="language-plaintext highlighter-rouge">postgres-nfs-pvc.yml</code>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolumeClaim</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">postgresql</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">accessModes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">ReadWriteOnce</span>
<span class="na">resources</span><span class="pi">:</span>
<span class="na">requests</span><span class="pi">:</span>
<span class="na">storage</span><span class="pi">:</span> <span class="s">20Gi</span>
</code></pre></div> </div>
</li>
<li>
<p>Create the PVC within OpenShift</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nv">$ </span>oc create <span class="nt">-f</span> postgres-nfs-pvc.yml
persistentvolumeclaim <span class="s2">"postgresql"</span> created
</code></pre></div> </div>
</li>
<li>Edit the Ansible Tower <code class="language-plaintext highlighter-rouge">inventory</code> file and set the following variable to the PVC name: <code class="language-plaintext highlighter-rouge">openshift_pg_pvc_name=postgresql</code></li>
</ul>
<h3 id="option-2b-tower-managed-database-with-non-persistent-data">Option 2b: Tower Managed Database, with Non-Persistent Data</h3>
<p>With this option, PostgreSQL is installed by the Tower installer in OpenShift using an <code class="language-plaintext highlighter-rouge">emptyDir</code> volume that Tower creates for you. More info can be found <a href="https://kubernetes.io/docs/concepts/storage/volumes/#emptydir">here</a>.</p>
<p>For this option, you need to set the following variables in the <code class="language-plaintext highlighter-rouge">inventory</code> file:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">openshift_pg_emptydir=true</code></li>
</ul>
<h3 id="install">Install</h3>
<p>Now we are ready to finally start the Ansible Tower installation.</p>
<ul>
<li>Set an environment variable for the OpenShift password:<br />
<code class="language-plaintext highlighter-rouge">export OPENSHIFT_PASSWORD=my-password</code></li>
<li>Install Ansible Tower using extra variable to provide the password:<br />
<code class="language-plaintext highlighter-rouge">./setup_openshift.sh -e openshift_password=$OPENSHIFT_PASSWORD</code></li>
<li>Get another coffee!</li>
<li>Once done, you should check the health of the pods to ensure they are running<br />
<code class="language-plaintext highlighter-rouge">oc get pods</code></li>
</ul>
<h2 id="part-3---access-ansible-tower">Part 3 - Access Ansible Tower</h2>
<p>To access Ansible Tower use either the GUI or command line to get the URL.</p>
<p><img src="https://ansiblejunky.com/assets/images/ansible-tower-on-openshift-project.png" alt="Ansible Tower in OpenShift" class="full" /></p>
<ul>
<li>Using OpenShift GUI:
<ul>
<li>Open the OpenShift console: <code class="language-plaintext highlighter-rouge">crc console</code></li>
<li>Login by selecting <code class="language-plaintext highlighter-rouge">kube:admin</code> and using the credentials</li>
<li>Navigate to <code class="language-plaintext highlighter-rouge">Projects</code></li>
<li>Select <code class="language-plaintext highlighter-rouge">tower</code> project</li>
<li>Navigate to <code class="language-plaintext highlighter-rouge">Route</code> in the Project Details page</li>
<li>Navigate to the URL shown in the <code class="language-plaintext highlighter-rouge">Location</code> column and drilldown on it</li>
<li>New tab opens up with Ansible Tower</li>
</ul>
</li>
<li>Using Command Line
<ul>
<li>Change current project: <code class="language-plaintext highlighter-rouge">oc project tower</code></li>
<li>Get routes: <code class="language-plaintext highlighter-rouge">oc get routes</code></li>
<li>Use the value under the <code class="language-plaintext highlighter-rouge">HOST/PORT</code> column as the URL for Ansible Tower</li>
<li>Navigate to the URL in your browser</li>
</ul>
</li>
<li>Login using the <code class="language-plaintext highlighter-rouge">admin</code> credentials you defined in the <code class="language-plaintext highlighter-rouge">inventory</code> file for Tower installer</li>
<li>Import your <a href="https://docs.ansible.com/ansible-tower/latest/html/userguide/import_license.html">Tower license</a></li>
<li>Start having fun with Ansible Tower</li>
</ul>
<p><img src="https://ansiblejunky.com/assets/images/ansible-tower-on-openshift-tower.png" alt="Ansible Tower in OpenShift" class="full" /></p>
<h2 id="adjusting-ansible-tower-resource-requirements">Adjusting Ansible Tower Resource Requirements</h2>
<p>Ansible Tower installation creates a <code class="language-plaintext highlighter-rouge">tower</code> pod with four containers that perform different functions. Each container is configured with specific resource requirements. For a diagram of the pod and containers see <a href="https://docs.ansible.com/ansible-tower/latest/html/administration/openshift_configuration.html#openshift-deployment-and-configuration">this page</a>. These resource settings for CPU and memory can be found inside the install folder:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Change directory to Ansible Tower installation folder</span>
<span class="nv">$ </span><span class="nb">cd </span>ansible-tower-openshift-setup-<span class="k">*</span>
<span class="c"># Get all resource settings for Ansible Tower</span>
<span class="nv">$ </span><span class="nb">grep </span>request ./roles/kubernetes/defaults/main.yml
web_mem_request: 1
web_cpu_request: 500
task_mem_request: 2
task_cpu_request: 1500
redis_mem_request: 2
redis_cpu_request: 500
memcached_mem_request: 1
memcached_cpu_request: 500
</code></pre></div></div>
<p>You can override these settings by using one of the following methods:</p>
<ul>
<li>Override the variables by adding them at the end of the Tower installation <code class="language-plaintext highlighter-rouge">inventory</code> file and install or re-install Tower using these new values.</li>
<li>Override the OpenShift Deployment object yaml definition within OpenShift after Tower has already been installed and re-deploy the pod with the new values.
<ul>
<li>Get the deployment<br />
<code class="language-plaintext highlighter-rouge">oc get deployments</code></li>
<li>Describe the deployment<br />
<code class="language-plaintext highlighter-rouge">oc describe deployment ansible-tower</code></li>
<li>Change the deployment settings</li>
<li>Save new settings</li>
<li>Terminate the existing pod (this will automatically spin up a new pod with the new deployment definition)</li>
</ul>
</li>
</ul>
<p>I would recommend only adjusting the <code class="language-plaintext highlighter-rouge">task_mem_request</code> and <code class="language-plaintext highlighter-rouge">task_cpu_request</code> variables since they have the most impact on how tasks are performed within Ansible Tower.</p>
<p>More information on “Resource Requests and Request Planning” can be found <a href="https://docs.ansible.com/ansible-tower/latest/html/administration/openshift_configuration.html#resource-requests-and-request-planning">here</a></p>
<h2 id="remote-shell">Remote Shell</h2>
<p>If you wish to take a look under the hood of one of the deployed pods, simply establish a Remote Shell to a container within the pod. You can use the following example to get the list of containers and connect to one of them.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get list of containers for all pods in the tower namespace</span>
<span class="nv">$ </span>oc get pods <span class="nt">-n</span> tower <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{range .items[*]}{"POD: "}{.metadata.name}{"\n containers: "}{.spec.containers[*].name}{"\n\n"}{end}'</span>
POD: ansible-tower-5b6dc4547b-dstpf
containers: ansible-tower-web ansible-tower-task ansible-tower-redis ansible-tower-memcached
POD: postgresql-1-deploy
containers: deployment
POD: postgresql-1-ffqw2
containers: postgresql
<span class="c"># Remote shell into the `ansible-tower-web` container that sits inside the `ansible-tower-5b6dc4547b-dstpf` pod</span>
<span class="nv">$ </span>oc rsh <span class="nt">-c</span> ansible-tower-web ansible-tower-5b6dc4547b-dstpf
sh-4.2<span class="err">$</span>
</code></pre></div></div>
<h2 id="extras">Extras</h2>
<p>Some extra tips.</p>
<ul>
<li>You can find the <code class="language-plaintext highlighter-rouge">crc</code> settings and logs in your home directory at <code class="language-plaintext highlighter-rouge">~/.crc/</code>.</li>
<li>You can get more information on the PostgreSQL container pod used by Ansible Tower from <a href="https://docs.openshift.com/online/pro/using_images/db_images/postgresql.html">here</a>. This will also help you to create your own PostgreSQL instance that you can have Ansible Tower installer reference instead of it creating its own instance.</li>
</ul>
<h2 id="upgrading">Upgrading</h2>
<p>To upgrade the <code class="language-plaintext highlighter-rouge">crc</code> tool, simply re-download it and overwrite existing local instance.</p>
<p>To upgrade <code class="language-plaintext highlighter-rouge">Ansible Tower</code>, simply download the latest version of the bundle archive, prepare it and run it against your existing instance inside OpenShift.</p>
<h2 id="references">References</h2>
<p>Some further information:</p>
<ul>
<li><a href="https://docs.ansible.com/ansible-tower/latest/html/administration/openshift_configuration.html">Ansible Tower Admin Guide - OpenShift Deployment and Configuration</a></li>
<li><a href="https://code-ready.github.io/crc/">Introducing Red Hat CodeReady Containers</a></li>
<li><a href="https://docs.openshift.com/dedicated/4/welcome/index.html">OpenShift Dedicated 4 Documentation</a></li>
<li><a href="https://github.com/code-ready/crc">CodeReady Containers - source code repository</a></li>
<li><a href="https://developers.redhat.com/blog/2018/10/26/installing-and-managing-ansible-tower-on-red-hat-openshift-container-platform/">How to install Ansible Tower on Red Hat OpenShift</a></li>
<li><a href="https://www.jeffgeerling.com/blog/2019/trying-out-crc-code-ready-containers-run-openshift-4x-locally">Trying out CRC (Code Ready Containers) to run OpenShift 4.x locally</a></li>
<li><a href="https://www.jeffgeerling.com/blog/2019/run-ansible-tower-or-awx-kubernetes-or-openshift-tower-operator">Run Ansible Tower or AWX in Kubernetes or OpenShift with the Tower Operator</a></li>
<li><a href="https://haralduebele.blog/2019/09/13/red-hat-openshift-4-on-your-laptop/">Red Hat OpenShift 4 on your laptop</a></li>
<li><a href="https://developers.redhat.com/blog/2019/09/05/red-hat-openshift-4-on-your-laptop-introducing-red-hat-codeready-containers/">Red Hat OpenShift 4 on your laptop: Introducing Red Hat CodeReady Containers</a></li>
<li><a href="https://access.redhat.com/articles/2988581">OpenShift Admins Guide to jsonpath</a></li>
</ul>John WadleighThe simple guide to get you started with Ansible Tower on Openshift 4.x with just your laptopFavorite Whisky Bottles2020-04-05T00:00:00-07:002020-04-05T00:00:00-07:00https://ansiblejunky.com/blog/my-favorite-whisky-bottles<p>These are my favorite whisky bottles. I have tasted each of these multiple times in different situations and they seem to maintain a level of quality and complexity that continually draws me back to them.</p>
<p>At the moment, these bottles are priced from $50 up to $130, but most stay under the $100 range. Of course you can find good whisky under $50, however the bottles listed here are a few notches above and are worth the extra cash if you have it.</p>
<div class="feature__wrapper">
<!-- Create sorted list of products -->
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/ablob.16yo.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/8844/aberlour-16-year-old-double-cask"><h2 class="archive__item-title">Aberlour 16 Year</h2></a>
<div class="archive__item-excerpt">
<p>Incredible scotch. Smooth, sweet, and well balanced. Definitely buy 2 bottles if you can afford it.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/8844/aberlour-16-year-old-double-cask" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://cdn4.masterofmalt.com/whiskies/p-2813/aberlour/aberlour-abunadh-batch-62-whisky.jpg?ss=2.0" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.youtube.com/watch?v=Jrs_ofDYp4s"><h2 class="archive__item-title">Aberlour A'Bunadh</h2></a>
<div class="archive__item-excerpt">
<p>Truly amazing scotch! High ABV and for a good reason.</p>
</div>
<p><a href="https://www.youtube.com/watch?v=Jrs_ofDYp4s" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/ablob.non47.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/53636/aberlour-casg-annamh-batch-4"><h2 class="archive__item-title">Aberlour Casg Annamh</h2></a>
<div class="archive__item-excerpt">
<p>Similar to A’Bunadh but only 48% ABV. Incredible scotch!</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/53636/aberlour-casg-annamh-batch-4" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://cdn4.masterofmalt.com/whiskies/p-2813/balvenie/balvenie-12-year-old-the-sweet-toast-of-american-oak-whisky.jpg?ss=2.0" alt="" />
</div>
<div class="archive__item-body">
<a href="https://us.thebalvenie.com/stories/the-sweet-toast-of-american-oak"><h2 class="archive__item-title">Balvenie American Oak 12 Year</h2></a>
<div class="archive__item-excerpt">
<p>Very unique product to the Balvenie family. Full of unique flavors but still retains the smooth Balvenie character.</p>
</div>
<p><a href="https://us.thebalvenie.com/stories/the-sweet-toast-of-american-oak" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://cdn2.bigcommerce.com/server5500/tpbc2s65/products/565/images/598/balvenie12__92252__19978.1358534085.1280.1280.jpg?c=2" alt="" />
</div>
<div class="archive__item-body">
<a href="https://us.thebalvenie.com/our-whisky-range/view/doublewood-12"><h2 class="archive__item-title">Balvenie Double Wood 12 Year</h2></a>
<div class="archive__item-excerpt">
<p>Wonderful scotch, readily available in stores.</p>
</div>
<p><a href="https://us.thebalvenie.com/our-whisky-range/view/doublewood-12" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://us.thebalvenie.com/assets/Uploads/sherry-cask-lifestyle-image.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://us.thebalvenie.com/our-whisky-range/view/the-balvenie-single-barrel-15"><h2 class="archive__item-title">Balvenie Single Barrel 15 Year Sherry Cask</h2></a>
<div class="archive__item-excerpt">
<p>Fantasic! High ABV but worth it. If you can find it, buy it.</p>
</div>
<p><a href="https://us.thebalvenie.com/our-whisky-range/view/the-balvenie-single-barrel-15" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://us.thebalvenie.com/assets/Uploads/_resampled/FillWyIxMTcwIiwiMTIwMCJd/tripple-cask-12-years.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://us.thebalvenie.com/our-whisky-range/view/triple-cask"><h2 class="archive__item-title">Balvenie Triple Cask 12 Year</h2></a>
<div class="archive__item-excerpt">
<p>The affordable version of the Triple Cask family, and yet incredible quality.</p>
</div>
<p><a href="https://us.thebalvenie.com/our-whisky-range/view/triple-cask" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://us.thebalvenie.com/assets/Uploads/_resampled/FillWyIxMTcwIiwiMTIwMCJd/triple-cask-16-years.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://us.thebalvenie.com/our-whisky-range/view/the-balvenie-triple-cask-16"><h2 class="archive__item-title">Balvenie Triple Cask 16 Year</h2></a>
<div class="archive__item-excerpt">
<p>Super smooth and amazing character. Highly recommended!</p>
</div>
<p><a href="https://us.thebalvenie.com/our-whisky-range/view/the-balvenie-triple-cask-16" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://us.thebalvenie.com/assets/Uploads/peat-tripple-cask-lifestyle-image2.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://us.thebalvenie.com/our-whisky-range/view/the-balvenie-peated-triple-cask"><h2 class="archive__item-title">Balvenie Triple Cask Peated 14 Year</h2></a>
<div class="archive__item-excerpt">
<p>Nice smokey but silky scotch that still maintains Balvenie quality. Buy it!</p>
</div>
<p><a href="https://us.thebalvenie.com/our-whisky-range/view/the-balvenie-peated-triple-cask" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/bnrob.12yov8.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/28562/benriach-12-year-old-sherry-wood"><h2 class="archive__item-title">BenRiach 12 Year Sherry Wood</h2></a>
<div class="archive__item-excerpt">
<p>A sweet and smooth scotch with beautiful complexity at a reasonable price.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/28562/benriach-12-year-old-sherry-wood" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/cilob.12yov1.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/1619/caol-ila-12-year-old"><h2 class="archive__item-title">Caol Ila 12 Year</h2></a>
<div class="archive__item-excerpt">
<p>A scotch classic! Very smokey but with an incredibly beautiful smooth character.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/1619/caol-ila-12-year-old" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="http://www.glengrant.com/media/115246/12-years.png" alt="" />
</div>
<div class="archive__item-body">
<a href="http://www.glengrant.com/int/en/whiskies/12-years/"><h2 class="archive__item-title">Glen Grant 12 Year</h2></a>
<div class="archive__item-excerpt">
<p>Great price for a great quality whisky.</p>
</div>
<p><a href="http://www.glengrant.com/int/en/whiskies/12-years/" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="http://www.glengrant.com/media/116357/18-years.png" alt="" />
</div>
<div class="archive__item-body">
<a href="http://www.glengrant.com/int/en/whiskies/18-years/"><h2 class="archive__item-title">Glen Grant 18 Year</h2></a>
<div class="archive__item-excerpt">
<p>Simply buy it.</p>
</div>
<p><a href="http://www.glengrant.com/int/en/whiskies/18-years/" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/grnob.12yov4.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/2623/glendronach-12-year-old-original"><h2 class="archive__item-title">Glendronach 12 Year</h2></a>
<div class="archive__item-excerpt">
<p>Incredible scotch and a great price. If you see it, buy it.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/2623/glendronach-12-year-old-original" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/grnob.15yov1.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/9731/glendronach-15-year-old-revival-sherry-cask"><h2 class="archive__item-title">Glendronach 15 Year</h2></a>
<div class="archive__item-excerpt">
<p>A sherry-bomb with beautiful complexity.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/9731/glendronach-15-year-old-revival-sherry-cask" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/grnob.18yov2.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/9732/glendronach-18-year-old-allardice-sherry-cask"><h2 class="archive__item-title">Glendronach 18 Year</h2></a>
<div class="archive__item-excerpt">
<p>One of the best sherry-focused scotch in the market.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/9732/glendronach-18-year-old-allardice-sherry-cask" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/gfdob.18yo.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/2093/glenfiddich-18-year-old"><h2 class="archive__item-title">Glenfiddich 18 Year Old</h2></a>
<div class="archive__item-excerpt">
<p>Buy 2 bottles of this superb scotch.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/2093/glenfiddich-18-year-old" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/glvob.15yov1.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/2400/glenlivet-15-year-old-french-oak-reserve"><h2 class="archive__item-title">Glenlivet 15 Year Old French Oak Reserve</h2></a>
<div class="archive__item-excerpt">
<p>Simply buy it.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/2400/glenlivet-15-year-old-french-oak-reserve" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/glvob.18yov1.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/15703/glenlivet-18-year-old"><h2 class="archive__item-title">Glenlivet 18 Year Old</h2></a>
<div class="archive__item-excerpt">
<p>Simply buy it.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/15703/glenlivet-18-year-old" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://cdn4.masterofmalt.com/whiskies/p-2813/glenmorangie/glenmorangie-lasanta-12-year-old-whisky.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.masterofmalt.com/whiskies/glenmorangie/glenmorangie-lasanta-12-year-old-whisky/"><h2 class="archive__item-title">Glenmorangie Lasanta 12 Year</h2></a>
<div class="archive__item-excerpt">
<p>Worth a visit if you love smooth and sweet scotch.</p>
</div>
<p><a href="https://www.masterofmalt.com/whiskies/glenmorangie/glenmorangie-lasanta-12-year-old-whisky/" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/japan_hib11.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/29388/hibiki-harmony"><h2 class="archive__item-title">Hibiki Harmony</h2></a>
<div class="archive__item-excerpt">
<p>Blended Japanese whisky that you simply must purchase. Very nice and enjoyable dram. Gorgeous bottle!</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/29388/hibiki-harmony" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/blend_joh84.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/552/johnnie-walker-blue-label"><h2 class="archive__item-title">Johnnie Walker Blue Label 18 Year</h2></a>
<div class="archive__item-excerpt">
<p>Blended scotch that seems to continue to go up in price. Forget buying a shot at a bar. Buy the bottle.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/552/johnnie-walker-blue-label" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/vatted_joh6.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/32602/johnnie-walker-green-label-15-year-old"><h2 class="archive__item-title">Johnnie Walker Green Label 15 Year</h2></a>
<div class="archive__item-excerpt">
<p>Blended scotch at it’s best! Incredible balance using some of the best scotch from various distilleries such as Talisker.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/32602/johnnie-walker-green-label-15-year-old" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/lgvob.16yo.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/3121/lagavulin-16-year-old"><h2 class="archive__item-title">Lagavulin 16 Year</h2></a>
<div class="archive__item-excerpt">
<p>Classic scotch. Always have a bottle of this at home.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/3121/lagavulin-16-year-old" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/lrgob.10yov1.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/3395/laphroaig-10-year-old"><h2 class="archive__item-title">Laphroaig 10 Year</h2></a>
<div class="archive__item-excerpt">
<p>Smokey and incredibly smooth. Another scotch classic.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/3395/laphroaig-10-year-old" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/macob.12yov26.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/34537/macallan-12-year-old-double-cask"><h2 class="archive__item-title">Macallan 12 Year Double Cask</h2></a>
<div class="archive__item-excerpt">
<p>A scotch classic. Beautifully crafted, slightly smokey and sweet yet well balanced. Always have a bottle at home!</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/34537/macallan-12-year-old-double-cask" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/obnob.14yo.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/4132/oban-14-year-old"><h2 class="archive__item-title">Oban 14 Year</h2></a>
<div class="archive__item-excerpt">
<p>A scotch classic! Slightly smokey with a spice.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/4132/oban-14-year-old" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/irish_red1.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/2906/redbreast-12-year-old"><h2 class="archive__item-title">Redbreast 12 Year</h2></a>
<div class="archive__item-excerpt">
<p>An Irish whisky classic! Good whisky for a good price.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/2906/redbreast-12-year-old" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
<div class="feature__item">
<div class="archive__item">
<div class="archive__item-teaser" style="border:5px solid black !important; height:300px !important">
<img src="https://img.thewhiskyexchange.com/900/talob.10yo.jpg" alt="" />
</div>
<div class="archive__item-body">
<a href="https://www.thewhiskyexchange.com/p/4837/talisker-10-year-old"><h2 class="archive__item-title">Talisker 10 Year</h2></a>
<div class="archive__item-excerpt">
<p>A scotch classic! Smokey with spice and complexity.</p>
</div>
<p><a href="https://www.thewhiskyexchange.com/p/4837/talisker-10-year-old" class="btn btn--primary">Watch the Review</a></p>
</div>
</div>
</div>
</div>John WadleighA selection of my favorite whiskies from around the world.Ansible 101 - Include vs Import2018-11-03T00:00:00-07:002018-11-03T00:00:00-07:00https://ansiblejunky.com/blog/ansible-101-include-vs-import<p><img src="https://ansiblejunky.com/assets/images/ansible-short.png" alt="Ansible" class="full" /></p>
<p>I am asked quite often about the differences between using <code class="language-plaintext highlighter-rouge">import</code> or <code class="language-plaintext highlighter-rouge">include</code> in Ansible. For example <code class="language-plaintext highlighter-rouge">import_role</code> or <code class="language-plaintext highlighter-rouge">include_role</code> - what should you expect when using one or the other?</p>
<p>Ansible documentation does a great job in explaining a lot of this and I recommend at least starting there. However, I felt a need to write a few example Playbooks for my own and to give others another way to look at it.</p>
<p>Below are some examples to help explain the differences between <code class="language-plaintext highlighter-rouge">import</code> and <code class="language-plaintext highlighter-rouge">include</code>.</p>
<h2 id="example---parsing">Example - Parsing</h2>
<p>A major difference between <code class="language-plaintext highlighter-rouge">import</code> and <code class="language-plaintext highlighter-rouge">include</code> tasks:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">import</code> tasks will be parsed at the beginning when you run your playbook</li>
<li><code class="language-plaintext highlighter-rouge">include</code> tasks will be parsed at the moment Ansible hits them</li>
</ul>
<p>Lets use an example.</p>
<p>We create a simple Ansible Role with a couple tasks - one that sets a variable and another that has a typo and should fail when parsed. It should show a syntax error. (Hint: Create the role quickly using <code class="language-plaintext highlighter-rouge">ansible-galaxy init example</code> command)</p>
<p><code class="language-plaintext highlighter-rouge">example/tasks/main.yml</code>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="pi">-</span> <span class="na">set_fact</span><span class="pi">:</span>
<span class="na">role_setfact_var</span><span class="pi">:</span> <span class="s">testvalue</span>
<span class="pi">-</span> <span class="na">debugger</span><span class="pi">:</span>
</code></pre></div></div>
<p>Now we write the Playbook to run a simple debug task first and then we include the role.</p>
<p><code class="language-plaintext highlighter-rouge">test.yml</code>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span> <span class="s">localhost</span>
<span class="na">gather_facts</span><span class="pi">:</span> <span class="s">no</span>
<span class="na">tasks</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">debug</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">include_role</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">example</span>
</code></pre></div></div>
<p>Running the Playbook shows that the first task <code class="language-plaintext highlighter-rouge">debug</code> was successfull but then Ansible tried to perform the tasks inside the role and it hit our syntax error.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">PLAY [localhost] **************************************************************************************</span>
<span class="s">TASK [debug] ******************************************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span> <span class="s">=> {</span>
<span class="s">"msg"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Hello</span><span class="nv"> </span><span class="s">world!"</span>
<span class="err">}</span>
<span class="s">TASK [include_role</span> <span class="pi">:</span> <span class="s">example] *************************************************************************</span>
<span class="s">ERROR! no action detected in task. This often indicates a misspelled module name, or incorrect module path.</span>
<span class="s">The error appears to have been in '/Users/jwadleig/Projects/ansible-example-include-vs-import/roles/example/tasks/main.yml'</span><span class="pi">:</span> <span class="s">line 3, column 3, but may</span>
<span class="s">be elsewhere in the file depending on the exact syntax problem.</span>
<span class="na">The offending line appears to be</span><span class="pi">:</span>
<span class="na">role_setfact_var</span><span class="pi">:</span> <span class="s">testvalue</span>
<span class="pi">-</span> <span class="na">debugger</span><span class="pi">:</span>
<span class="s">^ here</span>
<span class="s">to retry, use</span><span class="pi">:</span> <span class="s">--limit @/Users/jwadleig/Projects/ansible-example-include-vs-import/test.retry</span>
<span class="s">PLAY RECAP ********************************************************************************************</span>
<span class="na">localhost </span><span class="pi">:</span> <span class="s">ok=1 changed=0 unreachable=0 failed=0</span>
</code></pre></div></div>
<p>This all seems quite normal. Now let us instead use <code class="language-plaintext highlighter-rouge">import_role</code> in our playbook and see the output again.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">ERROR! no action detected in task. This often indicates a misspelled module name, or incorrect module path.</span>
<span class="s">The error appears to have been in '/Users/jwadleig/Projects/ansible-example-include-vs-import/roles/example/tasks/main.yml'</span><span class="pi">:</span> <span class="s">line 3, column 3, but may be elsewhere in the file depending on the exact syntax problem.</span>
<span class="na">The offending line appears to be</span><span class="pi">:</span>
<span class="na">role_setfact_var</span><span class="pi">:</span> <span class="s">testvalue</span>
<span class="pi">-</span> <span class="na">debugger</span><span class="pi">:</span>
<span class="s">^ here</span>
</code></pre></div></div>
<p>Wait, why didn’t Ansible run our first task? The reason is because the <code class="language-plaintext highlighter-rouge">import_role</code> task was processed at the time playbooks are parsed. So it found the syntax error much earlier and so it never was able to even run our first task with the <code class="language-plaintext highlighter-rouge">debug</code> module.</p>
<blockquote>
<p>This is an example that really emphasizes the fact that <code class="language-plaintext highlighter-rouge">import</code> is parsed quite early. This is what is referred to as <code class="language-plaintext highlighter-rouge">static</code>.</p>
</blockquote>
<h2 id="example---using-conditional-clauses">Example - Using conditional clauses</h2>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span> <span class="s">localhost</span>
<span class="na">gather_facts</span><span class="pi">:</span> <span class="s">no</span>
<span class="na">tasks</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Conditional role</span>
<span class="na">include_role</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">example</span>
<span class="na">when</span><span class="pi">:</span> <span class="no">false</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Apply condition to each task in role</span>
<span class="na">import_role</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">example</span>
<span class="na">when</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>
<p>Running this playbook results in the following output.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">$ ansible-playbook test.yml</span>
<span class="s">PLAY [localhost] **************************************************************************************</span>
<span class="s">TASK [include_role</span> <span class="pi">:</span> <span class="s">example] *************************************************************************</span>
<span class="na">skipping</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">set_fact] *****************************************************************************</span>
<span class="na">skipping</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span>
<span class="s">PLAY RECAP ********************************************************************************************</span>
<span class="na">localhost </span><span class="pi">:</span> <span class="s">ok=0 changed=0 unreachable=0 failed=0</span>
</code></pre></div></div>
<p>Notice the following things:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">include_role</code> task itself was skipped because the <code class="language-plaintext highlighter-rouge">when:</code> clause is applied to the <code class="language-plaintext highlighter-rouge">include_role</code> task</li>
<li><code class="language-plaintext highlighter-rouge">import_role</code> task applied the <code class="language-plaintext highlighter-rouge">when:</code> clause to the task inside the role, so the output only showed the task inside the role that was skipped</li>
</ul>
<p>In fact, Ansible documentation states:</p>
<blockquote>
<p>Most keywords, loops and conditionals will only be applied to the imported tasks, not to the statement itself.</p>
</blockquote>
<h2 id="example---using-tags">Example - Using tags</h2>
<p>Another common question is how tags are affected when using <code class="language-plaintext highlighter-rouge">import</code> vs <code class="language-plaintext highlighter-rouge">include</code>.</p>
<ul>
<li>When using tags with <code class="language-plaintext highlighter-rouge">include_role</code>, the tags are applied <em>only</em> to the <code class="language-plaintext highlighter-rouge">include_task</code> itself - not the tasks inside the role!</li>
<li>When using tags with <code class="language-plaintext highlighter-rouge">import_role</code>, the tags are applied to all the tasks inside the role and <em>not</em> to the import_role task itself.</li>
</ul>
<p>Let’s fix our <code class="language-plaintext highlighter-rouge">example</code> role and remove the typos.</p>
<p><code class="language-plaintext highlighter-rouge">example/tasks/main.yml</code>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="pi">-</span> <span class="na">set_fact</span><span class="pi">:</span>
<span class="na">role_setfact_var</span><span class="pi">:</span> <span class="s2">"</span><span class="s">inside</span><span class="nv"> </span><span class="s">example</span><span class="nv"> </span><span class="s">role"</span>
<span class="pi">-</span> <span class="na">debug</span><span class="pi">:</span> <span class="s">var=role_setfact_var</span>
</code></pre></div></div>
<p>Let’s write a playbook that uses tags. Note that we will add a <code class="language-plaintext highlighter-rouge">debug</code> task to make sure we know when the <code class="language-plaintext highlighter-rouge">include_role</code> task has completed and the next task is starting. This will help us understand what is happening.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span> <span class="s">localhost</span>
<span class="na">gather_facts</span><span class="pi">:</span> <span class="s">no</span>
<span class="na">tasks</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Apply tags to only this task (include_role)</span>
<span class="na">include_role</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">example</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">install</span>
<span class="pi">-</span> <span class="na">debug</span><span class="pi">:</span>
<span class="na">msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">include_role</span><span class="nv"> </span><span class="s">completed"</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">install</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Apply tags to tasks inside the role (import_role)</span>
<span class="na">import_role</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">example</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">install</span>
</code></pre></div></div>
<p>Running the playbook without tags results in Ansible running all tasks. Notice that “inside example role” was printed twice - once for import_role and once for include_role.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">PLAY [localhost] ***********************************************************************</span>
<span class="s">TASK [Apply tags to only this task (include_role)] *************************************</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">set_fact] **************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">debug] *****************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span> <span class="s">=> {</span>
<span class="s">"role_setfact_var"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">inside</span><span class="nv"> </span><span class="s">example</span><span class="nv"> </span><span class="s">role"</span>
<span class="err">}</span>
<span class="s">TASK [debug] ***************************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span> <span class="s">=> {</span>
<span class="s">"msg"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">include_role</span><span class="nv"> </span><span class="s">completed"</span>
<span class="err">}</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">set_fact] **************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">debug] *****************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span> <span class="s">=> {</span>
<span class="s">"role_setfact_var"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">inside</span><span class="nv"> </span><span class="s">example</span><span class="nv"> </span><span class="s">role"</span>
<span class="err">}</span>
<span class="s">PLAY RECAP *****************************************************************************</span>
<span class="na">localhost </span><span class="pi">:</span> <span class="s">ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0</span>
</code></pre></div></div>
<p>Now let’s run the same playbook but limit the tasks but passing a tag.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-playbook test.yml <span class="nt">--tags</span><span class="o">=</span><span class="nb">install</span>
</code></pre></div></div>
<p>The results will show you how tags behave differently with <code class="language-plaintext highlighter-rouge">include_role</code> and <code class="language-plaintext highlighter-rouge">import_role</code>. Notice the message “inside example role” is printed only once from inside the <code class="language-plaintext highlighter-rouge">import_role</code> task.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">PLAY [localhost] ***********************************************************************</span>
<span class="s">TASK [Apply tags to only this task (include_role)] *************************************</span>
<span class="s">TASK [debug] ***************************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span> <span class="s">=> {</span>
<span class="s">"msg"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">include_role</span><span class="nv"> </span><span class="s">completed"</span>
<span class="err">}</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">set_fact] **************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">debug] *****************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span> <span class="s">=> {</span>
<span class="s">"role_setfact_var"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">inside</span><span class="nv"> </span><span class="s">example</span><span class="nv"> </span><span class="s">role"</span>
<span class="err">}</span>
<span class="s">PLAY RECAP *****************************************************************************</span>
<span class="na">localhost </span><span class="pi">:</span> <span class="s">ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0</span>
</code></pre></div></div>
<p>If you really want <code class="language-plaintext highlighter-rouge">include_role</code> to apply tags to all tasks inside the role, then you need to use the <code class="language-plaintext highlighter-rouge">apply</code> option. Let’s make that change in our playbook and test it. Notice we still keep the tag on the <code class="language-plaintext highlighter-rouge">include_role</code> task to make sure this task is executed, otherwise none of the tasks inside the role will run.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span> <span class="s">localhost</span>
<span class="na">gather_facts</span><span class="pi">:</span> <span class="s">no</span>
<span class="na">tasks</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Apply tags to only this task (include_role)</span>
<span class="na">include_role</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">example</span>
<span class="na">apply</span><span class="pi">:</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">install</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">install</span>
<span class="pi">-</span> <span class="na">debug</span><span class="pi">:</span>
<span class="na">msg</span><span class="pi">:</span> <span class="s2">"</span><span class="s">include_role</span><span class="nv"> </span><span class="s">completed"</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">install</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Apply tags to tasks inside the role (import_role)</span>
<span class="na">import_role</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">example</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">install</span>
</code></pre></div></div>
<p>Now run the playbook again and pass the tag limit.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-playbook test.yml <span class="nt">--tags</span><span class="o">=</span><span class="nb">install</span>
</code></pre></div></div>
<p>And the results show all tasks are running now.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">PLAY [localhost] ***********************************************************************</span>
<span class="s">TASK [Apply tags to only this task (include_role)] *************************************</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">set_fact] **************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">debug] *****************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span> <span class="s">=> {</span>
<span class="s">"role_setfact_var"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">inside</span><span class="nv"> </span><span class="s">example</span><span class="nv"> </span><span class="s">role"</span>
<span class="err">}</span>
<span class="s">TASK [debug] ***************************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span> <span class="s">=> {</span>
<span class="s">"msg"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">include_role</span><span class="nv"> </span><span class="s">completed"</span>
<span class="err">}</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">set_fact] **************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span>
<span class="s">TASK [example</span> <span class="pi">:</span> <span class="s">debug] *****************************************************************</span>
<span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">localhost</span><span class="pi">]</span> <span class="s">=> {</span>
<span class="s">"role_setfact_var"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">inside</span><span class="nv"> </span><span class="s">example</span><span class="nv"> </span><span class="s">role"</span>
<span class="err">}</span>
<span class="s">PLAY RECAP *****************************************************************************</span>
<span class="na">localhost </span><span class="pi">:</span> <span class="s">ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0</span>
</code></pre></div></div>
<p>For <code class="language-plaintext highlighter-rouge">include_role</code>, you can use the <code class="language-plaintext highlighter-rouge">apply</code> option for other keywords.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://docs.ansible.com/ansible/latest/modules/import_role_module.html">import_role</a></li>
<li><a href="https://docs.ansible.com/ansible/latest/modules/include_role_module.html">include_role</a></li>
<li><a href="https://docs.ansible.com/ansible/latest/modules/include_tasks_module.html">include_tasks</a></li>
<li><a href="https://docs.ansible.com/ansible/latest/modules/import_tasks_module.html">import_tasks</a></li>
<li><a href="https://docs.ansible.com/ansible/latest/modules/import_playbook_module.html">import_playbook</a></li>
<li><a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse.html#re-using-files-and-roles">Re-using files and roles with include and import</a></li>
</ul>John WadleighAn explanation of importing vs. including roles, tasks and playbooksBuddy’s Story2018-10-12T00:00:00-07:002018-10-12T00:00:00-07:00https://ansiblejunky.com/blog/buddys-story<p>A story about a dog named Buddy who finds himself in a new world filled with fun adventures. He follows his owner Shane everywhere and discovers his new life.
<br /><br />
<b>Donation: </b> Proceeds from the book will be donated to our favorite local animal shelter <a href="https://pawsofcoronado.org/" target="_blank">PAWS of Coronado</a>.<br />
<b>Pages: </b>24
<br />
<b>Format: </b>Hardcover
<br />
<b>Available: </b><a href="https://www.amazon.com/dp/1732599300?ref=myi_title_dp" target="_blank">Amazon</a></p>
<p><a href="https://www.amazon.com/dp/1732599300?ref=myi_title_dp" target="_blank"><img src="https://ansiblejunky.com/assets/images/buddys-story-cover.jpg" alt="Buddy's Story" /></a></p>
<!--
<b>Price: </b> $20
<div class="paypalbox">
<form target="paypal" action="https://www.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_cart">
<input type="hidden" name="business" value="wadleigh.john@gmail.com">
<input type="hidden" name="lc" value="US">
<input type="hidden" name="item_name" value="Buddy's Story">
<input type="hidden" name="amount" value="20">
<input type="hidden" name="currency_code" value="USD">
<input type="hidden" name="button_subtype" value="products">
<input type="hidden" name="no_note" value="0">
<input type="hidden" name="tax_rate" value="">
<input type="hidden" name="shipping" value="">
<input type="hidden" name="add" value="1">
<input type="hidden" name="bn" value="PP-ShopCartBF:btn_cart_LG.gif:NonHostedGuest">
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_cart_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
</form>
</div>
<br>
-->
<!--
<b>PayPal: </b> Purchasing the book online is currently only possible using a PayPal account. If you do not have one, please visit [PayPal](https://paypal.com). Alternatively, please contact me directly for alternative payment methods.
-->
<div class="fb-like"></div>
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = 'https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v3.1';
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>River WadleighA children's book about a dog discovering his new world.The Little Girl from Tunisia2018-10-06T00:00:00-07:002018-10-06T00:00:00-07:00https://ansiblejunky.com/blog/tunisian-adventure<p>For those unfamiliar with the country, Tunisia is a country located in the northen region of the African continent. The country is bordering the Mediterranean Sea, between Algeria and Libya.</p>
<ul>
<li>population</li>
<li>history</li>
</ul>
<p>In June, 2006, I decided to visit Tunisia in June, 2006, with my father. For a week and half we traveled with a guide throughout the country. The experience was priceless! Journeys like this one can open your eyes to a new culture and way of life.</p>
<p><img src="https://ansiblejunky.com/assets/images/20060829-Kairouan-0001.jpg" alt="Tunisian Girl" class="full" /></p>
<p>A little girl walks along the cobble-stones of Kairouan in Tunisia to visit her grandmother. This photo is special to me because it tells a story of the life in the medina walls of Kairouan.</p>John WadleighMy photos from visiting the beautiful country of Tunisia.Custom Themes with Marp2018-09-12T00:00:00-07:002018-09-12T00:00:00-07:00https://ansiblejunky.com/blog/custom-themes-with-marp<p><img src="https://ansiblejunky.com/assets/images/marp.png" alt="Marp" class="full" /></p>
<p>Recently I’ve been using this simple tool called <a href="https://github.com/yhatt/marp">Marp</a> for generating presentation slides from my markdown documents. It’s really a nice interface and simple to use. However it appears limited to only 2 themes for your slides. After some research I found some great ways to customize the themes.</p>
<h2 id="create-a-custom-theme">Create a Custom Theme</h2>
<p>Follow these steps to create a new theme that can be selected from within the Marp application.</p>
<ol>
<li>
<p>Clone the repository from Github</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> git clone https://github.com/yhatt/marp
</code></pre></div> </div>
</li>
<li>
<p>Initialize the project (using <code class="language-plaintext highlighter-rouge">npm</code> or <code class="language-plaintext highlighter-rouge">yarn</code>)</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> npm <span class="nb">install</span> <span class="nt">-g</span> appdmg
<span class="nb">cd </span>marp
npm <span class="nb">install</span>
</code></pre></div> </div>
</li>
<li>
<p>Copy existing theme to create the new theme</p>
<p>Copy an existing SASS file (such as <code class="language-plaintext highlighter-rouge">gaia.sass</code>) to create your new style. For now do not edit your new theme. Let’s just get this new theme to show up in the Application. When that is working for you, then you can come back to this file and make your edits how you wish.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">cp </span>sass/themes/gaia.sass sass/themes/newtheme.sass
</code></pre></div> </div>
</li>
<li>
<p>Modify coffeescript files</p>
<p>Modify the following 2 coffeescript files to add the new theme to the application.</p>
<p><code class="language-plaintext highlighter-rouge">coffee/classes/mds_main_menu.coffee</code></p>
<p>Add a new entry for your theme in the following code section that contains the array of <em>*themes</em>.</p>
<div class="language-coffee highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">themes</span><span class="o">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">label</span><span class="o">:</span> <span class="s">'&Default'</span>
<span class="na">enabled</span><span class="o">:</span> <span class="vi">@</span><span class="na">window</span><span class="o">?</span>
<span class="na">type</span><span class="o">:</span> <span class="k">if</span> <span class="vi">@</span><span class="na">window</span><span class="o">?</span> <span class="k">then</span> <span class="s">'radio'</span> <span class="k">else</span> <span class="s">'normal'</span>
<span class="na">checked</span><span class="o">:</span> <span class="o">!</span><span class="vi">@</span><span class="na">states</span><span class="o">?</span><span class="p">.</span><span class="na">theme</span> <span class="o">||</span> <span class="vi">@</span><span class="na">states</span><span class="p">.</span><span class="na">theme</span> <span class="o">==</span> <span class="s">'default'</span>
<span class="na">click</span><span class="o">:</span> <span class="o">=></span> <span class="vi">@</span><span class="na">window</span><span class="p">.</span><span class="na">mdsWindow</span><span class="p">.</span><span class="na">send</span> <span class="s">'setTheme'</span><span class="p">,</span> <span class="s">'default'</span> <span class="nx">unless</span> <span class="vi">@</span><span class="na">window</span><span class="p">.</span><span class="na">mdsWindow</span><span class="p">.</span><span class="na">freeze</span>
<span class="p">}</span>
<span class="p">{</span>
<span class="na">label</span><span class="o">:</span> <span class="s">'&Gaia'</span>
<span class="na">enabled</span><span class="o">:</span> <span class="vi">@</span><span class="na">window</span><span class="o">?</span>
<span class="na">type</span><span class="o">:</span> <span class="k">if</span> <span class="vi">@</span><span class="na">window</span><span class="o">?</span> <span class="k">then</span> <span class="s">'radio'</span> <span class="k">else</span> <span class="s">'normal'</span>
<span class="na">checked</span><span class="o">:</span> <span class="vi">@</span><span class="na">states</span><span class="p">.</span><span class="na">theme</span> <span class="o">==</span> <span class="s">'gaia'</span>
<span class="na">click</span><span class="o">:</span> <span class="o">=></span> <span class="vi">@</span><span class="na">window</span><span class="p">.</span><span class="na">mdsWindow</span><span class="p">.</span><span class="na">send</span> <span class="s">'setTheme'</span><span class="p">,</span> <span class="s">'gaia'</span> <span class="nx">unless</span> <span class="vi">@</span><span class="na">window</span><span class="p">.</span><span class="na">mdsWindow</span><span class="p">.</span><span class="na">freeze</span>
<span class="p">}</span>
<span class="p">{</span>
<span class="na">label</span><span class="o">:</span> <span class="s">'&NewTheme'</span>
<span class="na">enabled</span><span class="o">:</span> <span class="vi">@</span><span class="na">window</span><span class="o">?</span>
<span class="na">type</span><span class="o">:</span> <span class="k">if</span> <span class="vi">@</span><span class="na">window</span><span class="o">?</span> <span class="k">then</span> <span class="s">'radio'</span> <span class="k">else</span> <span class="s">'normal'</span>
<span class="na">checked</span><span class="o">:</span> <span class="vi">@</span><span class="na">states</span><span class="p">.</span><span class="na">theme</span> <span class="o">==</span> <span class="s">'newtheme'</span>
<span class="na">click</span><span class="o">:</span> <span class="o">=></span> <span class="vi">@</span><span class="na">window</span><span class="p">.</span><span class="na">mdsWindow</span><span class="p">.</span><span class="na">send</span> <span class="s">'setTheme'</span><span class="p">,</span> <span class="s">'newtheme'</span> <span class="nx">unless</span> <span class="vi">@</span><span class="na">window</span><span class="p">.</span><span class="na">mdsWindow</span><span class="p">.</span><span class="na">freeze</span>
<span class="p">}</span>
<span class="p">]</span>
</code></pre></div> </div>
<p><code class="language-plaintext highlighter-rouge">coffee/classes/mds_md_setting.coffee</code></p>
<p>Add the name of the new theme to the array.</p>
<div class="language-coffee highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">theme</span><span class="o">:</span> <span class="p">(</span><span class="nx">v</span><span class="p">)</span> <span class="o">-></span>
<span class="nx">basename</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="na">basename</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span>
<span class="k">return</span> <span class="k">if</span> <span class="nx">basename</span> <span class="o">in</span> <span class="p">[</span><span class="s">'default'</span><span class="p">,</span> <span class="s">'gaia'</span><span class="p">,</span> <span class="s">'newtheme'</span><span class="p">]</span> <span class="k">then</span> <span class="s">"css/themes/</span><span class="si">#{</span><span class="nx">basename</span><span class="si">}</span><span class="s">.css"</span> <span class="k">else</span> <span class="no">null</span>
</code></pre></div> </div>
</li>
<li>
<p>Build the project</p>
<p>Test the new changes by building the project. This will compile the changes and run the application.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> npm start
</code></pre></div> </div>
</li>
<li>
<p>Create new application</p>
<p>If you are happy with the results, you can also create a new release which can be copied to your application folder and overwrite the previous version.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> yarn gulp release
</code></pre></div> </div>
</li>
</ol>
<h2 id="custom-styles">Custom Styles</h2>
<p>You can have custom styles for each slide without having to always create a new theme within the application. Use the following tricks.</p>
<h3 id="using-the-style-tag">Using the <style> tag</h3>
<ul>
<li>Select one of the themes from the application menu, <strong>View > Theme > Default</strong> for example.</li>
<li>Override any of the theme’s style by adding the <code class="language-plaintext highlighter-rouge"><style></code> tag to your markdown document.</li>
</ul>
<p>Here is an example. Note that you might need to use <code class="language-plaintext highlighter-rouge">!important</code> for some properties to be able to override the existing styles from the theme you selected.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><style></span>
<span class="nc">.slide</span> <span class="p">{</span>
<span class="nl">background-color</span><span class="p">:</span> <span class="no">black</span><span class="p">;</span>
<span class="nl">font</span><span class="p">:</span> <span class="m">40px</span> <span class="n">arial</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.slide</span> <span class="nt">a</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">indigo</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.slide</span> <span class="nt">h1</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">fuchsia</span> <span class="cp">!important</span><span class="p">;</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.slide</span> <span class="nt">h2</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">darkviolet</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.slide</span> <span class="nt">p</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">darkorchid</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt"></style></span>
</code></pre></div></div>
<p>A few explanations of these style settings:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">.slide</code> is about slide related settings</li>
<li><code class="language-plaintext highlighter-rouge">.slide a</code> modifies the links</li>
<li><code class="language-plaintext highlighter-rouge">.slide h1</code> references the titles of header level 1</li>
<li><code class="language-plaintext highlighter-rouge">!important</code> overrides the setting set by the theme</li>
</ul>
<p>Instead of having a huge HTML tag at the top of your markdown, you can create a CSS file that contains your standard theme settings and then simply import that file using the <code class="language-plaintext highlighter-rouge"><link></code> tag.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">type=</span><span class="s">"text/css"</span> <span class="na">href=</span><span class="s">"style.css"</span><span class="nt">></span>
</code></pre></div></div>
<p>In fact you can do both. Use the <code class="language-plaintext highlighter-rouge"><link></code> tag and use the overriding <code class="language-plaintext highlighter-rouge"><style></code> tag to create customized looks for a particular set of slides.</p>
<h3 id="copy-existing-theme">Copy existing theme</h3>
<p>You can also copy an existing theme that Marp provides (Default or Gaia) and extract the generated CSS file from the application to then customize it and use it going forward.</p>
<p>To get the CSS file for a particular theme, you need to run the application from the source code. Follow the steps above and run the application using <code class="language-plaintext highlighter-rouge">npm start</code>. At this point you can see that the <code class="language-plaintext highlighter-rouge">css</code> folder has been generated. You can now copy the CSS file that you want.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cp </span>css/themes/default.css my/folder/default.css
</code></pre></div></div>
<p>Finally, use the <code class="language-plaintext highlighter-rouge"><link></code> tag to point to the new <code class="language-plaintext highlighter-rouge">default.css</code> file and whenever you want to make some changes to the theme go ahead and edi the CSS file directly.</p>
<h2 id="summary">Summary</h2>
<p>Making custom slides and themes in Marp is not so bad at all. Hopefully the new <a href="https://github.com/marp-team/marp">Marp Next</a> application will have these features more easily available for us.</p>
<h2 id="references">References</h2>
<p>Credit to the following blogs for the initial ideas.</p>
<ul>
<li><a href="https://tic-et-net.org/2017/11/30/how-to-change-the-theme-in-marp/">How to change the theme in Marp</a>.</li>
<li><a href="https://www.slideshare.net/iktakahiro/how-to-build-a-custom-theme-for-marp">How to build a custom theme for Marp</a></li>
</ul>John WadleighCreate custom themes using the nice markdown presentation tool called Marp