Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Devops agent: added extension configuration #134

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 109 additions & 7 deletions azure_devops_agent/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
# Azure devops agent

This module allows the creation of an Azure DevOps agent (VM scale set), using either a standard OS image or a custom-built image.
It also gives the possibility to add a custom script extension to the VMs, either one of the provided in this module or a custom extension provided externally

## How to use

By default, this module creates a ScaleSet using Ubuntu22.04, without using os disk encryption, and providing SSH access using a generated ssh key

additional parameters allow you to create VM SS using:

- standard image (provided by az marketplace)
- custom image (previously built and stored on the same resource group of the SS)
- shared image (previously built and stored on a Shared Image Gallery, even in a different subscription, if enabled)

with:

- no extensions (the SS will start with the VM image, plain and simple)
- with provided extension (the VM will run the extension provided at startup)
- with custom extension (the VM will run the extension you wrote at startup)

```hcl
resource "azurerm_resource_group" "azdo_rg" {
count = var.enable_azdoa ? 1 : 0
Expand All @@ -16,7 +29,7 @@ resource "azurerm_resource_group" "azdo_rg" {
}

# with custom image (previously built. check the module `azure_devops_agent_custom_image` for more details)
module "module "azdoa_vmss_li" {" {
module "azdoa_vmss_li" {
source = "git::https://github.com/pagopa/terraform-azurerm-v3.git//azure_devops_agent?ref=<version>"
count = var.enable_azdoa ? 1 : 0
name = "${local.azuredevops_agent_vm_name}"
Expand All @@ -25,14 +38,15 @@ module "module "azdoa_vmss_li" {" {
subscription_name = data.azurerm_subscription.current.display_name
subscription_id = data.azurerm_subscription.current.id
location = var.location
source_image_name = "my-image-name" # the image must be stored in the same subscription/resource group of this resource
image_type = "custom" # enables usage of "source_image_name"
image_name = var.azdoa_image_name
image_version = var.azdoa_image_version # the image must be stored in the same subscription/resource group of this resource
image_type = "managed" # enables usage of "image_name" and "image_version"

tags = var.tags
}

# with default image
module "module "azdoa_vmss_li" {" {
module "azdoa_vmss_li" {
source = "git::https://github.com/pagopa/terraform-azurerm-v3.git//azure_devops_agent?ref=<version>"
count = var.enable_azdoa ? 1 : 0
name = "${local.azuredevops_agent_vm_name}"
Expand All @@ -46,7 +60,7 @@ module "module "azdoa_vmss_li" {" {
}

# with standard image
module "module "azdoa_vmss_li" {" {
module "azdoa_vmss_li" {
source = "git::https://github.com/pagopa/terraform-azurerm-v3.git//azure_devops_agent?ref=<version>"
count = var.enable_azdoa ? 1 : 0
name = "${local.azuredevops_agent_vm_name}"
Expand All @@ -66,9 +80,90 @@ module "module "azdoa_vmss_li" {" {
}


# with provided extension
module "azdoa_vmss_li" {
source = "git::https://github.com/pagopa/terraform-azurerm-v3.git//azure_devops_agent?ref=<version>"
count = var.enable_azdoa ? 1 : 0
name = "${local.azuredevops_agent_vm_name}"
resource_group_name = azurerm_resource_group.azdo_rg[0].name
subnet_id = module.azdoa_snet[0].id
subscription_name = data.azurerm_subscription.current.display_name
subscription_id = data.azurerm_subscription.current.id
location = var.location
image_reference = {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}

extension_name = "install_requirements" # matches the folder name in "extensions" directory

tags = var.tags
}

# with custom extension
module "azdoa_vmss_li" {
source = "git::https://github.com/pagopa/terraform-azurerm-v3.git//azure_devops_agent?ref=39c5e91"
count = var.enable_azdoa ? 1 : 0
name = local.azuredevops_agent_vm_name
resource_group_name = azurerm_resource_group.azdo_rg[0].name
subnet_id = module.azdoa_snet[0].id
subscription_name = data.azurerm_subscription.current.display_name
subscription_id = data.azurerm_subscription.current.subscription_id
location = var.location
image_name = var.azdoa_image_name
image_version = var.azdoa_image_version
image_type = "managed"

extension_name = "tcpflow"
custom_extension_path = "${path.module}/extensions/tcpflow/script-config.json"

tags = var.tags
}


# with shared image
module "azdoa_vmss_li" {
source = "git::https://github.com/pagopa/terraform-azurerm-v3.git//azure_devops_agent?ref=39c5e91"
count = var.enable_azdoa ? 1 : 0
name = local.azuredevops_agent_vm_name
resource_group_name = azurerm_resource_group.azdo_rg[0].name
subnet_id = module.azdoa_snet[0].id
subscription_name = data.azurerm_subscription.current.display_name
subscription_id = data.azurerm_subscription.current.subscription_id
location = var.location
image_name = var.azdoa_image_name
image_version = var.azdoa_image_version
image_type = "shared"

shared_subscription_id = var.shared_subscription_id
shared_resource_group_name = var.shared_rg_name
shared_gallery_name = var.shared_gallery_name

tags = var.tags
}

```

## Extensions

Here is the list of the provided extensions

| Name | Description |
|------|-----------------------------------------------------------------------------------------------|
| install_requirements| Installs all the required packages for an azure devops agent, such as az-cli, docker, helm... |



### How to define a custom extension

If you want to provide a custom extension, you need to define you own extension script and extension config file (like the ones defined here, in the `extensions` folder).
There is no limitation in the file naming, just be sure con configure the correct path in the `json` file.
Then you have to provide it to this module using both the `extension_name` and `custom_extension_path` properties, as shown in the example above.

In this case, the extension name can be arbitrary, since it does not need to match with a folder in this module

<!-- markdownlint-disable -->
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
## Requirements
Expand Down Expand Up @@ -96,6 +191,7 @@ No modules.
|------|------|
| [azurerm_linux_virtual_machine_scale_set.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_virtual_machine_scale_set) | resource |
| [azurerm_ssh_public_key.this_public_key](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/ssh_public_key) | resource |
| [azurerm_virtual_machine_scale_set_extension.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_machine_scale_set_extension) | resource |
| [tls_private_key.this_key](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key) | resource |

## Inputs
Expand All @@ -104,13 +200,19 @@ No modules.
|------|-------------|------|---------|:--------:|
| <a name="input_admin_password"></a> [admin\_password](#input\_admin\_password) | (Optional) The Password which should be used for the local-administrator on this Virtual Machine. Changing this forces a new resource to be created. will be stored in the raw state as plain-text | `string` | `null` | no |
| <a name="input_authentication_type"></a> [authentication\_type](#input\_authentication\_type) | (Required) Type of authentication to use with the VM. Defaults to password for Windows and SSH public key for Linux. all enables both ssh and password authentication. | `string` | `"SSH"` | no |
| <a name="input_custom_extension_path"></a> [custom\_extension\_path](#input\_custom\_extension\_path) | (Optional) if 'extension\_name' is not in the provided extensions, defines the path where to find the extension settings | `string` | `null` | no |
| <a name="input_encryption_set_id"></a> [encryption\_set\_id](#input\_encryption\_set\_id) | (Optional) An existing encryption set | `string` | `null` | no |
| <a name="input_extension_name"></a> [extension\_name](#input\_extension\_name) | (Optional) name of the extension to add to the VM. Either one of the provided (must match the folder name) or a custom extension (arbitrary name) | `string` | `null` | no |
| <a name="input_image_name"></a> [image\_name](#input\_image\_name) | (Optional) The image name to be used, valid for 'shared' or 'managed' image\_type | `string` | `null` | no |
| <a name="input_image_reference"></a> [image\_reference](#input\_image\_reference) | (Optional) A source\_image\_reference block as defined below. | <pre>object({<br> publisher = string<br> offer = string<br> sku = string<br> version = string<br> })</pre> | <pre>{<br> "offer": "0001-com-ubuntu-server-jammy",<br> "publisher": "Canonical",<br> "sku": "22_04-lts-gen2",<br> "version": "latest"<br>}</pre> | no |
| <a name="input_image_type"></a> [image\_type](#input\_image\_type) | (Required) Defines the source image to be used, whether 'custom' or 'standard'. `custom` requires `source_image_name` to be defined, `standard` requires `image_reference` | `string` | `"custom"` | no |
| <a name="input_image_type"></a> [image\_type](#input\_image\_type) | (Required) Defines the source image to be used, whether 'managed' or 'standard'. `managed` and `shared` requires `image_name` and `image_version` to be defined, `standard` requires `image_reference` | `string` | `"managed"` | no |
| <a name="input_image_version"></a> [image\_version](#input\_image\_version) | (Optional) The image version to be used, valid for 'shared' or 'managed' image\_type | `string` | `null` | no |
| <a name="input_location"></a> [location](#input\_location) | (Optional) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created. | `string` | `"westeurope"` | no |
| <a name="input_name"></a> [name](#input\_name) | (Required) The name of the Linux Virtual Machine Scale Set. Changing this forces a new resource to be created. | `string` | n/a | yes |
| <a name="input_resource_group_name"></a> [resource\_group\_name](#input\_resource\_group\_name) | (Required) The name of the Resource Group in which the Linux Virtual Machine Scale Set should be exist. Changing this forces a new resource to be created. | `string` | n/a | yes |
| <a name="input_source_image_name"></a> [source\_image\_name](#input\_source\_image\_name) | (Optional) The name of an Image which each Virtual Machine in this Scale Set should be based on. It must be stored in the same subscription & resource group of this resource | `string` | n/a | yes |
| <a name="input_shared_gallery_name"></a> [shared\_gallery\_name](#input\_shared\_gallery\_name) | (Optional) The shared image gallery (AZ compute gallery) name the shared image is stored | `string` | `null` | no |
| <a name="input_shared_resource_group_name"></a> [shared\_resource\_group\_name](#input\_shared\_resource\_group\_name) | (Optional) The resource group name where the shared image is stored | `string` | `null` | no |
| <a name="input_shared_subscription_id"></a> [shared\_subscription\_id](#input\_shared\_subscription\_id) | (Optional) The subscription id where the shared image is stored | `string` | `null` | no |
| <a name="input_storage_sku"></a> [storage\_sku](#input\_storage\_sku) | (Optional) The SKU of the storage account with which to persist VM. Use a singular sku that would be applied across all disks, or specify individual disks. Usage: [--storage-sku SKU \| --storage-sku ID=SKU ID=SKU ID=SKU...], where each ID is os or a 0-indexed lun. Allowed values: Standard\_LRS, Premium\_LRS, StandardSSD\_LRS, UltraSSD\_LRS, Premium\_ZRS, StandardSSD\_ZRS. | `string` | `"StandardSSD_LRS"` | no |
| <a name="input_subnet_id"></a> [subnet\_id](#input\_subnet\_id) | (Required) An existing subnet ID | `string` | `null` | no |
| <a name="input_subscription_id"></a> [subscription\_id](#input\_subscription\_id) | (Required) Azure subscription id | `string` | n/a | yes |
Expand Down
15 changes: 15 additions & 0 deletions azure_devops_agent/extensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# How to write an extension

If you want to include a new extension in this module, you simply have to create a new folder in the `extensions` directory, containing the script configuration `json` and `sh` file required to define the extension.
The folder must be named after the extension name you want to use, and that you will later use to reference it

The json file is used by the VM to locate the executable file, while the executable contains the actual "source" of the extension.

[Here](https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/custom-script-linux)'s the official documentation on how to write an extension


**N.B.:** json file MUST be named `script-config.json` since by convention that's the name that is looked for by this module

Then you simply need to reference the new extension name when using this module

**N.B.:** be sure to double-check the path defined in `script-config.json`, since you will not get any warning
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"fileUris": [
"https://raw.githubusercontent.com/pagopa/terraform-azurerm-v3/main/azure_devops_agent/extensions/install_requirements/script-config.sh"
],
"commandToExecute": "./script-config.sh"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash


# install zip unzip ca-certificates curl wget apt-transport-https lsb-release gnupg jq
apt-get -y update
apt-get -y --allow-unauthenticated install zip unzip ca-certificates curl wget apt-transport-https lsb-release gnupg jq

zip --version
echo "✅ zip installed"
unzip --version
echo "✅ unzip installed"
jq
echo "✅ jq installed"

# install az cli
curl -sL https://aka.ms/InstallAzureCLIDeb | bash

az acr helm install-cli -y --client-version 3.12.0

az aks install-cli --client-version 1.25.10 --kubelogin-version 0.0.29


# setup Docker installation from https://docs.docker.com/engine/install/ubuntu/
curl -fsSL https://download.docker.com/linux/ubuntu/gpg |
gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null


apt-get -y update
apt-get -y install python3-pip
apt-get -y --allow-unauthenticated install docker-ce docker-ce-cli containerd.io docker-compose-plugin

az --version
echo "✅ az-cli installed"
kubectl version --short
echo "✅ kubectl installed"
docker -v
echo "✅ docker installed"
helm version
echo "✅ helm installed"
python3 --version
echo "✅ python3 installed"

# install yq from https://github.com/mikefarah/yq#install
YQ_VERSION="v4.33.3"
YQ_BINARY="yq_linux_amd64"
wget https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}.tar.gz -O - |\
tar xz && mv ${YQ_BINARY} /usr/bin/yq

yq --version
echo "✅ yq installed"

# install SOPS from https://github.com/mozilla/sops
SOPS_VERSION="v3.7.3"
SOPS_BINARY="3.7.3_amd64.deb"
wget https://github.com/mozilla/sops/releases/download/v3.7.3/sops_3.7.3_amd64.deb
apt install -y $PWD/sops_3.7.3_amd64.deb
sops -v
echo "✅ sops installed"

# prepare machine for k6 large load test

sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_timestamps=1
ulimit -n 250000
24 changes: 22 additions & 2 deletions azure_devops_agent/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ resource "azurerm_ssh_public_key" "this_public_key" {

#build the image id
locals {
source_image_id = "/subscriptions/${var.subscription_id}/resourceGroups/${var.resource_group_name}/providers/Microsoft.Compute/images/${var.source_image_name}"
# managed id
source_image_id = "/subscriptions/${var.subscription_id}/resourceGroups/${var.resource_group_name}/providers/Microsoft.Compute/images/${var.image_name}-${var.image_version}"
# shared id
shared_source_image_id = "/subscriptions/${var.shared_subscription_id}/resourceGroups/${var.shared_resource_group_name}/providers/Microsoft.Compute/galleries/${var.shared_gallery_name}/images/${var.image_name}/versions/${var.image_version}"

# final id
use_image_id = var.image_type == "managed" ? local.source_image_id : local.shared_source_image_id
}


# create scale set
resource "azurerm_linux_virtual_machine_scale_set" "this" {
name = var.name
Expand All @@ -29,7 +36,7 @@ resource "azurerm_linux_virtual_machine_scale_set" "this" {


# only one of source_image_id and source_image_reference is allowed
source_image_id = var.image_type == "custom" ? local.source_image_id : null
source_image_id = var.image_type != "standard" ? local.use_image_id : null

dynamic "source_image_reference" {
# only one of source_image_id and source_image_reference is allowed
Expand Down Expand Up @@ -67,6 +74,7 @@ resource "azurerm_linux_virtual_machine_scale_set" "this" {
subnet_id = var.subnet_id
}
}

platform_fault_domain_count = 1
single_placement_group = false
upgrade_mode = "Manual"
Expand All @@ -76,7 +84,19 @@ resource "azurerm_linux_virtual_machine_scale_set" "this" {
# Ignore changes to these tags because they are generated by az devops.
tags["__AzureDevOpsElasticPool"],
tags["__AzureDevOpsElasticPoolTimeStamp"],
# ignored because the number of instances is managed by az devops
instances
]
}
}

resource "azurerm_virtual_machine_scale_set_extension" "this" {
count = var.extension_name != null ? 1 : 0
name = var.extension_name
virtual_machine_scale_set_id = azurerm_linux_virtual_machine_scale_set.this.id
publisher = "Microsoft.Azure.Extensions"
type = "CustomScript"
type_handler_version = "2.0"
settings = file(var.custom_extension_path != null ? var.custom_extension_path : "${path.module}/extensions/${var.extension_name}/script-config.json")
}

Loading