Jekyll2023-11-11T13:44:26+00:00https://www.logitblog.com/feed.xmlLogit BlogThis is my own blog about various IT topicsHands-on: Citrix Terraform provider2023-11-11T00:00:00+00:002023-11-11T00:00:00+00:00https://www.logitblog.com/hands-on-citrix-terraform-provider<p>Last week, Citrix released a Tech preview of the <a href="https://registry.terraform.io/providers/citrix/citrix/latest" target="_blank">Citrix Terraform provider</a>, which allows you to automate deployments and configurations of Citrix Virtual Apps & Desktops (CVAD) and Citrix Desktop as a Service (DaaS). This is very exciting for those doing infrastructure as code deployment, as this will help speed up the Citrix deployments. This post will explore the provider and its current capabilities.</p>
<h2 id="tech-preview-and-capabilities">Tech Preview and Capabilities</h2>
<p>As mentioned in the introduction, this release at the time of writing is in Tech Preview, which means this release is not officially supported yet. This Tech Preview is intended to be used in non-production or limited-production environments and to give customers an opportunity to share feedback.</p>
<p>The current release, <a href="https://github.com/citrix/terraform-provider-citrix/releases" target="_blank">version 0.3.2</a> supports managing the following resources:</p>
<ul>
<li>Hypervisors</li>
<li>Resource Pools</li>
<li>Machine Catalogs</li>
<li>Delivery Groups</li>
</ul>
<p>Currently, the provider supports Citrix Virtual Apps & Desktops (CVAD) and Citrix Desktop as a Service (DaaS) solutions. More information can be found on the <a href="https://registry.terraform.io/providers/citrix/citrix/latest" target="_blank">Terraform website</a> and the <a href="https://github.com/citrix/terraform-provider-citrix" target="_blank">GitHub repository</a>.</p>
<h2 id="getting-started">Getting started</h2>
<p>To get started with the Citrix Terraform provider, begin with the provider configuration. Based on the <a href="https://registry.terraform.io/providers/citrix/citrix/latest/docs" target="_blank">documentation</a>, two possibilities exist to authenticate to your Citrix environment. This can be either CVAD, an on-premises environment, or DaaS, the cloud-based solution.</p>
<blockquote>
<p>For CVAD, the Web Studio is required, available from version 2212 and later. Please be aware the only resources available at this time are DaaS only. At the time of writing, I could not connect successfully to the on-premises CVAD environment.</p>
</blockquote>
<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">terraform</span> <span class="p">{</span>
<span class="nx">required_providers</span> <span class="p">{</span>
<span class="nx">citrix</span> <span class="p">=</span> <span class="p">{</span>
<span class="nx">source</span> <span class="p">=</span> <span class="s2">"citrix/citrix"</span>
<span class="nx">version</span> <span class="p">=</span> <span class="s2">"0.3.2"</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1"># Cloud Provider</span>
<span class="nx">provider</span> <span class="s2">"citrix"</span> <span class="p">{</span>
<span class="nx">customer_id</span> <span class="p">=</span> <span class="s2">""</span>
<span class="nx">client_id</span> <span class="p">=</span> <span class="s2">""</span>
<span class="nx">client_secret</span> <span class="p">=</span> <span class="s2">""</span>
<span class="p">}</span>
<span class="c1"># On-Premise Provider</span>
<span class="nx">provider</span> <span class="s2">"citrix"</span> <span class="p">{</span>
<span class="nx">hostname</span> <span class="p">=</span> <span class="s2">"10.0.0.6"</span>
<span class="nx">client_id</span> <span class="p">=</span> <span class="s2">"foo.local</span><span class="err">\\</span><span class="s2">admin"</span>
<span class="nx">client_secret</span> <span class="p">=</span> <span class="s2">"foo"</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="zone">Zone</h3>
<p>To create a hypervisor connection, machine catalog, or delivery group, a zone is required. The <a href="https://registry.terraform.io/providers/citrix/citrix/latest/docs/resources/daas_zone" target="_blank">zone</a> is just the resource location that contains the cloud connectors. The <code class="language-plaintext highlighter-rouge">citrix_daas_zone</code> will not create the zone (resource location); therefore, it must be created beforehand. Once created in the Citrix DaaS portal, use the exact same name to have a successful deployment, like the example below.</p>
<p><img src="/assets/images/posts/2023-11-11-hands-on-citrix-terraform-provider/citrix-daas-resource-location.png" alt="citrix-daas-resource-location" /></p>
<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"citrix_daas_zone"</span> <span class="s2">"zone"</span> <span class="p">{</span>
<span class="nx">name</span> <span class="p">=</span> <span class="s2">"go-euc"</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="hypervisor">Hypervisor</h3>
<p>Adding a <a href="https://registry.terraform.io/providers/citrix/citrix/latest/docs/resources/daas_hypervisor" target="_blank">hypervisor connection</a> can be done using the <code class="language-plaintext highlighter-rouge">citrix_daas_hypervisor</code> resource. Based on the <a href="https://registry.terraform.io/providers/citrix/citrix/latest/docs/resources/daas_hypervisor" target="_blank">documentation</a>, it can add an Azure, GPC, or AWS hypervisor connection. For this example, let’s stick with the Azure hypervisor connection, which is the most familiar to me.</p>
<p>Additionally, there is the <a href="https://registry.terraform.io/providers/citrix/citrix/latest/docs/resources/daas_hypervisor_resource_pool" target="_blank"><code class="language-plaintext highlighter-rouge">citrix_daas_hypervisor_resource_pool</code></a>, which is the resource pool for the hypervisor connection. This requires an existing virtual network with a subnet in your Azure environment.</p>
<p>The following example will add a hypervisor connection and resource pool:</p>
<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"citrix_daas_hypervisor"</span> <span class="s2">"hypervisor"</span> <span class="p">{</span>
<span class="nx">name</span> <span class="p">=</span> <span class="s2">"go-euc-azure"</span>
<span class="nx">connection_type</span> <span class="p">=</span> <span class="s2">"AzureRM"</span>
<span class="nx">zone</span> <span class="p">=</span> <span class="nx">citrix_daas_zone</span><span class="err">.</span><span class="nx">zone</span><span class="err">.</span><span class="nx">id</span>
<span class="nx">active_directory_id</span> <span class="p">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">azure_tenant_id</span>
<span class="nx">subscription_id</span> <span class="p">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">azure_subscription_id</span>
<span class="nx">application_secret</span> <span class="p">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">azure_application_secret</span>
<span class="nx">application_id</span> <span class="p">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">azure_application_id</span>
<span class="p">}</span>
<span class="nx">resource</span> <span class="s2">"citrix_daas_hypervisor_resource_pool"</span> <span class="s2">"resource_pool"</span> <span class="p">{</span>
<span class="nx">name</span> <span class="p">=</span> <span class="s2">"go-euc-pool"</span>
<span class="nx">hypervisor</span> <span class="p">=</span> <span class="nx">citrix_daas_hypervisor</span><span class="err">.</span><span class="nx">hypervisor</span><span class="err">.</span><span class="nx">id</span>
<span class="nx">region</span> <span class="p">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">location</span> <span class="c1">#azurerm_resource_group.rg.location</span>
<span class="nx">virtual_network_resource_group</span> <span class="p">=</span> <span class="nx">azurerm_virtual_network</span><span class="err">.</span><span class="nx">vnet</span><span class="err">.</span><span class="nx">resource_group_name</span>
<span class="nx">virtual_network</span> <span class="p">=</span> <span class="nx">azurerm_virtual_network</span><span class="err">.</span><span class="nx">vnet</span><span class="err">.</span><span class="nx">name</span>
<span class="nx">subnets</span> <span class="p">=</span> <span class="p">[</span><span class="nx">azurerm_subnet</span><span class="err">.</span><span class="nx">subnet</span><span class="err">.</span><span class="nx">name</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/posts/2023-11-11-hands-on-citrix-terraform-provider/citrix-daas-hosting-connection.png" alt="citrix-daas-hosting-connection" /></p>
<h3 id="machine-catalog-and-delivery-group">Machine catalog and delivery group</h3>
<p>The next phase would be creating a machine catalog and delivery group. As there are some prerequisites to creating the machine catalog, I did not manage yet to have a working example. This requires having a running domain, a master VM with the VDA installed, and active cloud connectors. Due to limited time, I have decided to postpone this to a later stage. However, the <a href="https://registry.terraform.io/providers/citrix/citrix/latest/docs/resources/daas_machine_catalog" target="_blank">documentation does provide an example</a>.</p>
<h2 id="github-example">GitHub example</h2>
<p>The code used in this example is published on <a href="https://github.com/RyanBijkerk/terraform-citrix" target="_blank">GitHub</a>, including the Azure requirements. Store the following information in the terraform.tfvars:</p>
<table>
<thead>
<tr>
<th style="text-align: left">Variable</th>
<th style="text-align: left">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">customer_id</td>
<td style="text-align: left">Citrix Customer ID</td>
</tr>
<tr>
<td style="text-align: left">client_id</td>
<td style="text-align: left">Citrix Client ID</td>
</tr>
<tr>
<td style="text-align: left">client_secret</td>
<td style="text-align: left">Citrix Client Secret</td>
</tr>
<tr>
<td style="text-align: left">name</td>
<td style="text-align: left">Name of the resources</td>
</tr>
<tr>
<td style="text-align: left">azure_tenant_id</td>
<td style="text-align: left">Azure Tenant ID</td>
</tr>
<tr>
<td style="text-align: left">azure_subscription_id</td>
<td style="text-align: left">Azure Subscription ID</td>
</tr>
<tr>
<td style="text-align: left">azure_application_id</td>
<td style="text-align: left">Azure Application ID</td>
</tr>
<tr>
<td style="text-align: left">azure_application_secret</td>
<td style="text-align: left">Azure Application Secret</td>
</tr>
</tbody>
</table>
<p><br />
Run the following command in the correct working directory:</p>
<pre><code class="language-cmd">terraform init
terraform apply
</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>It is very exciting that Citrix is creating a Terraform provider, as this will speed up the automated deployments. The provider is a highly requested feature and it is awesome to see Citrix is putting this effort in. At this stage, the tech preview is limited to DaaS only but it is expected to see frequent updates with more functionality. So, it’s definitely something to keep an eye out for on the GitHub release page.</p>
<p>I hope this post gave a brief overview of how to get started with the Citrix Terraform provider, and if you have any questions or comments, please leave them below.</p>
<p>Photo by <a href="https://unsplash.com/@lennykuhne?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash" target="_blank">Lenny Kuhne</a> on <a href="https://unsplash.com/photos/gray-vehicle-being-fixed-inside-factory-using-robot-machines-jHZ70nRk7Ns?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash" target="_blank">Unsplash</a></p>salLast week, Citrix released a Tech preview of the Citrix Terraform provider, which allows you to automate deployments and configurations of Citrix Virtual Apps & Desktops (CVAD) and Citrix Desktop as a Service (DaaS). This is very exciting for those doing infrastructure as code deployment, as this will help speed up the Citrix deployments. This post will explore the provider and its current capabilities.Deploying a Hashicorp Vault in Microsoft Azure via Terraform2023-09-11T00:00:00+00:002023-09-11T00:00:00+00:00https://www.logitblog.com/deploying-a-hashicorp-vault-in-microsoft-azure-via-terraform<p>When implementing infrastructure in the cloud, it is considered a best practice to employ a vault solution for securely storing credentials. HashiCorp offers such a solution known as Vault, which can be deployed via a Docker container. This blog post will demonstrate the process of deploying HashiCorp Vault on Microsoft Azure using Terraform.</p>
<h2 id="what-is-hashicorp-vault">What is Hashicorp Vault?</h2>
<p>Before delving into the details, it is important to grasp the true essence of what Vault represents.</p>
<p>HashiCorp Vault is an identity-based secrets and encryption management system. A <em>secret</em> is anything that you want to tightly control access to, such as API encryption keys, passwords, and certificates. Vault provides encryption services that are gated by authentication and authorization methods. Using Vault’s UI, CLI, or HTTP API, access to secrets and other sensitive data can be securely stored and managed, tightly controlled (restricted), and auditable.</p>
<p>Most enterprises today have credentials sprawled across their organisations. Passwords, API keys, and credentials are stored in plain text, app source code, config files, and other locations. Because these credentials live everywhere, the sprawl can make it difficult and daunting to really know who has access and authorization to what. Having credentials in plain text also increases the potential for malicious attacks, both by internal and external attackers.</p>
<p>Vault was designed with these challenges in mind. Vault takes all of these credentials and centralizes them so that they are defined in one location, which reduces unwanted exposure to credentials. But Vault takes it a few steps further by making sure users, apps, and systems are authenticated and explicitly authorized to access resources, while also providing an audit trail that captures and preserves a history of clients’ actions.</p>
<p>Source: <a href="https://developer.hashicorp.com/vault/docs/what-is-vault" target="_blank">Introduction - Vault - HashiCorp Developer</a></p>
<h2 id="why-use-a-hashicorp-vault-in-microsoft-azure">Why use a Hashicorp Vault in Microsoft Azure?</h2>
<p>What an excellent question, because, as you may already be aware, Azure does offer a built-in solution known as Azure KeyVault. However, in this particular scenario, the choice to utilize HashiCorp Vault is rooted in our multi-cloud strategy. This example draws inspiration from the GO-EUC Infrastructure as Code project, which revolves around deploying test environments across various cloud providers and on-premises setups. All infrastructure deployments are orchestrated using HashiCorp Terraform, and the desired state is configured using Ansible.</p>
<p>As you might be aware, Terraform is cloud-specific, meaning deployment targets are tailored to a specific platform. This, however, isn’t the case with Ansible. Opting for HashiCorp allows us to seamlessly integrate a single Vault solution into Ansible, making it possible to reuse all Ansible playbooks across different platforms. This streamlines operations and reduces overhead significantly.</p>
<h2 id="terraform-configuration">Terraform configuration</h2>
<p>There isn’t a preconfigured Azure offering for deploying HashiCorp Vault through a gallery item. Fortunately, HashiCorp provides a Docker image for Vault, which you can find <a href="https://hub.docker.com/r/hashicorp/vault" target="_blank">here</a> on Docker Hub. This allows us to employ a container instance to host Vault within Azure.</p>
<p>By default, when utilizing the Docker image, it operates in developer mode, essentially running in-memory. Consequently, any added secrets are not stored persistently. Therefore, to transition to a production-ready setup, it becomes necessary to allocate storage for storing both configuration data and Vault files.</p>
<h3 id="vault-config">Vault config</h3>
<p>Let’s begin by discussing the Vault configuration file, which serves as the foundation for Vault operation. Here’s a simple example where TLS is disabled. It’s important to note that in a production environment, it is highly recommended to enable TLS and provide a valid certificate for enhanced security.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>storage "file" {
path = "/etc/vault/file"
}
listener "tcp" {
address = "0.0.0.0:8200"
cluster_address = "0.0.0.0:8201"
tls_disable = true
}
api_addr = "http://0.0.0.0:8200"
ui = true
disable_mlock = true
</code></pre></div></div>
<p>More information about the configuration file can be found here: <a href="https://developer.hashicorp.com/vault/docs/configuration" target="_blank">Server Configuration - Vault - HashiCorp Developer</a></p>
<h3 id="providers">Providers</h3>
<p>Now, let’s break down the Terraform configuration, organized into individual files. Since this deployment is specifically for Azure, the <code class="language-plaintext highlighter-rouge">hashicorp/azurerm</code> module is essential. Additionally, we employ the <code class="language-plaintext highlighter-rouge">hashicorp/random</code> module to generate a random name for the storage account.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>terraform {
required_version = ">= 1.2"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">=2.9"
}
random = {
source = "hashicorp/random"
version = ">=3.4"
}
}
}
provider "azurerm" {
features {}
subscription_id = var.azure_subscription_id
client_id = var.azure_client_id
client_secret = var.azure_client_secret
tenant_id = var.azure_tenant_id
}
</code></pre></div></div>
<h3 id="resource-group">Resource group</h3>
<p>As always, we start with the resource group that encompasses all the various components. The region is determined by a variable specified later in the configuration.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>resource "azurerm_resource_group" "rg" {
name = "golab-backend"
location = var.azure_region
}
</code></pre></div></div>
<h3 id="virtual-network">Virtual Network</h3>
<p>To establish connectivity to the container instance and, consequently, to Vault, a virtual network is a necessity. In this example, we utilize two distinct subnets: one for management purposes and another dedicated exclusively to the container instance.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>resource "azurerm_virtual_network" "vnet" {
name = "golab-vnet"
address_space = ["10.0.0.0/16"]
dns_servers = [cidrhost("10.0.200.0/24", 10)]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_subnet" "mgmt" {
name = "golab-mgmt"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.200.0/24"]
}
resource "azurerm_subnet" "docker" {
name = "golab-docker"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.124.0/24"]
delegation {
name = "dl-docker"
service_delegation {
name = "Microsoft.ContainerInstance/containerGroups"
actions = ["Microsoft.Network/virtualNetworks/subnets/join/action", "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action"]
}
}
lifecycle {
ignore_changes = [
delegation
]
}
}
resource "azurerm_network_profile" "docker" {
name = "np-docker"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
container_network_interface {
name = "nic-docker"
ip_configuration {
name = "ip-docker"
subnet_id = azurerm_subnet.docker.id
}
}
}
</code></pre></div></div>
<h3 id="storage-account">Storage account</h3>
<p>To furnish the configuration file to the container instance while also ensuring Vault’s persistence, we rely on a storage account. Because the storage account name must be unique, we utilize the <code class="language-plaintext highlighter-rouge">random_integer</code> to generate a unique value. Two directories will be established: <code class="language-plaintext highlighter-rouge">config</code>, where the <code class="language-plaintext highlighter-rouge">config.hcl</code> file will be stored, and <code class="language-plaintext highlighter-rouge">file</code>, which serves as the default location for storing all Vault files.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>resource "random_integer" "vault" {
min = 10000
max = 99999
}
resource "azurerm_storage_account" "vault" {
name = "vault${random_integer.vault.result}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "azurerm_storage_share" "vault" {
name = "vault"
storage_account_name = azurerm_storage_account.vault.name
quota = 50
lifecycle {
ignore_changes = all
}
}
resource "azurerm_storage_share_directory" "config-dir" {
name = "config"
share_name = azurerm_storage_share.vault.name
storage_account_name = azurerm_storage_account.vault.name
lifecycle {
ignore_changes = all
}
}
resource "azurerm_storage_share_directory" "file-dir" {
name = "file"
share_name = azurerm_storage_share.vault.name
storage_account_name = azurerm_storage_account.vault.name
lifecycle {
ignore_changes = all
}
}
resource "azurerm_storage_share_file" "file" {
name = "config.hcl"
path = azurerm_storage_share_directory.config-dir.name
storage_share_id = azurerm_storage_share.vault.id
source = "./config/vault/config.hcl"
lifecycle {
ignore_changes = all
}
}
</code></pre></div></div>
<h3 id="container-instance">Container instance</h3>
<p>The container instance will utilize the public and latest available version of the <code class="language-plaintext highlighter-rouge">hashicorp/vault</code> image from DockerHub. Depending on the resource requirements, it’s possible to allocate more CPU and memory. A volume mount will be configured to map to the storage account where both the configuration and persistent files are stored. The command issued will run Vault in production mode, with the mapping to the specified configuration.</p>
<p>The default port, 8200, will also be mapped. Notably, the <code class="language-plaintext highlighter-rouge">ip_address_type</code> is set to private, meaning that Vault is only accessible from within the virtual network. It’s worth mentioning that this example does not include a virtual machine, so adding one would be contingent on your specific use-case.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>resource "azurerm_container_group" "docker" {
name = "golab-docker"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_address_type = "Private"
network_profile_id = azurerm_network_profile.docker.id
os_type = "Linux"
container {
name = "vault"
image = "hashicorp/vault:latest"
cpu = "1"
memory = "2"
commands = [ "/bin/sh", "-c", "vault server -config=/etc/vault/config/config.hcl" ]
ports {
port = 8200
protocol = "TCP"
}
volume {
name = "vault"
mount_path = "/etc/vault"
read_only = false
share_name = azurerm_storage_share.vault.name
storage_account_name = azurerm_storage_account.vault.name
storage_account_key = azurerm_storage_account.vault.primary_access_key
}
}
}
</code></pre></div></div>
<h3 id="variables">Variables</h3>
<p>Lastly, let’s cover the essential variables required for this configuration.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>variable "azure_subscription_id" {
type = string
sensitive = true
}
variable "azure_client_id" {
type = string
sensitive = true
}
variable "azure_client_secret" {
type = string
sensitive = true
}
variable "azure_tenant_id" {
type = string
sensitive = true
}
variable "azure_region" {
type = string
default = "westeurope"
}
</code></pre></div></div>
<p>You can find the complete code sample on my <a href="https://github.com/RyanBijkerk/vault-in-azure" target="_blank">GitHub page</a>.</p>
<h2 id="end-result-and-conclusion">End result and conclusion</h2>
<p>When you apply the configuration from this example, it will result in the following deployment.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# azurerm_container_group.docker will be created
+ resource "azurerm_container_group" "docker" {
+ dns_name_label_reuse_policy = "Unsecure"
+ exposed_port = (known after apply)
+ fqdn = (known after apply)
+ id = (known after apply)
+ ip_address = (known after apply)
+ ip_address_type = "Private"
+ location = "westeurope"
+ name = "golab-docker"
+ network_profile_id = (known after apply)
+ os_type = "Linux"
+ resource_group_name = "golab-backend"
+ restart_policy = "Always"
+ sku = "Standard"
+ container {
+ commands = [
+ "/bin/sh",
+ "-c",
+ "vault server -config=/etc/vault/config/config.hcl",
]
+ cpu = 1
+ image = "hashicorp/vault:latest"
+ memory = 2
+ name = "vault"
+ ports {
+ port = 8200
+ protocol = "TCP"
}
+ volume {
+ empty_dir = false
+ mount_path = "/etc/vault"
+ name = "vault"
+ read_only = false
+ share_name = "vault"
+ storage_account_key = (sensitive value)
+ storage_account_name = (known after apply)
}
}
}
# azurerm_network_profile.docker will be created
+ resource "azurerm_network_profile" "docker" {
+ container_network_interface_ids = (known after apply)
+ id = (known after apply)
+ location = "westeurope"
+ name = "np-docker"
+ resource_group_name = "golab-backend"
+ container_network_interface {
+ name = "nic-docker"
+ ip_configuration {
+ name = "ip-docker"
+ subnet_id = (known after apply)
}
}
}
# azurerm_resource_group.rg will be created
+ resource "azurerm_resource_group" "rg" {
+ id = (known after apply)
+ location = "westeurope"
+ name = "golab-backend"
}
# azurerm_storage_account.vault will be created
+ resource "azurerm_storage_account" "vault" {
+ access_tier = (known after apply)
+ account_kind = "StorageV2"
+ account_replication_type = "LRS"
+ account_tier = "Standard"
+ allow_nested_items_to_be_public = true
+ cross_tenant_replication_enabled = true
+ default_to_oauth_authentication = false
+ enable_https_traffic_only = true
+ id = (known after apply)
+ infrastructure_encryption_enabled = false
+ is_hns_enabled = false
+ large_file_share_enabled = (known after apply)
+ location = "westeurope"
+ min_tls_version = "TLS1_2"
+ name = (known after apply)
+ nfsv3_enabled = false
+ primary_access_key = (sensitive value)
+ primary_blob_connection_string = (sensitive value)
+ primary_blob_endpoint = (known after apply)
+ primary_blob_host = (known after apply)
+ primary_connection_string = (sensitive value)
+ primary_dfs_endpoint = (known after apply)
+ primary_dfs_host = (known after apply)
+ primary_file_endpoint = (known after apply)
+ primary_file_host = (known after apply)
+ primary_location = (known after apply)
+ primary_queue_endpoint = (known after apply)
+ primary_queue_host = (known after apply)
+ primary_table_endpoint = (known after apply)
+ primary_table_host = (known after apply)
+ primary_web_endpoint = (known after apply)
+ primary_web_host = (known after apply)
+ public_network_access_enabled = true
+ queue_encryption_key_type = "Service"
+ resource_group_name = "golab-backend"
+ secondary_access_key = (sensitive value)
+ secondary_blob_connection_string = (sensitive value)
+ secondary_blob_endpoint = (known after apply)
+ secondary_blob_host = (known after apply)
+ secondary_connection_string = (sensitive value)
+ secondary_dfs_endpoint = (known after apply)
+ secondary_dfs_host = (known after apply)
+ secondary_file_endpoint = (known after apply)
+ secondary_file_host = (known after apply)
+ secondary_location = (known after apply)
+ secondary_queue_endpoint = (known after apply)
+ secondary_queue_host = (known after apply)
+ secondary_table_endpoint = (known after apply)
+ secondary_table_host = (known after apply)
+ secondary_web_endpoint = (known after apply)
+ secondary_web_host = (known after apply)
+ sftp_enabled = false
+ shared_access_key_enabled = true
+ table_encryption_key_type = "Service"
+ blob_properties {
+ change_feed_enabled = (known after apply)
+ change_feed_retention_in_days = (known after apply)
+ default_service_version = (known after apply)
+ last_access_time_enabled = (known after apply)
+ versioning_enabled = (known after apply)
+ container_delete_retention_policy {
+ days = (known after apply)
}
+ cors_rule {
+ allowed_headers = (known after apply)
+ allowed_methods = (known after apply)
+ allowed_origins = (known after apply)
+ exposed_headers = (known after apply)
+ max_age_in_seconds = (known after apply)
}
+ delete_retention_policy {
+ days = (known after apply)
}
+ restore_policy {
+ days = (known after apply)
}
}
+ network_rules {
+ bypass = (known after apply)
+ default_action = (known after apply)
+ ip_rules = (known after apply)
+ virtual_network_subnet_ids = (known after apply)
+ private_link_access {
+ endpoint_resource_id = (known after apply)
+ endpoint_tenant_id = (known after apply)
}
}
+ queue_properties {
+ cors_rule {
+ allowed_headers = (known after apply)
+ allowed_methods = (known after apply)
+ allowed_origins = (known after apply)
+ exposed_headers = (known after apply)
+ max_age_in_seconds = (known after apply)
}
+ hour_metrics {
+ enabled = (known after apply)
+ include_apis = (known after apply)
+ retention_policy_days = (known after apply)
+ version = (known after apply)
}
+ logging {
+ delete = (known after apply)
+ read = (known after apply)
+ retention_policy_days = (known after apply)
+ version = (known after apply)
+ write = (known after apply)
}
+ minute_metrics {
+ enabled = (known after apply)
+ include_apis = (known after apply)
+ retention_policy_days = (known after apply)
+ version = (known after apply)
}
}
+ routing {
+ choice = (known after apply)
+ publish_internet_endpoints = (known after apply)
+ publish_microsoft_endpoints = (known after apply)
}
+ share_properties {
+ cors_rule {
+ allowed_headers = (known after apply)
+ allowed_methods = (known after apply)
+ allowed_origins = (known after apply)
+ exposed_headers = (known after apply)
+ max_age_in_seconds = (known after apply)
}
+ retention_policy {
+ days = (known after apply)
}
+ smb {
+ authentication_types = (known after apply)
+ channel_encryption_type = (known after apply)
+ kerberos_ticket_encryption_type = (known after apply)
+ multichannel_enabled = (known after apply)
+ versions = (known after apply)
}
}
}
# azurerm_storage_share.vault will be created
+ resource "azurerm_storage_share" "vault" {
+ access_tier = (known after apply)
+ enabled_protocol = "SMB"
+ id = (known after apply)
+ metadata = (known after apply)
+ name = "vault"
+ quota = 50
+ resource_manager_id = (known after apply)
+ storage_account_name = (known after apply)
+ url = (known after apply)
}
# azurerm_storage_share_directory.config-dir will be created
+ resource "azurerm_storage_share_directory" "config-dir" {
+ id = (known after apply)
+ name = "config"
+ share_name = "vault"
+ storage_account_name = (known after apply)
}
# azurerm_storage_share_directory.file-dir will be created
+ resource "azurerm_storage_share_directory" "file-dir" {
+ id = (known after apply)
+ name = "file"
+ share_name = "vault"
+ storage_account_name = (known after apply)
}
# azurerm_storage_share_file.file will be created
+ resource "azurerm_storage_share_file" "file" {
+ content_length = (known after apply)
+ content_type = "application/octet-stream"
+ id = (known after apply)
+ name = "config.hcl"
+ path = "config"
+ source = "./config/vault/config.hcl"
+ storage_share_id = (known after apply)
}
# azurerm_subnet.docker will be created
+ resource "azurerm_subnet" "docker" {
+ address_prefixes = [
+ "10.0.124.0/24",
]
+ enforce_private_link_endpoint_network_policies = (known after apply)
+ enforce_private_link_service_network_policies = (known after apply)
+ id = (known after apply)
+ name = "golab-docker"
+ private_endpoint_network_policies_enabled = (known after apply)
+ private_link_service_network_policies_enabled = (known after apply)
+ resource_group_name = "golab-backend"
+ virtual_network_name = "golab-vnet"
+ delegation {
+ name = "dl-docker"
+ service_delegation {
+ actions = [
+ "Microsoft.Network/virtualNetworks/subnets/join/action",
+ "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action",
]
+ name = "Microsoft.ContainerInstance/containerGroups"
}
}
}
# azurerm_subnet.mgmt will be created
+ resource "azurerm_subnet" "mgmt" {
+ address_prefixes = [
+ "10.0.200.0/24",
]
+ enforce_private_link_endpoint_network_policies = (known after apply)
+ enforce_private_link_service_network_policies = (known after apply)
+ id = (known after apply)
+ name = "golab-mgmt"
+ private_endpoint_network_policies_enabled = (known after apply)
+ private_link_service_network_policies_enabled = (known after apply)
+ resource_group_name = "golab-backend"
+ virtual_network_name = "golab-vnet"
}
# azurerm_virtual_network.vnet will be created
+ resource "azurerm_virtual_network" "vnet" {
+ address_space = [
+ "10.0.0.0/16",
]
+ dns_servers = [
+ "10.0.200.10",
]
+ guid = (known after apply)
+ id = (known after apply)
+ location = "westeurope"
+ name = "golab-vnet"
+ resource_group_name = "golab-backend"
+ subnet = (known after apply)
}
# random_integer.vault will be created
+ resource "random_integer" "vault" {
+ id = (known after apply)
+ max = 99999
+ min = 10000
+ result = (known after apply)
}
Plan: 12 to add, 0 to change, 0 to destroy.
</code></pre></div></div>
<p><img src="/assets/images/posts/2023-09-11-deploying-a-hashicorp-vault-in-microsoft-azure-via-terraform/azure-backend-deployment.png" alt="azure-backend-deployment" /></p>
<p>As previously mentioned, the Vault is only accessible from within the virtual network.</p>
<p><img src="/assets/images/posts/2023-09-11-deploying-a-hashicorp-vault-in-microsoft-azure-via-terraform/hashicorp-vault-ui.png" alt="hashicorp-vault-ui" /></p>
<p>When selecting a Vault solution, it is of paramount importance to have a clear understanding of the long-term goals and assess whether the chosen solution aligns with those objectives. In the case of GO-EUC, the utilization of multiple cloud solutions depends on research requirements. To support all deployment types and facilitate the reuse of Ansible configurations, HashiCorp Vault emerges as the most suitable choice. It can seamlessly operate on various platforms, including on-premises setups, enabling a consistent method for storing and retrieving secrets. Furthermore, HashiCorp Vault is an open-source Docker instance, making it freely accessible for use.</p>
<p>Additionally, HashiCorp offers an enterprise solution where the Vault is hosted for you. For more information, please visit the HashiCorp website.</p>
<p>If you have any questions or comments, please feel free to leave them below.</p>
<p>Photo by <a href="https://unsplash.com/@isthatbrock?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Brock Wegner</a> on <a href="https://unsplash.com/photos/3ROwc3JSjCk?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Unsplash</a></p>salWhen implementing infrastructure in the cloud, it is considered a best practice to employ a vault solution for securely storing credentials. HashiCorp offers such a solution known as Vault, which can be deployed via a Docker container. This blog post will demonstrate the process of deploying HashiCorp Vault on Microsoft Azure using Terraform.Mastering Mocking with Pester: CommandNotFoundException2023-08-18T00:00:00+00:002023-08-18T00:00:00+00:00https://www.logitblog.com/mastering-mocking-with-pester-commandnotfoundexception<p>Unit testing serves as a crucial method to ensure the quality of code. This principle is just as applicable to PowerShell through the utilization of Pester. Pester, the ubiquitous testing and mocking framework for PowerShell, becomes an indispensable tool in this context. However, what should one do when you encounter CommandNotFoundException? In this blog post, we’ll explore this specific scenario and explain the resolution within your Pester tests.</p>
<h2 id="grasping-mocking-in-pester">Grasping Mocking in Pester</h2>
<p>For those unfamiliar with Pester and its mocking capabilities, here’s a concise introduction to Pester’s mocking features. As outlined in the documentation, Pester offers a set of mocking functions that streamline the process of simulating dependencies and validating behaviors. These functions enable you to effectively “shim” data layers or mock intricate functions that already possess their own tests.</p>
<p>Source: <a href="https://pester.dev/docs/commands/Mock" target="_blank">Mock</a></p>
<p>When conducting code tests, the objective is to validate the logic of a script or function without actually executing it. Imagine a scenario where a script is designed to delete user accounts from Microsoft Entra, which is the new name for Azure Active Directory. Naturally, during testing, you wouldn’t want actual deletions to occur. Mocking a commandlet replaces its genuine execution during tests, granting you the ability to manipulate behaviors and create diverse test scenarios.</p>
<p>To illustrate, consider the following mock commandlet:</p>
<pre><code class="language-PowerShell">Mock Get-Content {"This is a mocked example"}
</code></pre>
<p>In this example, any invocation of <code class="language-plaintext highlighter-rouge">Get-Content</code> during test execution returns the string “This is a mocked example.”</p>
<h2 id="the-challenge-and-its-solution">The Challenge and Its Solution</h2>
<p>A prerequisite when mocking commandlets or functions is the availability of the actual commandlet. Let’s revisit the previous example. I’ve constructed a simple script that removes an Azure AD User:</p>
<pre><code class="language-PowerShell">[CmdletBinding()]
param (
[Parameter()]
[string]
$UserEmail
)
Get-AzureADUser -ObjectId $UserEmail | Remove-AzureAdUser -Force
</code></pre>
<p>To test this, I’ve created a concise Pester test:</p>
<pre><code class="language-PowerShell">Describe "Azure script" {
Context "When parameters are correct" {
It "Should remove the Azure AD User" {
Mock Get-AzureADUser {}
Mock Remove-AzureAdUser {}
{ .\Azure.ps1 -UserEmail "ryan@go.euc"} | Should -Not -Throw
}
}
}
</code></pre>
<p>Running this test on a machine without the AzureAD module yields the following error:</p>
<pre><code class="language-PowerShell">Starting discovery in 1 files.
Discovery found 1 tests in 71ms.
Running tests.
[-] Azure script.When parameters are correct.Should remove the Azure AD User 30ms (22ms|8ms)
CommandNotFoundException: Could not find Command Get-AzureADUser
Tests completed in 198ms
Tests Passed: 0, Failed: 1, Skipped: 0 NotRun: 0
</code></pre>
<p>To address this, the <code class="language-plaintext highlighter-rouge">Get-AzureAdUser</code> and <code class="language-plaintext highlighter-rouge">Remove-AzureAdUser</code> commands need to be available. Two approaches can achieve this.</p>
<p>The simpler approach involves installing and loading the module in the testing environment. However, circumstances may occur where installation is unfeasible or, as in this example, loading the module isn’t possible. Personally, I work on a Macbook Pro M2, and attempting to load the module leads to the following error:</p>
<pre><code class="language-PowerShell">Import-Module: The current processor architecture is: MSIL. The module '/Users/ryan/.local/share/powershell/Modules/AzureAD/2.0.2.182/AzureAD.psd1' requires the following architecture: Amd64.
</code></pre>
<p>To bypass this, dummy functions can be crafted and defined in the <code class="language-plaintext highlighter-rouge">BeforeAll</code> block. This approach makes the commandlet available, enabling Pester to mock it:</p>
<pre><code class="language-PowerShell">BeforeAll {
Function Get-AzureADUser {
}
Function Remove-AzureAdUser {
}
}
</code></pre>
<p>The end result, in this instance, is as follows:</p>
<pre><code class="language-PowerShell">Running tests from '/Users/ryan/Development/PowerShell/Azure.Test.ps1'
Describing Azure script
Context When parameters are correct
[+] Should remove the Azure AD User 12ms (6ms|6ms)
Tests completed in 35ms
Tests Passed: 1, Failed: 0, Skipped: 0 NotRun: 0
</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>Pester is a robust framework empowering comprehensive testing of PowerShell code, thereby enhancing overall quality. Did you know that you can also incorporate Pester within your Azure DevOps pipeline results? Additional details can be found here: <a href="https://www.logitblog.com/increase-the-success-rate-of-azure-devops-pipelines-using-pester/" target="_blank">Logit Blog - Increase the success rate of Azure DevOps pipelines using Pester</a></p>
<p>Should you encounter a <code class="language-plaintext highlighter-rouge">CommandNotFoundException</code> error during a Pester test, it simply indicates the unavailability of the commandlet. You can address this by either creating a basic dummy function within the test or ensuring the module’s availability.</p>
<p>Credit to <a href="https://www.linkedin.com/in/niels-reloe-12b67a66/" target="_blank">Niels Reloe</a>. Together, we resolved this issue and continued our journey toward achieving 100% code coverage!</p>
<p>I trust this explanation has been helpful. Feel free to leave any questions in the comments section below.</p>
<p>Photo by <a href="https://unsplash.com/@anil_sharma_india?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Anil Sharma</a> on <a href="https://unsplash.com/photos/fE3uYk0Ri1U?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Unsplash</a></p>salUnit testing serves as a crucial method to ensure the quality of code. This principle is just as applicable to PowerShell through the utilization of Pester. Pester, the ubiquitous testing and mocking framework for PowerShell, becomes an indispensable tool in this context. However, what should one do when you encounter CommandNotFoundException? In this blog post, we’ll explore this specific scenario and explain the resolution within your Pester tests. Grasping Mocking in Pester For those unfamiliar with Pester and its mocking capabilities, here’s a concise introduction to Pester’s mocking features. As outlined in the documentation, Pester offers a set of mocking functions that streamline the process of simulating dependencies and validating behaviors. These functions enable you to effectively “shim” data layers or mock intricate functions that already possess their own tests.Ansible: using the Evergreen API for collecting Windows apps2023-02-02T00:00:00+00:002023-02-02T00:00:00+00:00https://www.logitblog.com/ansible-using-the-evergreen-api-for-collecting-windows-apps<p>Obtaining the latest version of an application can often be a challenge. To address this issue, tools and projects such as Chocolaty, WinGet, or Evergreen were created. However, there may be situations where using these solutions is not permitted, and manual management is required. The Evergreen API enables you to retrieve information on the latest version of an application, while still managing the download and installation process yourself. This blog post will demonstrate an example of using the new Evergreen API in an Ansible playbook.</p>
<h2 id="what-is-evergreen">What is Evergreen?</h2>
<p><a href="https://stealthpuppy.com/evergreen" target="_blank">Evergreen</a> is a PowerShell module that provides the latest version numbers and download URLs for common Windows applications. The module features simple functions for tasks such as:</p>
<p>Retrieving the latest version of an application for comparison with a previously installed or downloaded version.
Returning the URL for the latest version of the application for local installation or deployment to target machines.</p>
<p>This project is managed by <a href="https://twitter.com/stealthpuppy" target="_blank">Aaron Packer</a>, also known as stealthpuppy, and is available on <a href="https://github.com/aaronparker/evergreen" target="_blank">GitHub to the public</a>.</p>
<p>Recently, Aaron introduced the Evergreen API in a tweet:</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">A new version of <a href="https://twitter.com/hashtag/Evergreen?src=hash&ref_src=twsrc%5Etfw">#Evergreen</a> to be pushed to the PowerShell Gallery soon, but more importantly, I've marked the Evergreen API as version 1.0.0 - now you can use Evergreen without having to install the module <a href="https://t.co/s2AwXriiZN">https://t.co/s2AwXriiZN</a></p>— Aaron Parker (@stealthpuppy) <a href="https://twitter.com/stealthpuppy/status/1618559925424357376?ref_src=twsrc%5Etfw">January 26, 2023</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>As stated in the tweet, the API allows for the use of Evergreen without installing the necessary PowerShell modules. This is a valuable addition to the project, as it enables individuals like myself to utilize Evergreen while still maintaining control over the installation process.</p>
<h2 id="exploring-the-evergreen-api">Exploring the Evergreen API</h2>
<p>The following page contains documentation on how to use the Evergreen API: <a href="https://stealthpuppy.com/evergreen/invoke/" target="_blank">How to use the Evergreen API - Evergreen (stealthpuppy.com)</a></p>
<p>A Swagger page is also available, enabling you to directly execute the API and view the results within the Swagger interface. In addition to clear documentation, the Swagger interface is useful in gaining a better understanding of the API structure.</p>
<p><a href="https://app.swaggerhub.com/apis/stealthpuppy/evergreen-api/1.0.0" target="_blank">evergreen-api - 1.0.0 - stealthpuppy - SwaggerHub</a></p>
<p>There are two basic API calls:</p>
<ul>
<li>/apps, which returns all available applications.</li>
<li>/app/{appName}, which returns the details of a specific application.</li>
</ul>
<p>Both API calls are publicly accessible and do not require any form of authentication.</p>
<p>The schema of an application will always include a version and URI. The URI will contain the download link for the setup file. It is important to note that the other properties may vary among different applications. For example:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"Version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"22.01"</span><span class="p">,</span><span class="w">
</span><span class="nl">"Architecture"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ARM32"</span><span class="p">,</span><span class="w">
</span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"exe"</span><span class="p">,</span><span class="w">
</span><span class="nl">"URI"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://nchc.dl.sourceforge.net/project/sevenzip/7-Zip/22.01/7z2201-arm.exe"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"Version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"22.003.20314"</span><span class="p">,</span><span class="w">
</span><span class="nl">"Language"</span><span class="p">:</span><span class="w"> </span><span class="s2">"MUI"</span><span class="p">,</span><span class="w">
</span><span class="nl">"Architecture"</span><span class="p">:</span><span class="w"> </span><span class="s2">"x64"</span><span class="p">,</span><span class="w">
</span><span class="nl">"Name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Reader DC 2022.003.20314 MUI for Windows-64bit"</span><span class="p">,</span><span class="w">
</span><span class="nl">"URI"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://ardownload2.adobe.com/pub/adobe/acrobat/win/AcrobatDC/2200320314/AcroRdrDCx642200320314_MUI.exe"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<h2 id="using-the-evergreen-api-in-an-ansible-playbook">Using the Evergreen API in an Ansible playbook</h2>
<p>The following demonstrates how to utilize the Evergreen API in practice. This example focuses on retrieving the latest version of Microsoft Visual Studio Code (since it’s the best editor ever!).</p>
<p>When accessing the information through the Swagger API, it becomes apparent that multiple installers are available. To retrieve the desired version, filtering is necessary based on specific criteria:</p>
<ul>
<li>Architecture: x64</li>
<li>Channel: Stable</li>
<li>Platform: win32-x64</li>
</ul>
<p>The output will be the installer for the 64-bit machine. The playbook will follow these steps:</p>
<ul>
<li>Collect the available version</li>
<li>Apply filter</li>
<li>Install application</li>
</ul>
<p>The API call can be executed and the required information can be collected by utilizing the win_uri 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="s2">"</span><span class="s">Collect</span><span class="nv"> </span><span class="s">Evergreen</span><span class="nv"> </span><span class="s">info</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">app_name</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">win_uri</span><span class="pi">:</span>
<span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://evergreen-api.stealthpuppy.com/app/{{</span><span class="nv"> </span><span class="s">app_name</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">method</span><span class="pi">:</span> <span class="s">GET</span>
<span class="na">content_type</span><span class="pi">:</span> <span class="s">application/json</span>
<span class="na">return_content</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">register</span><span class="pi">:</span> <span class="s">app</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">app.json</code> return value will be equivalent to the results of the Swagger API, which is a list of available installers.</p>
<p>A json_query filter can be applied to obtain the correct version:</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="s2">"</span><span class="s">Set</span><span class="nv"> </span><span class="s">Evergreen</span><span class="nv"> </span><span class="s">object</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">app_name</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">set_fact</span><span class="pi">:</span>
<span class="na">app_details</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">app.json</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">json_query('[?Architecture</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">`x64`</span><span class="nv"> </span><span class="s">&&</span><span class="nv"> </span><span class="s">Channel</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">`Stable`</span><span class="nv"> </span><span class="s">&&</span><span class="nv"> </span><span class="s">Platform</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">`win32-x64`]')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">first</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div></div>
<p>By selecting the first result, only one property will be returned, ensuring that app_details is a single object rather than a list.</p>
<p>Based on the details, the application can be installed:</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="s2">"</span><span class="s">Install</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">app_name</span><span class="nv"> </span><span class="s">}}</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">app_details.Version</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">win_package</span><span class="pi">:</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">app_details.URI</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">creates_path</span><span class="pi">:</span> <span class="s">C:\Program Files\Microsoft VS Code\Code.exe</span>
<span class="na">arguments</span><span class="pi">:</span> <span class="s">/VERYSILENT /NORESTART /MERGETASKS=!runcode</span>
<span class="na">expected_return_code</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">0</span><span class="pi">]</span>
<span class="na">state</span><span class="pi">:</span> <span class="s">present</span>
</code></pre></div></div>
<p>The application can then be installed based on the details. The win_package path can be a URL, allowing for direct download of the setup from the internet. It is important to note that an internet connection is required for both using the API and downloading the setup.
Example playbook:</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">name</span><span class="pi">:</span> <span class="s">Test Evergreen</span>
<span class="na">hosts</span><span class="pi">:</span> <span class="s">mngt</span>
<span class="na">vars_files</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">ansible.yml</span>
<span class="pi">-</span> <span class="s">domain.yml</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">Update fact to MicrosoftVisualStudioCode</span>
<span class="na">set_fact</span><span class="pi">:</span>
<span class="na">app_name</span><span class="pi">:</span> <span class="s">MicrosoftVisualStudioCode</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Collect</span><span class="nv"> </span><span class="s">Evergreen</span><span class="nv"> </span><span class="s">info</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">app_name</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">win_uri</span><span class="pi">:</span>
<span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://evergreen-api.stealthpuppy.com/app/{{</span><span class="nv"> </span><span class="s">app_name</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">method</span><span class="pi">:</span> <span class="s">GET</span>
<span class="na">content_type</span><span class="pi">:</span> <span class="s">application/json</span>
<span class="na">return_content</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">register</span><span class="pi">:</span> <span class="s">app</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Set</span><span class="nv"> </span><span class="s">Evergreen</span><span class="nv"> </span><span class="s">object</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">app_name</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">set_fact</span><span class="pi">:</span>
<span class="na">app_details</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">app.json</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">json_query('[?Architecture</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">`x64`</span><span class="nv"> </span><span class="s">&&</span><span class="nv"> </span><span class="s">Channel</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">`Stable`</span><span class="nv"> </span><span class="s">&&</span><span class="nv"> </span><span class="s">Platform</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">`win32-x64`]')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">first</span><span class="nv"> </span><span class="s">}}"</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Install</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">app_name</span><span class="nv"> </span><span class="s">}}</span><span class="nv"> </span><span class="s">{{</span><span class="nv"> </span><span class="s">app_details.Version</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">win_package</span><span class="pi">:</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">app_details.URI</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">creates_path</span><span class="pi">:</span> <span class="s">C:\Program Files\Microsoft VS Code\Code.exe</span>
<span class="na">arguments</span><span class="pi">:</span> <span class="s">/VERYSILENT /NORESTART /MERGETASKS=!runcode</span>
<span class="na">expected_return_code</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">0</span><span class="pi">]</span>
<span class="na">state</span><span class="pi">:</span> <span class="s">present</span>
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>Evergreen was already a robust project, but with the addition of the Evergreen API, it can be consumed in even more versatile ways. This blog post demonstrated an example of integrating the API in an Ansible playbook, showcasing its benefits.</p>
<p>However, there are a few areas for improvement. The “app” object returns various properties that vary between applications. Consistency across all packages would be more advantageous, as it would allow for the use of a single filter, for example. At the time of writing, there is also no information in the API regarding the required installation parameters. Including this information in the API would be valuable, so you don’t have to search for it elsewhere in the project.</p>
<p>Despite these areas for improvement, I am truly impressed with the Evergreen API. I was able to create this example from scratch within 15 minutes. Keep up the great work, Aaron, and I look forward to following the progress of your project.</p>
<p>Photo by <a href="https://unsplash.com/@fabioha?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">fabio</a> on <a href="https://unsplash.com/photos/oyXis2kALVg?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Unsplash</a></p>salObtaining the latest version of an application can often be a challenge. To address this issue, tools and projects such as Chocolaty, WinGet, or Evergreen were created. However, there may be situations where using these solutions is not permitted, and manual management is required. The Evergreen API enables you to retrieve information on the latest version of an application, while still managing the download and installation process yourself. This blog post will demonstrate an example of using the new Evergreen API in an Ansible playbook.Ansible: combining loop results in a single list2023-01-28T00:00:00+00:002023-01-28T00:00:00+00:00https://www.logitblog.com/ansible-combining-loop-results-in%20a-single-list<p>Sometimes, you may encounter a scenario that appears to be straightforward, but when it comes to coding, you find yourself quickly becoming frustrated. One such challenge is combining the results of multiple loops into a single list. It can take some time to find the right solution, and I thought it would be useful to document the process. This blog post will explain the challenge in detail and provide a solution for combining the results of multiple loops into a single list.</p>
<h2 id="the-challenge">The challenge</h2>
<p>The goal of this challenge is to collect data from multiple locations and combine it into a single list for further processing. This example will focus on collecting information on a Windows-based system, but remember that the solution can be applied to both Windows and Linux systems.</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">Test on the management server</span>
<span class="na">hosts</span><span class="pi">:</span> <span class="s">mngt</span>
<span class="na">vars_files</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">ansible.yml</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">collect dirs</span>
<span class="na">win_find</span><span class="pi">:</span>
<span class="na">paths</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">item</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">patterns</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">*.exe'</span><span class="pi">]</span>
<span class="na">register</span><span class="pi">:</span> <span class="s">find</span>
<span class="na">loop</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">C:\Program Files (x86)\LoadGen\LoadGen Agent</span>
<span class="pi">-</span> <span class="s">C:\Program Files\Microsoft VS Code</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">show results</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">{{</span><span class="nv"> </span><span class="s">find</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div></div>
<p>The variable ‘find’ will contain the following information. Note that for improved readability, some objects have been removed from the output.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">ok</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">mngt-1</span><span class="pi">]</span> <span class="s">=> {</span>
<span class="s">"msg"</span><span class="pi">:</span> <span class="pi">{</span>
<span class="s2">"</span><span class="s">changed"</span><span class="pi">:</span> <span class="nv">false</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">msg"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">All</span><span class="nv"> </span><span class="s">items</span><span class="nv"> </span><span class="s">completed"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">results"</span><span class="pi">:</span> <span class="pi">[</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">ansible_loop_var"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">item"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">files"</span><span class="pi">:</span> <span class="pi">[</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent</span><span class="se">\\</span><span class="s">DNKLocalClient.exe"</span>
<span class="pi">},</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent</span><span class="se">\\</span><span class="s">DNKRDExec.exe"</span>
<span class="pi">},</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent</span><span class="se">\\</span><span class="s">DNKVMwareView.exe"</span>
<span class="pi">},</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent</span><span class="se">\\</span><span class="s">DNKWVD.exe"</span>
<span class="pi">},</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent</span><span class="se">\\</span><span class="s">DNKXenAppExec.exe"</span>
<span class="pi">},</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent</span><span class="se">\\</span><span class="s">DNKXenAppExecEnhanced.exe"</span>
<span class="pi">},</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Firewall.exe"</span>
<span class="pi">},</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">LoadBot</span><span class="nv"> </span><span class="s">Agent.exe"</span>
<span class="pi">},</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent</span><span class="se">\\</span><span class="s">TestLoadBot.exe"</span>
<span class="pi">}</span>
<span class="pi">],</span>
<span class="s2">"</span><span class="s">invocation"</span><span class="pi">:</span> <span class="pi">{</span>
<span class="s2">"</span><span class="s">module_args"</span><span class="pi">:</span> <span class="pi">{</span>
<span class="s2">"</span><span class="s">age"</span><span class="pi">:</span> <span class="nv">null</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">age_stamp"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mtime"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">checksum_algorithm"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">sha1"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">file_type"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">file"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">follow"</span><span class="pi">:</span> <span class="nv">false</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">get_checksum"</span><span class="pi">:</span> <span class="nv">true</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">hidden"</span><span class="pi">:</span> <span class="nv">false</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">paths"</span><span class="pi">:</span> <span class="pi">[</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent"</span>
<span class="pi">],</span>
<span class="s2">"</span><span class="s">patterns"</span><span class="pi">:</span> <span class="pi">[</span>
<span class="s2">"</span><span class="s">*.exe"</span>
<span class="pi">],</span>
<span class="s2">"</span><span class="s">recurse"</span><span class="pi">:</span> <span class="nv">false</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">size"</span><span class="pi">:</span> <span class="nv">null</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">use_regex"</span><span class="pi">:</span> <span class="nv">false</span>
<span class="pi">}</span>
<span class="pi">},</span>
<span class="s2">"</span><span class="s">item"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="nv"> </span><span class="s">(x86)</span><span class="se">\\</span><span class="s">LoadGen</span><span class="se">\\</span><span class="s">LoadGen</span><span class="nv"> </span><span class="s">Agent"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">matched"</span><span class="pi">:</span> <span class="nv">9</span>
<span class="pi">},</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">ansible_loop_var"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">item"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">changed"</span><span class="pi">:</span> <span class="nv">false</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">examined"</span><span class="pi">:</span> <span class="nv">23</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">failed"</span><span class="pi">:</span> <span class="nv">false</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">files"</span><span class="pi">:</span> <span class="pi">[</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Microsoft</span><span class="nv"> </span><span class="s">VS</span><span class="nv"> </span><span class="s">Code</span><span class="se">\\</span><span class="s">Code.exe"</span>
<span class="pi">},</span>
<span class="pi">{</span>
<span class="s2">"</span><span class="s">path"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Microsoft</span><span class="nv"> </span><span class="s">VS</span><span class="nv"> </span><span class="s">Code</span><span class="se">\\</span><span class="s">unins000.exe"</span>
<span class="pi">}</span>
<span class="pi">],</span>
<span class="s2">"</span><span class="s">invocation"</span><span class="pi">:</span> <span class="pi">{</span>
<span class="s2">"</span><span class="s">module_args"</span><span class="pi">:</span> <span class="pi">{</span>
<span class="s2">"</span><span class="s">age"</span><span class="pi">:</span> <span class="nv">null</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">age_stamp"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mtime"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">checksum_algorithm"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">sha1"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">file_type"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">file"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">follow"</span><span class="pi">:</span> <span class="nv">false</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">get_checksum"</span><span class="pi">:</span> <span class="nv">true</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">hidden"</span><span class="pi">:</span> <span class="nv">false</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">paths"</span><span class="pi">:</span> <span class="pi">[</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Microsoft</span><span class="nv"> </span><span class="s">VS</span><span class="nv"> </span><span class="s">Code"</span>
<span class="pi">],</span>
<span class="s2">"</span><span class="s">patterns"</span><span class="pi">:</span> <span class="pi">[</span>
<span class="s2">"</span><span class="s">*.exe"</span>
<span class="pi">],</span>
<span class="s2">"</span><span class="s">recurse"</span><span class="pi">:</span> <span class="nv">false</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">size"</span><span class="pi">:</span> <span class="nv">null</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">use_regex"</span><span class="pi">:</span> <span class="nv">false</span>
<span class="pi">}</span>
<span class="pi">},</span>
<span class="s2">"</span><span class="s">item"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Microsoft</span><span class="nv"> </span><span class="s">VS</span><span class="nv"> </span><span class="s">Code"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">matched"</span><span class="pi">:</span> <span class="nv">2</span>
<span class="pi">}</span>
<span class="pi">],</span>
<span class="s2">"</span><span class="s">skipped"</span><span class="pi">:</span> <span class="nv">false</span>
<span class="pi">}</span>
<span class="err">}</span>
</code></pre></div></div>
<p>The output displays an object with a “results” value, which is an array containing multiple objects. In this example, there are two result objects as the task was executed using two loop paths. Each result object has a property called “files”, which is also an array containing strings of the resulting files. In this example, these are the executables that were found at the designated locations. The goal is to obtain a list of the files from both results.</p>
<h2 id="the-solution">The solution</h2>
<p>To achieve the desired outcome, the json_query function can be utilized to filter the results. The function allows for the application of <a href="https://jmespath.org/" target="_blank">JMESPath</a> filters. To gain a better understanding of how to apply these filters, it is recommended to read the documentation, which includes multiple examples that can be experimented with on the website.</p>
<p>In this specific case, the following filter will yield the desired result: <code class="language-plaintext highlighter-rouge">results[*].files[*].path</code>.
However, when using this filter with the json_query function, the following result will be obtained:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{</span>
<span class="pi">[</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">7za.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">cblauncher.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">rpctool.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">vmtoolsd.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">VMToolsHookProc.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">VMwareNamespaceCmd.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">VMwareResolutionSet.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">VMwareToolboxCmd.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">VMwareXferlogs.exe"</span>
<span class="pi">],</span>
<span class="pi">[</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">setup_wm.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmlaunch.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmpconfig.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmplayer.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmpnetwk.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmpnscfg.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmprph.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmpshare.exe"</span>
<span class="pi">]</span>
<span class="pi">}</span>
</code></pre></div></div>
<p>As there are two result objects, the paths are split into two separate arrays, which is not the desired outcome. To combine these two arrays into a single one, the “pipe flatten” can be used. The following command will give the desired end result:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">find</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">json_query('results[*].files[*].path')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">flatten</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div></div>
<p>Result:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{</span>
<span class="pi">[</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">7za.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">cblauncher.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">rpctool.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">vmtoolsd.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">VMToolsHookProc.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">VMwareNamespaceCmd.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">VMwareResolutionSet.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">VMwareToolboxCmd.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">VMware</span><span class="se">\\</span><span class="s">VMware</span><span class="nv"> </span><span class="s">Tools</span><span class="se">\\</span><span class="s">VMwareXferlogs.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">setup_wm.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmlaunch.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmpconfig.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmplayer.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmpnetwk.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmpnscfg.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmprph.exe"</span><span class="pi">,</span>
<span class="s2">"</span><span class="s">C:</span><span class="se">\\</span><span class="s">Program</span><span class="nv"> </span><span class="s">Files</span><span class="se">\\</span><span class="s">Windows</span><span class="nv"> </span><span class="s">Media</span><span class="nv"> </span><span class="s">Player</span><span class="se">\\</span><span class="s">wmpshare.exe"</span>
<span class="pi">]</span>
<span class="pi">}</span>
</code></pre></div></div>
<p>Full playbook:</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">Test on the management server</span>
<span class="na">hosts</span><span class="pi">:</span> <span class="s">mngt</span>
<span class="na">vars_files</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">ansible.yml</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">Collect dirs from multiple locations</span>
<span class="na">win_find</span><span class="pi">:</span>
<span class="na">paths</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">item</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">patterns</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">*.exe'</span><span class="pi">]</span>
<span class="na">register</span><span class="pi">:</span> <span class="s">find</span>
<span class="na">loop</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">C:\Program Files\VMware\VMware Tools</span>
<span class="pi">-</span> <span class="s">C:\Program Files\Windows Media Player</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Get list with output</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">{{</span><span class="nv"> </span><span class="s">find</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">json_query('results[*].files[*].path')</span><span class="nv"> </span><span class="s">|</span><span class="nv"> </span><span class="s">flatten</span><span class="nv"> </span><span class="s">}}"</span>
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>When one is not entirely familiar with a solution, as is the case with Ansible for me, it can take some time to understand how to achieve the desired result. This example, covered in this blog post, was the case for me.</p>
<p>By using the json_query function, it is possible to achieve the desired solution. It is important to have an understanding of the desired outcome structure in order to create the appropriate JMESPath filter. Additionally, it is important to learn the power of <a href="https://jmespath.org/" target="_blank">JMESPath</a> filters, as it also allows for filtering on specific objects within the query.</p>
<p>I hope this blog post was useful and if you have any questions, please feel free to leave a comment below.</p>
<p>Photo by <a href="https://unsplash.com/@glenncarstenspeters?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Glenn Carstens-Peters</a> on <a href="https://unsplash.com/photos/RLw-UC03Gwc?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Unsplash</a></p>salSometimes, you may encounter a scenario that appears to be straightforward, but when it comes to coding, you find yourself quickly becoming frustrated. One such challenge is combining the results of multiple loops into a single list. It can take some time to find the right solution, and I thought it would be useful to document the process. This blog post will explain the challenge in detail and provide a solution for combining the results of multiple loops into a single list.Terraform 101 tip: element function2021-08-27T00:00:00+00:002021-08-27T00:00:00+00:00https://www.logitblog.com/terraform-101-tips-element-function<p>In the last couple of months, my day-to-day work involves a lot working with Terraform. Now sometimes there are features or functions which are not described clearly. Therefore introducing 101 tips, a series sharing some tips that might help you. The first blog post of the series will be covering the function element.</p>
<h2 id="what-is-terraform">What is Terraform?</h2>
<p>For those who are not familiar with Terraform, Terraform is an infrastructure as code (IaC) tool that allows you to build, change, and version infrastructure safely and efficiently. This includes low-level components such as compute instances, storage, and networking, as well as high-level components such as DNS entries, SaaS features, etc. Terraform can manage both existing service providers and custom in-house solutions.</p>
<iframe width="800" height="450" src="https://www.youtube.com/embed/h970ZBgKINg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
<h3 id="key-features">Key Features</h3>
<ul>
<li>Infrastructure as Code</li>
<li>Execution Plans</li>
<li>Resource Graph</li>
<li>Change Automation</li>
</ul>
<p>More information can be found here: <a href="https://www.terraform.io/intro/index.html" target="_blank">Introduction - Terraform by HashiCorp</a>.</p>
<h2 id="function-element">Function: element</h2>
<p>Let’s start with covering the function documentation. The description of the function is as followed.</p>
<p>Element retrieves a single element from a list.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>element(list, index)
</code></pre></div></div>
<p>The index is zero-based. This function produces an error if used with an empty list. The index must be a non-negative integer.
Use the built-in index syntax list[index] in most cases. Use this function only for the special additional “wrap-around” behavior described below.
Source: <a href="https://www.terraform.io/docs/language/functions/element.html" target="_blank">element - Functions - Configuration Language - Terraform by HashiCorp</a>.</p>
<p>Now, this is one of the examples that is shown in the function documentation:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>> element(["a", "b", "c"], 1)
b
</code></pre></div></div>
<p>The function will select the property of an array based on the index number. In most programming languages when reaching a number that is out of the range of the array, you will receive an exception. But the element function will continue from the first item in the array. This will result in the following behavior.</p>
<table>
<thead>
<tr>
<th style="text-align: left">Index</th>
<th style="text-align: left">Array</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">0</td>
<td style="text-align: left">a</td>
</tr>
<tr>
<td style="text-align: left">1</td>
<td style="text-align: left">b</td>
</tr>
<tr>
<td style="text-align: left">2</td>
<td style="text-align: left">c</td>
</tr>
<tr>
<td style="text-align: left">3</td>
<td style="text-align: left">a</td>
</tr>
<tr>
<td style="text-align: left">4</td>
<td style="text-align: left">b</td>
</tr>
<tr>
<td style="text-align: left">5</td>
<td style="text-align: left">c</td>
</tr>
<tr>
<td style="text-align: left">6</td>
<td style="text-align: left">a</td>
</tr>
</tbody>
</table>
<h2 id="use-case-example">Use case example</h2>
<p>To fully understand the function and how to use it let’s cover this by using a use case.</p>
<p>Let’s say it is required to have 6 databases distributed over 3 different locations. This can easily be done using the element function.
The example is based on the <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/mariadb_server" target="_blank">azurerm_mariadb_server</a> resource.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>provider "azurerm" {
features {}
}
locals {
locations =["West Europe", "East US", "West US"]
}
resource "azurerm_resource_group" "example" {
count = 3
name = "example-resources"
location = element(local.locations, count.index)
}
resource "azurerm_mariadb_server" "example" {
count = 6
name = "example-mariadb-server-${count.index + 1}"
location = element(azurerm_resource_group.example, count.index).location
resource_group_name = element(azurerm_resource_group.example, count.index).name
administrator_login = "mariadbadmin"
administrator_login_password = "H@Sh1CoR3!"
sku_name = "B_Gen5_2"
storage_mb = 5120
version = "10.2"
auto_grow_enabled = true
backup_retention_days = 7
geo_redundant_backup_enabled = false
public_network_access_enabled = false
ssl_enforcement_enabled = true
}
</code></pre></div></div>
<p>The locals are used to create an array with the different locations. When creating the required resource group, the location is collected from the location array by using <code class="language-plaintext highlighter-rouge">element(local.locations, count.index)</code>.</p>
<p>To distribute the databases over these locations the element function is again used to select the correct resource group.</p>
<p><code class="language-plaintext highlighter-rouge">element(azurerm_resource_group.example, count.index).location</code></p>
<p>This example shows you can both select a single item from an array but also other properties, in this case from the created resource groups.</p>
<p>When planning the example it will result in creating 9 resources:</p>
<p>3 resource groups:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # azurerm_resource_group.example[0] will be created
+ resource "azurerm_resource_group" "example" {
+ id = (known after apply)
+ location = "westeurope"
+ name = "example-resources-1"
}
# azurerm_resource_group.example[1] will be created
+ resource "azurerm_resource_group" "example" {
+ id = (known after apply)
+ location = "eastus"
+ name = "example-resources-2"
}
# azurerm_resource_group.example[2] will be created
+ resource "azurerm_resource_group" "example" {
+ id = (known after apply)
+ location = "westus"
+ name = "example-resources-3"
}
</code></pre></div></div>
<p>6 databases</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # azurerm_mariadb_server.example[0] will be created
+ resource "azurerm_mariadb_server" "example" {
+ location = "westeurope"
+ name = "example-mariadb-server-1"
}
# azurerm_mariadb_server.example[1] will be created
+ resource "azurerm_mariadb_server" "example" {
+ location = "eastus"
+ name = "example-mariadb-server-2"
}
# azurerm_mariadb_server.example[2] will be created
+ resource "azurerm_mariadb_server" "example" {
+ location = "westus"
+ name = "example-mariadb-server-3"
}
# azurerm_mariadb_server.example[3] will be created
+ resource "azurerm_mariadb_server" "example" {
+ location = "westeurope"
+ name = "example-mariadb-server-4"
}
# azurerm_mariadb_server.example[4] will be created
+ resource "azurerm_mariadb_server" "example" {
+ location = "eastus"
+ name = "example-mariadb-server-5"
}
# azurerm_mariadb_server.example[5] will be created
+ resource "azurerm_mariadb_server" "example" {
+ location = "westus"
+ name = "example-mariadb-server-6"
}
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>Sometimes you end up in a situation where you will think, how are we going to do this? From my experience, I often encounter this, especially when you are not 100% familiar with the language and all the functions.</p>
<p>The element function is very powerful which can help you with selecting a single element or iterating through multiple options. As shown above, is a nice example to distribute those databases over the various locations and still using 1 definition. This shows the true power of the element function.</p>
<p>I hope this Terraform 101 tip was helpful and if you have any suggestions please let me know in the comments below.</p>
<p>Photo by <a href="https://unsplash.com/@fakurian?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Fakurian Design</a> on <a href="https://unsplash.com/s/photos/modern?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Unsplash</a></p>salIn the last couple of months, my day-to-day work involves a lot working with Terraform. Now sometimes there are features or functions which are not described clearly. Therefore introducing 101 tips, a series sharing some tips that might help you. The first blog post of the series will be covering the function element.LoadGen automation using PowerShell2021-06-30T00:00:00+00:002021-06-30T00:00:00+00:00https://www.logitblog.com/loadgen-automation-using-powershell<p>Automation is key when performing a lot of LoadGen tests. Using the automation ensure consistency and will reduce time. At GO-EUC automation is heavily used to continuously execute LoadGen performance tests. This blog post will share how to start and monitor a LoadGen test using PowerShell.</p>
<h2 id="what-is-loadgen">What is LoadGen?</h2>
<p>For those who are not familiar with LoadGen, let’s first start introducing it.</p>
<p>LoadGen offers customers and service providers a complete software solution to test, and maintain the optimal performance, user experience, scalability, and availability of (virtual) desktop environments including all of your business applications.
Whatever you wish to test, LoadGen makes it easier with our user-friendly software. Load tests, stress tests, performance tests, End-to-End Monitoring. The wide range of Load and Performance Testing options allows you to create customized simulations in just a few simple steps. The outcome means that if necessary, you can take appropriate action well in time so that the user experience of your IT environment remains optimal. LoadGen simulates end-user behavior for systems such as Citrix Virtual Apps and Desktops, Microsoft Remote Desktop Services, VMware Horizon, and Fat Clients.</p>
<p>Product and Application reliability and objective simulations that identify the possible effects of planned changes to your IT environment. It allows you to test what will happen before the actual change is applied, allowing you to gain full control over the performance of your IT environment and your end-users to continue using your systems unhindered.</p>
<p>More information about LoadGen can be found <a href="https://www.loadgen.com/" target="_blank">here</a>.</p>
<h2 id="the-steps-that-are-required">The steps that are required</h2>
<p>Before going into the PowerShell code details, let’s start by explaining the steps and requirements. In terms of requirements, this example requires a full setup and configured LoadGen environment which is ready to start a test. Also please be aware LoadGen requires to run in an interactive session, meaning it cannot be executed using a background process or services.</p>
<p>The example will do the following steps:</p>
<ul>
<li>Validate any running LoadGen Director process</li>
<li>Start LoadGen test based on the canvas</li>
<li>Monitor for new the database table in LoadGen database</li>
<li>Collect scenario and user configuration</li>
<li>Monitor launched users in the database</li>
</ul>
<p>These steps are all done using PowerShell and require the SQLServer module in Powershell, which can be installed by the following command:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">SqlServer</span><span class="w">
</span></code></pre></div></div>
<p>Source: <a href="https://www.powershellgallery.com/packages/Sqlserver" target="_blank">PowerShell Gallery | SqlServer</a>.</p>
<h2 id="powershell-code">PowerShell code</h2>
<p>The code has been split into two functions, the first for starting the LoadGen test and the second for monitoring the launched sessions.</p>
<p>Starting the test requires the parameter LoadGen canvas name, SQL server, and LoadGen database name.</p>
<pre><code class="language-PoweShell">Function Start-Test {
Param(
[string]$Canvas,
[string]$SqlSever,
[string]$Database )
$startDate = Get-Date
Write-Host (Get-Date) ": Validating on running instance of LoadGen..."
$loadGenProcess = Get-Process "LoadGen Director" -ErrorAction SilentlyContinue
if ($loadGenProcess) {
Write-Host (Get-Date) ": LoadGen still running, closing it..."
$loadGenProcess | Stop-Process -Force
}
Write-Host (Get-Date) ": Starting LoadGen Test..."
Start-Process -FilePath "$env:ProgramFiles\LoadGen\LoadGen Director\LoadGen Director.exe" -ArgumentList "/nvl:true /nvc /start:now /canvas:$Canvas" -WindowStyle Minimized
$queryDatabase = "SELECT name, create_date FROM $($DataBase).sys.tables WHERE name LIKE 'SMD%' ORDER BY create_date DESC"
Write-Host (Get-Date) ": Waiting for database table to be created..."
$i = 0;
while (!$tableName) {
$tables = Invoke-Sqlcmd -Query $queryDatabase -ServerInstance "flow-sql-1"
$tableName = ($tables | Where-Object {$_.create_date -ge $startDate}).name
Start-Sleep 5
if ($i -gt 30){
Write-Error (Get-Date) ": Could not find the table..."
}
$i++
}
Write-Host (Get-Date) ": Table found with name $tableName"
return $tableName
}
</code></pre>
<p>The following function is the monitoring function, which requires the same parameters plus the table name that has been returned from the previous function.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">Function</span><span class="w"> </span><span class="nf">Monitor-Test</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">Param</span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$TableName</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Canvas</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$SqlSever</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Database</span><span class="w"> </span><span class="p">)</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="s2">": Monitor LoadGen test."</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="s2">": Using table </span><span class="nv">$TableName</span><span class="s2">"</span><span class="w">
</span><span class="nv">$loadGenProf</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"C:\ProgramData\LoadGen\Common\LoadGenProfiles.xml"</span><span class="w">
</span><span class="nv">$loadGenDir</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"C:\ProgramData\LoadGen\Common\LoadGenDirector.xml"</span><span class="w">
</span><span class="p">[</span><span class="n">xml</span><span class="p">]</span><span class="nv">$loadGenProfConfig</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Content</span><span class="w"> </span><span class="nv">$loadGenProf</span><span class="w">
</span><span class="p">[</span><span class="n">xml</span><span class="p">]</span><span class="nv">$loadGenConfig</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Content</span><span class="w"> </span><span class="nv">$loadGenDir</span><span class="w">
</span><span class="nv">$scenario</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$loadGenProfConfig</span><span class="o">.</span><span class="nf">LoadProfilesDataset</span><span class="o">.</span><span class="nf">LoadProfiles</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">profileName</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$Canvas</span><span class="w"> </span><span class="p">})</span><span class="o">.</span><span class="nf">ActiveScenario</span><span class="w">
</span><span class="nv">$sessions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$loadGenConfig</span><span class="o">.</span><span class="nf">MainConfiguration</span><span class="o">.</span><span class="nf">Scenarios</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$scenario</span><span class="p">})</span><span class="o">.</span><span class="nf">Sessions</span><span class="w">
</span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="nv">$duration</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$loadGenConfig</span><span class="o">.</span><span class="nf">MainConfiguration</span><span class="o">.</span><span class="nf">Scenarios</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$scenario</span><span class="p">})</span><span class="o">.</span><span class="nf">TotalDuration</span><span class="w">
</span><span class="nv">$queryLaunchedSessions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"SELECT ISNULL(COUNT(Username),'') [Initializing] FROM [</span><span class="nv">$Database</span><span class="s2">].[dbo].[</span><span class="nv">$TableName</span><span class="s2">] WHERE Type = 'DenamikInitializing'"</span><span class="w">
</span><span class="nv">$timeoutTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">([</span><span class="n">int</span><span class="p">]</span><span class="nv">$duration</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">15</span><span class="p">)</span><span class="w">
</span><span class="nv">$startTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Date</span><span class="w">
</span><span class="nv">$timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="nv">$timeout</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$launchedSessions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="w">
</span><span class="kr">while</span><span class="p">(</span><span class="nv">$launchedSessions</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="nv">$sessions</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$launchedSessions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Invoke-Sqlcmd</span><span class="w"> </span><span class="nt">-Query</span><span class="w"> </span><span class="nv">$queryLaunchedSessions</span><span class="w"> </span><span class="nt">-ServerInstance</span><span class="w"> </span><span class="s2">"flow-sql-1"</span><span class="p">)</span><span class="o">.</span><span class="nf">Initializing</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="s2">": </span><span class="nv">$launchedSessions</span><span class="s2"> of </span><span class="nv">$sessions</span><span class="s2"> sessions launched."</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">30</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(((</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">$startTime</span><span class="p">)</span><span class="o">.</span><span class="nf">TotalMinutes</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="nv">$timeoutTime</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="kr">break</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$timeout</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="s2">": Timeout reached!"</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="s2">": All sessions launched."</span><span class="w">
</span><span class="nv">$durMin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">math</span><span class="p">]::</span><span class="n">Round</span><span class="p">((</span><span class="n">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">Subtract</span><span class="p">(</span><span class="nv">$startTime</span><span class="p">)</span><span class="o">.</span><span class="nf">TotalMinutes</span><span class="p">)</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$duration</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="nv">$durMin</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$idleTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">math</span><span class="p">]::</span><span class="n">Round</span><span class="p">(</span><span class="nv">$duration</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">$durMin</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="s2">": Waiting </span><span class="nv">$idleTime</span><span class="s2"> min. for the loop to finish..."</span><span class="w">
</span><span class="nv">$idleTimeSec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$idleTime</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nv">$idleTimeSec</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="s2">": Waiting 5 min. for the loop to finish..."</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">300</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Get-Process</span><span class="w"> </span><span class="s2">"LoadGen Director"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Stop-Process</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Write-Error</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="w"> </span><span class="s2">": Something went wrong while closing LoadGen Director..."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>The following example shows how both functions can be used:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$databaseTable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Start-Test</span><span class="w"> </span><span class="nt">-SqlServer</span><span class="w"> </span><span class="s2">"sql-1"</span><span class="w"> </span><span class="nt">-Database</span><span class="w"> </span><span class="s2">"LoadGen"</span><span class="w"> </span><span class="nt">-Canvas</span><span class="w"> </span><span class="s2">"Default_Citrix"</span><span class="w">
</span><span class="n">Monitor-Test</span><span class="w"> </span><span class="nt">-SqlServer</span><span class="w"> </span><span class="s2">"sql-1"</span><span class="w"> </span><span class="nt">-Database</span><span class="w"> </span><span class="s2">"LoadGen"</span><span class="w"> </span><span class="nt">-Canvas</span><span class="w"> </span><span class="s2">"Default_Citrix"</span><span class="w"> </span><span class="nt">-TableName</span><span class="w"> </span><span class="nv">$databaseTable</span><span class="w">
</span></code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>Repeatability is an important factor when executing a lot of performance tests. By using PowerShell it is possible to automated starting and monitoring a LoadGen test. When including this in total automation it is possible to continuously execute and monitor tests, just like at GO-EUC.</p>
<p>When you have questions or comments, please leave them below.</p>
<p>Photo by <a href="https://unsplash.com/@clayton_cardinalli?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Clayton Cardinalli</a> on <a href="https://unsplash.com/s/photos/automation?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Unsplash</a></p>salAutomation is key when performing a lot of LoadGen tests. Using the automation ensure consistency and will reduce time. At GO-EUC automation is heavily used to continuously execute LoadGen performance tests. This blog post will share how to start and monitor a LoadGen test using PowerShell.Releasing an Azure DevOps artifact on GitHub releases2021-05-29T00:00:00+00:002021-05-29T00:00:00+00:00https://www.logitblog.com/releasing-an-azure-devops-artifact-on-github-releases<p>When distributing software there are multiple options to achieve this. It can be provided via the website, using a cloud storage solution, using GitHub, etc. GitHub is a very common location to share the source code and releases, but it can also be used to just release an executable. This blog post will show how to release an Azure DevOps artifact via a GitHub release.</p>
<h2 id="releases-on-github">Releases on GitHub</h2>
<p>Before going into the details, it is important to have a clear understanding of releases on GitHub.</p>
<p>Releases are deployable software iterations you can package and make available for a wider audience to download and use.</p>
<p>Releases are based on Git tags, which mark a specific point in your repository’s history. A tag date may be different than a release date since they can be created at different times. For more information about viewing your existing tags, see “Viewing your repository’s releases and tags.”</p>
<p>You can receive notifications when new releases are published in a repository without receiving notifications about other updates to the repository. For more information, see “Viewing your subscriptions.”</p>
<p>Anyone with read access to a repository can view and compare releases, but only people with write permissions to a repository can manage releases. For more information, see “Managing releases in a repository.”</p>
<p>People with admin permissions to a repository can choose whether Git Large File Storage (Git LFS) objects are included in the ZIP files and tarballs that GitHub creates for each release. For more information, see “Managing Git LFS objects in archives of your repository.”</p>
<p>If a release fixes a security vulnerability, you should publish a security advisory in your repository. GitHub reviews each published security advisory and may use it to send Dependabot alerts to affected repositories. For more information, see “About GitHub Security Advisories.”</p>
<p>You can view the Dependents tab of the dependency graph to see which repositories and packages depend on code in your repository, and may therefore be affected by a new release. For more information, see “About the dependency graph.”</p>
<p>You can also use the Releases API to gather information, such as the number of times people download a release asset. For more information, see “Releases.”</p>
<p>Source: <a href="https://docs.github.com/en/github/administering-a-repository/about-releases" target="_blank">About releases - GitHub Docs</a></p>
<h2 id="creating-the-artifact">Creating the artifact</h2>
<p>The first step is to create the artifact. This is a bit depending on the type of software you build and create, but the goal is to get the binaries into the artifact staging directory. This can be done using the copy action in the YAML based pipeline.</p>
<blockquote>
<p>Quick side note, it is a best practice to use the YAML based pipelines as these are managed by code in the repository.</p>
</blockquote>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CopyFiles@2</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">SourceFolder</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.DefaultWorkingDirectory)\MySolution\bin\$(BuildConfiguration)\'</span>
<span class="na">Contents</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s">*.exe</span>
<span class="s">*.dll</span>
<span class="na">TargetFolder</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(Build.ArtifactStagingDirectory)'</span>
</code></pre></div></div>
<p>This snipped will copy all executable and DLL files into the ArtifactStagingDirectory.</p>
<p>The next step is to publish the artifact which can be done with the following snippet:</p>
<pre><code class="language-YAML"> - task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: release'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'release'
publishLocation: 'Container'
</code></pre>
<p>At this stage, the artifact is available and ready to be released.</p>
<h2 id="releasing-to-github">Releasing to GitHub</h2>
<p>Now by default Azure DevOps has the GitHubRelease task available, so this means no marketplace plugin needs to be installed to achieve our goal.
But before this can be leveraged, a connection to the GitHub account needs to be made. This can be done in the project settings and the service connections.</p>
<p><a href="/assets/images/posts2021-05-29-releasing-an-azure-devops-artifact-on-github-releases/azure-devops-service-connections.png" data-lightbox="azure-devops-service-connections">
<img src="/assets/images/posts/2021-05-29-releasing-an-azure-devops-artifact-on-github-releases/azure-devops-service-connections.png" alt="azure-devops-service-connections" />
</a></p>
<p>Create a new service connection and select GitHub:</p>
<p><a href="/assets/images/posts2021-05-29-releasing-an-azure-devops-artifact-on-github-releases/azure-devops-service-connections-github.png" data-lightbox="azure-devops-service-connections-github">
<img src="/assets/images/posts/2021-05-29-releasing-an-azure-devops-artifact-on-github-releases/azure-devops-service-connections-github.png" alt="azure-devops-service-connections-github" />
</a></p>
<p>By default the Authentication method is on Grant authorization, this will guide you through the authentication flow. Depending on your preference you can choose one of the two methods.
Hit the authorize and follow the authentication flow.</p>
<p><a href="/assets/images/posts2021-05-29-releasing-an-azure-devops-artifact-on-github-releases/azure-devops-service-connections-github-done.png" data-lightbox="azure-devops-service-connections-github-done">
<img src="/assets/images/posts/2021-05-29-releasing-an-azure-devops-artifact-on-github-releases/azure-devops-service-connections-github-done.png" alt="azure-devops-service-connections-github-done" />
</a></p>
<p>In the end, a service connection is available.</p>
<p><a href="/assets/images/posts2021-05-29-releasing-an-azure-devops-artifact-on-github-releases/azure-devops-service-connection-listed.png" data-lightbox="azure-devops-service-connection-listed">
<img src="/assets/images/posts/2021-05-29-releasing-an-azure-devops-artifact-on-github-releases/azure-devops-service-connection-listed.png" alt="azure-devops-service-connection-listed" />
</a></p>
<p>The GitHubRelease task does require a couple of parameters. At first the service connection and the repository name on GitHub. Secondly, it also required the SHA of the latest commit. As this is a separate repository, this information is not directly available. Luckily GitHub does provide an API so this can be collected using that. Now in this example, PowerShell is used to collect the SHA of the latest commit.</p>
<pre><code class="language-YAML"> - task: PowerShell@2
displayName: 'Get latest SHA commmit from repo'
inputs:
targetType: 'inline'
script: |
$commits = Invoke-RestMethod -Method GET -Uri "https://api.github.com/repos/Account/repo/commits"
$sha = $commits[0].sha
Write-Host "##vso[task.setvariable variable=sha;]$sha"
</code></pre>
<p>Now collected this information the release can be created. In this example, the tag is a variable that has been set in a separate task.</p>
<pre><code class="language-YAML"> - task: GitHubRelease@1
inputs:
gitHubConnection: 'example_github'
repositoryName: 'Account/repo'
action: 'create'
target: '$(sha)'
tagSource: 'userSpecifiedTag'
tag: '$(tag)'
title: 'v$(tag)'
assets: '$(Build.ArtifactStagingDirectory)\release\*.exe'
addChangeLog: false
</code></pre>
<p>In the end, you will end up with a release on GitHub like this:</p>
<p><a href="/assets/images/posts2021-05-29-releasing-an-azure-devops-artifact-on-github-releases/gitub-release.png" data-lightbox="gitub-release">
<img src="/assets/images/posts/2021-05-29-releasing-an-azure-devops-artifact-on-github-releases/gitub-release.png" alt="gitub-release" />
</a></p>
<h2 id="staging-best-practices">Staging best practices</h2>
<p>A release of the software is in most cases a controlled moment, so you want to avoid on each build a new release is created. In Azure DevOps pipelines you can create multiple stages including conditions. Using the following condition will only run the stage when it is run from the main branch.</p>
<pre><code class="language-YAML"> condition: contains(variables['build.sourceBranch'], 'refs/heads/main')
</code></pre>
<p>Please take note, as this stage might be run an a separate build agent, the artifact needs to be download. Here is a full example:</p>
<pre><code class="language-YAML">- stage: Release
condition: contains(variables['build.sourceBranch'], 'refs/heads/main')
displayName: 'Release Software'
jobs:
- job: Release
steps:
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'release'
downloadPath: '$(System.ArtifactsDirectory)'
</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>In some cases, you might want to release your software on GitHub even though the source code is hosted in Azure DevOps. Leveraging the GitHub integration and the GitHub API it is possible to create a new release via the pipeline. This way you can easily distribute your software.</p>
<p>If you have any questions or feedback on how to improve this, please let me know in the comments.</p>
<p>Photo by <a href="https://unsplash.com/@kellysikkema?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Kelly Sikkema</a> on <a href="https://unsplash.com/s/photos/delivery?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></p>salWhen distributing software there are multiple options to achieve this. It can be provided via the website, using a cloud storage solution, using GitHub, etc. GitHub is a very common location to share the source code and releases, but it can also be used to just release an executable. This blog post will show how to release an Azure DevOps artifact via a GitHub release.Terraform & Ansible: Better Together2021-04-30T00:00:00+00:002021-04-30T00:00:00+00:00https://www.logitblog.com/terraform-&-ansible-better-together<p>Over the last couple of months, a lot of my time has been defining technology stacks for multiple projects. Many organizations are in the middle of the DevOps transition and looking for guidance in selecting the right technology stack. This blog post will share some of my thoughts and dig a little bit deeper into Terraform & Ansible.</p>
<h2 id="what-is-terraform">What is Terraform?</h2>
<p>Terraform is a tool for building, changing, and versioning infrastructure safely and efficiently. Terraform can manage existing and popular service providers as well as custom in-house solutions.</p>
<p>Configuration files describe to Terraform the components needed to run a single application or your entire datacenter. Terraform generates an execution plan describing what it will do to reach the desired state, and then executes it to build the described infrastructure. As the configuration changes, Terraform can determine what changed and create incremental execution plans which can be applied.</p>
<p>The infrastructure Terraform can manage includes low-level components such as compute instances, storage, and networking, as well as high-level components such as DNS entries, SaaS features, etc.</p>
<p>Source: <a href="https://www.terraform.io/intro/index.html" target="_blank">Introduction - Terraform by HashiCorp</a></p>
<h2 id="what-is-ansible">What is Ansible?</h2>
<p>Ansible is a radically simple IT automation engine that automates cloud provisioning, configuration management, application deployment, intra-service orchestration, and many other IT needs.</p>
<p>Designed for multi-tier deployments since day one, Ansible models your IT infrastructure by describing how all of your systems inter-relate, rather than just managing one system at a time.</p>
<p>It uses no agents and no additional custom security infrastructure, so it’s easy to deploy - and most importantly, it uses a very simple language (YAML, in the form of Ansible Playbooks) that allow you to describe your automation jobs in a way that approaches plain English.</p>
<p>Source: <a href="https://www.ansible.com/overview/how-ansible-works" target="_blank">How Ansible Works - Ansible</a></p>
<h2 id="everything-as-code-strategy">Everything as code strategy</h2>
<p>The term Infrastructure as Code (IaC) is becoming a standard, but from my perspective, this is just a small part of the bigger picture. From the DevOps thoughts, it is all about combining development and operation to minimize the gap between both worlds. By using a term as Infrastructure as Code the same gab is created. Infrastructure is the foundation of the services you want to offer, which is in most cases applications or access to data. In the end-user computing context, this can be a virtual desktop to deliver these applications.</p>
<p>“Everything as Code” is the correct term and from my perspective should be the standard. Everything as Code implies all components from infrastructure to the application delivery should be defined in code.</p>
<h2 id="better-together">Better together</h2>
<p>With “everything as code” in mind, combining a Terraform with Ansible is a perfect combination to achieve this goal. Terraform allows the provision of the infrastructure where Ansible is there to install the applications and ensure the desired state is applied.</p>
<p>When selecting the toolset for a project the flexibility is always a key factor. In some cases, the environment might be still on-premises but in some cases, there is a desire to move the infrastructure to the cloud in the long term. As Terraform has multiple providers it empowers the flexibility to move to the cloud.</p>
<p>Now in most cases and environments consist of multiple operating systems as Windows and Linux based. In a traditional environment, these are managed by dedicated teams. The Linux world is slowly invading the Windows side and more and more services are transitioning toward Linux. As Ansible can both manage Windows and Linux this is the ideal solution to ensure both can be managed by code.</p>
<p>In conclusion, both Terraform and Ansible empowers the flexibility to manage the infrastructure and application from a code perspective no matter the cloud provider and operating system.</p>
<h2 id="executing-ansible-from-terraform">Executing Ansible from Terraform</h2>
<p>Now to apply this in practice, lets cover an example. Terraform is capable to execute directly an Ansible playbook by using the local-exec provisioner. For example:</p>
<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">provisioner</span> <span class="s2">"local-exec"</span> <span class="p">{</span>
<span class="nx">command</span> <span class="p">=</span> <span class="s2">"ansible-playbook playbook-example.yaml -i ${vsphere_virtual_machine.vm_dhcp[count.index].default_ip_address}, -e ansible_user=${var.local_admin} -e ansible_password=${var.local_admin_password} -e ansible_connection=winrm -e ansible_winrm_server_cert_validation=ignore -e ansible_port=5985"</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The -i parameter stands for the inventory, this is the address of the machine which is in this case provided by Terraform. The -e parameter is the extra variables, this way you can provide the required parameters from the playbook. In this example the -e parameters are used to define the ansible_user and ansible_password for authentication and the ansible_connection for defining the WINRM protocol as the communication protocol, as this is a Windows-based machine.</p>
<p>Now as the Ansible playbook will be executed locally on the machine or DevOps agent, it must be a Linux-based operating system. This is because Ansible cannot be initiated from a Windows operating system.
See the Ansible documentation for more information: <a href="https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html" target="_blank">Installing Ansible - Ansible Documentation</a>.</p>
<h2 id="the-drawback-of-this-strategy-and-toolset">The drawback of this strategy and toolset</h2>
<p>Now it is important to also understand the drawback of this strategy. At first, knowledge and mindset are big drawbacks of this strategy. This is also the biggest feedback received which was covered in a previous post: <a href="https://www.logitblog.com/top-5-challenges-of-devops-in-end-user-computing-(euc)/" target="_blank">Logit Blog - Top 5 challenges of DevOps in End User Computing (EUC)</a>.</p>
<p>Even though the toolset is available for years, in general to a lot of organizations it is new. This means teams need to get familiar with the tools but also the way of working in code. Luckily, there is a lot of content out there to get you started.</p>
<p>The choice for Ansible will force you to use a Linux-based operating system as the DevOps agent, as Ansible can only be executed from a Linux operating system. Nowadays PowerShell is supported on Linux, which should make this adoption a bit easier, but it is still a learning curve for those who never worked with Linux.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Infrastructure as Code is just a small scope, so when starting a new project everything as code should be the defacto standard. By setting this as one of the principles is ensuring the set the correct expectation.</p>
<p>When selecting the toolset it is important to ensure to take the longer goals into account like the transition to a cloud provider. A solution like terraform also provides the possibility to adopt a multi-cloud strategy.</p>
<p>By combing a terraform and ansible together will empower everything in code as it can provision both infrastructure and the application.
Ansible playbook can directly be executed using the local-exec provisioner in Terraform ensuring the desired state is applied to the deployed infrastructure.</p>
<p>Hopefully, this post is helpful in your code journey and if you have any questions or remarks, please leave them in the comments below.</p>
<p>Photo by <a href="https://unsplash.com/@adigold1?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Adi Goldstein</a> on <a href="https://unsplash.com/s/photos/together?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" target="_blank">Unsplash</a></p>salOver the last couple of months, a lot of my time has been defining technology stacks for multiple projects. Many organizations are in the middle of the DevOps transition and looking for guidance in selecting the right technology stack. This blog post will share some of my thoughts and dig a little bit deeper into Terraform & Ansible.Top 5 challenges of DevOps in End User Computing (EUC)2021-03-29T00:00:00+00:002021-03-29T00:00:00+00:00https://www.logitblog.com/top-5-challenges-of-devops-in-end-user-computing-(euc)<p>An ever increasing among of organizations are moving to a DevOps way of working. More often than not, this comes with challenges for both organizations and employees alike. This blog post will cover the top 5 challenges of the DevOps transition in End User Computing (EUC).</p>
<h2 id="scope-and-challenges">Scope and challenges</h2>
<p>Now there are multiple disciplines in the world of IT. When working with multiple disciplines, it is clear there is a huge difference in approach and mindset. For example, for developers adopting a DevOps mindset is less of a learning curve compared to a Windows sysadmin. Therefore the scope for this blog post is in the context of End User Computing (EUC). Based on experience this is the group that struggles a lot with adopting a true DevOps mindset. The following 5 items are collected from Twitter interaction from a couple of weeks ago:</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">I have a question for the <a href="https://twitter.com/hashtag/EUC?src=hash&ref_src=twsrc%5Etfw">#EUC</a> community!<br /><br />What is the biggest challenge when moving to a DevOps strategy in the <a href="https://twitter.com/hashtag/EUC?src=hash&ref_src=twsrc%5Etfw">#EUC</a> space?<br /><br />Please RT and leave your answer below 👇</p>— Ryan Ververs-Bijkerk (@Logitblog) <a href="https://twitter.com/Logitblog/status/1369930350294687744?ref_src=twsrc%5Etfw">March 11, 2021</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>At first, thank you all for participating in the conversation. To summarize, the following challenges are most submitted:</p>
<ul>
<li>People and the mindset</li>
<li>Complexity and skillset</li>
<li>Change in way of working</li>
<li>Technology</li>
<li>Supportability</li>
</ul>
<h2 id="people-and-the-mindset">People and the mindset</h2>
<p>The most submitted by far are the people and the mindset. This is the fact on every level within the organizations, from management to the workforce and is clearly the biggest challenge of the DevOps way of working. Without having the proper mindset, it is almost impossible to adopt DevOps successfully. Now, this is caused by a lack of understanding. The IT world is complex with a lot of dependencies which is always under pressure of time and budget.
Unfortunately, there is no simple patch that can be applied to fix this challenge. But it has to start with education on all levels in the organization. Facility by coaching and create room to fail.</p>
<p>A starting point will create some principles in the way of working and create the end goal. For the best adoption, this should be created by the team itself under the correct guidance. To create a better understanding of the teams but also with management, it is crucial to share your journey. Share what the team is experiencing with and try to facilitate some insight into the complexity of the landscape. When doing so, this often will create understanding and comprehension.</p>
<h2 id="complexity-and-skillset">Complexity and skillset</h2>
<p>It is often in a larger organization some sysadmins are doing the same thing over and over again which results in a low skillset limited to their scope. When introducing DevOps a new world of complexity is introduced resulting in resistance due to the knowledge and experience. From my personal experience, by providing the correct guidance it is possible to educate on these techniques. Again, it is important the team and individuals are open and willing to learn, otherwise it will never work.</p>
<p>When coaching a team, it is important to create an environment where there is room to fail. Failing fast is the best way to learn. Some prefer to follow a course but in the end, you will learn fast by getting your hand dirty. It is also important to create involvement. Ensure decisions are made by the entire team. Now a common pitfall is there will be a lot of discussions without any outcome, so therefore it is important to create a format where decision-making is facilitated. A nice example format is <a href="https://adr.github.io/" target="_blank">ADR’s, architectural design records</a>. This can be prepared by an individual or couples which will be proposed to the team. This way a decision is well prepared and facilitates a mutual team understanding which is documented for later reference.</p>
<h2 id="change-in-way-of-working">Change in way of working</h2>
<p>Change is often forced by various outside factors. This can be done personally due to life choices, forced by an organization due to various reasons but also by vendors. With the introduction of Windows 10, there was a big change in the release cycle, going at a way higher pace than before. This forces a lot of sysadmins to continues work on new releases of Windows. Now, this also backfired a bit as there was a lot of reticence forcing Microsoft in providing a longer support cycle for specific releases.</p>
<p>Change can be hard for people resulting in reticence. There are many types of research out there about changing behavior but this is often caused by motivation or techniques.
Motivation is very personal but can be facilitated by providing coaching. Now, this cannot be done within a single day, but the fact is the world is moving.</p>
<p>Techniques or technology is a separate topic, but it is important to have an open mindset for new techniques. By technical guidance, techniques can be taught. But again, it is important the individual is willing to learn.</p>
<p>In the end, it is important to create understand the result of the change. For example, working in a DevOps way will result in traceability and stability. This is, of course, depending on various factors but in providing these goals or principles a mutual understanding is created.</p>
<h2 id="technology">Technology</h2>
<p>Working with new technology might be scary for some due to the lack of understanding. Some people think they are less smart compared to others or are embarrassed they don’t know it. It is so important to avoid this behavior in the organizational culture because this will cause an impediment to learn a new technique.
Like mentioned before, you will learn by failing. It is important to nurture a safe environment where people can fail from both cultural and technical perspectives.</p>
<p>Due to the fast pace of new technology is important to provide the time to explore. This can be done by reading what is happening in the market but also getting the hands dirty by just trying out.</p>
<h2 id="supportability">Supportability</h2>
<p>This was an interesting topic, supportability can be seen from multiple perspectives. This can be the supportability from an organization and technical perspective.
As mentioned in other topics, an organization should support the employees, but the big question are they willing to invest?</p>
<p>From a technical perspective, you probably will encounter technology that will not fit in the DevOps way of working. It is always important to start the conversation and share your vision and the impediments to eventually find the correct way to use the technique. It might happen some vendors cannot adopt this, which might force you to look for alternative solutions.</p>
<p>In the end, it is always important to create the let’s do it vibe exploring the possibilities of making it into.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Getting an organization to a DevOps way of working is complex but the biggest limiting factor is the people. It is important to create a buzz around the change and ensure the entire organization is there to facilitate it. Don’t be afraid to fail, as this is the fasted way of learning.</p>
<p>Ensure to create a can-do mindset and keep repeating the end goal to create a mutual understanding.</p>
<p>do you have some golden tips that might help an organization or individual in the DevOps way of working? Please share them below in the comments.</p>
<p>Photo by <a target="_blank" href="https://unsplash.com/@myleon?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Leon</a> on <a target="_blank" href="https://unsplash.com/s/photos/team?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></p>salAn ever increasing among of organizations are moving to a DevOps way of working. More often than not, this comes with challenges for both organizations and employees alike. This blog post will cover the top 5 challenges of the DevOps transition in End User Computing (EUC).