In der heutigen Diskussion konzentrieren wir uns auf die Implementierung einer Terraform-AKSInfrastruktur in der Azure-Cloud unter Verwendung von GitLab CI/CD-Pipelines. Im anschließenden Diagramm wird der Ablauf, den wir in diesem Artikel erörtern werden, veranschaulicht:
Voraussetzungen:
- Az CLI- Tool installiert
- Git- Tool installiert
- Ein Gitlab-Konto eröffnen, geht auch kostenlos
- Azure Account bzw. ein Azure Tenant
Sobald wir ein Azure aktiven Tenant haben und ein Abonnement, müssen wir die Ressourcenanbieter registrieren. Dafür navigieren wir auf das Abonnement >> im Menü links auf Ressourcenanbieter und dann folgende Ressounrcenanbieter müssen registriert werden ‚Microsoft.OperationalInsights‘, ‚Microsoft.ManagedIdentity‘, ‚Microsoft.Network‘, ‚Microsoft.OperationsManagement‘ und ‚Microsoft.ContainerService‘, ‚Microsoft.KubernetesConfiguration‘
Terraform Module
Wir werden einen einfachen Code verwenden, um eine Azure Kubrenetes Service bereitzustellen.
Hier ist der Code: „Der bereitgestellte Code erfolgt ohne jegliche Gewährleistung.”
Es ist wichtig, zunächst das wir ein Azure-Speicherkonto einrichten, um die Terraform-Zustandsdatei sicher zu speichern. Nachdem wir dies eingerichtet haben, vervollständigen wir den Abschnitt #Terraform State mit den erforderlichen Angaben. Dazu gehören die Angabe der Ressourcengruppe wo der Storage Account liegt, der Name selbst vom Storage Account, der Container-Name sowie der Name der Zustandsdatei, in der der Terraform-Status gesichert wird. Hier bitte achten auf die best practices von Microsoft für den Namenskonvention.
provider.tf
terraform {
required_version = ">=1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
azuread = {
source = "hashicorp/azuread"
version = "~>2.0"
}
azapi = {
source = "Azure/azapi"
version = "1.12.0"
}
random = {
source = "hashicorp/random"
version = "3.6.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = ">= 4.1.0"
}
helm = {
source = "helm"
version = ">= 2.9.0"
}
kubernetes = {
source = "kubernetes"
version = ">= 2.18.1"
}
}
}
provider "azurerm" {
features {}
skip_provider_registration = true
}
#Terraform State
terraform {
backend "azurerm" {
resource_group_name = ""
storage_account_name = ""
container_name = ""
key = ""
}
}
main.tf
########################################################################
# Terraform AKS Module
############# To Do List ###############################################
#
# - Storage Account to store the State
# - Storage Account to configure AKS-Backup
# - Azure AD-Groups (RBAC)
# - Azure AD-Users (RBAC)
# - Connect to GitLab
# - Log Analytics Workspace (Monitoring)
#
########################################################################
data "azurerm_client_config" "current" {}
# create Azure Resource Group
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.resource_group_location
tags = local.tags
}
# Locals block for hardcoded names
locals {
backend_address_pool_name = "${azurerm_virtual_network.vnet.name}-beap"
frontend_port_name = "${azurerm_virtual_network.vnet.name}-feport"
frontend_ip_configuration_name = "${azurerm_virtual_network.vnet.name}-feip"
http_setting_name = "${azurerm_virtual_network.vnet.name}-be-htst"
listener_name = "${azurerm_virtual_network.vnet.name}-httplstn"
request_routing_rule_name = "${azurerm_virtual_network.vnet.name}-rqrt"
tags = {
environment = "prd"
source = "terraform"
"workload name" = "aks"
Org = "Org"
}
}
# Datasource to get Subnets
data "azurerm_subnet" "kubesubnet" {
name = var.aks_subnet_name
virtual_network_name = azurerm_virtual_network.vnet.name
resource_group_name = azurerm_resource_group.rg.name
}
data "azurerm_subnet" "appgwsubnet" {
name = var.appgw_subnet_name
virtual_network_name = azurerm_virtual_network.vnet.name
resource_group_name = azurerm_resource_group.rg.name
}
# Datasource to get Identity
data "azurerm_user_assigned_identity" "ingress" {
name = "ingressapplicationgateway-${azurerm_kubernetes_cluster.aks.name}"
resource_group_name = azurerm_kubernetes_cluster.aks.node_resource_group
}
# Datasource to get Latest Azure AKS latest Version
data "azurerm_kubernetes_service_versions" "current" {
location = azurerm_resource_group.rg.location
include_preview = false
}
# Virtual network (vnet)
resource "azurerm_virtual_network" "vnet" {
name = var.virtual_network_name
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
address_space = [var.virtual_network_address_prefix]
subnet {
name = var.aks_subnet_name
address_prefix = var.aks_subnet_address_prefix
}
subnet {
name = var.appgw_subnet_name
address_prefix = var.app_gateway_subnet_address_prefix
}
tags = local.tags
}
resource "random_id" "my_id" {
byte_length = 4
}
# Storage Account
resource "azurerm_storage_account" "sa" {
name = "${var.sotrage_account_name}${random_id.my_id.hex}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
}
# Create Blob container
resource "azurerm_storage_container" "container" {
name = var.sotrage_container_name
storage_account_name = azurerm_storage_account.sa.name
container_access_type = "blob" # Update access type as needed (private, blob, container, or anonymous)
}
resource "azurerm_user_assigned_identity" "aks" {
name = "aks-ide-${var.aks_cluster_name}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
tags = local.tags
}
resource "azurerm_log_analytics_workspace" "analyticswork" {
name = "k8s-workspace-${random_id.my_id.hex}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
sku = "PerGB2018"
}
resource "azurerm_log_analytics_solution" "analyticssolut" {
solution_name = "ContainerInsights"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
workspace_resource_id = azurerm_log_analytics_workspace.analyticswork.id
workspace_name = azurerm_log_analytics_workspace.analyticswork.name
plan {
publisher = "Microsoft"
product = "OMSGallery/ContainerInsights"
}
}
# AKS cluster
resource "azurerm_kubernetes_cluster" "aks" {
name = var.aks_cluster_name
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
dns_prefix = var.aks_cluster_name
private_cluster_enabled = var.aks_private_cluster
role_based_access_control_enabled = var.aks_enable_rbac
sku_tier = var.aks_sku_tier
default_node_pool {
name = var.default_node_pool_name
node_count = var.aks_node_count
vm_size = var.aks_vm_size
os_disk_size_gb = var.aks_os_disk_size
max_pods = var.max_pods
max_count = var.max_count
min_count = var.min_count
vnet_subnet_id = data.azurerm_subnet.kubesubnet.id
enable_auto_scaling = var.enable_auto_scaling
}
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.aks.id]
}
linux_profile {
admin_username = var.cluster_admin
ssh_key {
key_data = jsondecode(azapi_resource_action.ssh_public_key_gen.output).publicKey
}
}
# azure_active_directory_role_based_access_control {
# managed = true
# admin_group_object_ids = [azuread_group.aks_administrators.id]
# }
network_profile {
network_plugin = "kubenet"
dns_service_ip = var.aks_dns_service_ip
service_cidr = var.aks_service_cidr
load_balancer_sku = "standard"
}
azure_policy_enabled = true
oms_agent {
log_analytics_workspace_id = azurerm_log_analytics_workspace.analyticswork.id
}
ingress_application_gateway {
gateway_id = azurerm_application_gateway.appgw.id
}
depends_on = [
azurerm_application_gateway.appgw
]
tags = local.tags
}
resource "azurerm_kubernetes_cluster_node_pool" "user" {
name = var.node_pool_user
kubernetes_cluster_id = azurerm_kubernetes_cluster.aks.id
vm_size = var.aks_vm_size
node_count = 1
vnet_subnet_id = data.azurerm_subnet.kubesubnet.id
max_count = var.max_count
min_count = var.min_count
enable_auto_scaling = true
mode = "User"
tags = local.tags
}
resource "azurerm_public_ip" "pip" {
name = var.pip_name
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
allocation_method = "Static"
sku = "Standard"
tags = local.tags
}
resource "azurerm_application_gateway" "appgw" {
name = var.app_gateway_name
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
sku {
name = var.app_gateway_tiers["tier2"]
tier = var.app_gateway_tiers["tier2"]
capacity = 1
}
gateway_ip_configuration {
name = "appGatewayIpConfig"
subnet_id = data.azurerm_subnet.appgwsubnet.id
}
frontend_port {
name = local.frontend_port_name
port = 80
}
frontend_ip_configuration {
name = local.frontend_ip_configuration_name
public_ip_address_id = azurerm_public_ip.pip.id
}
backend_address_pool {
name = local.backend_address_pool_name
}
backend_http_settings {
name = local.http_setting_name
cookie_based_affinity = "Disabled"
port = 80
protocol = "Http"
request_timeout = 1
}
http_listener {
name = local.listener_name
frontend_ip_configuration_name = local.frontend_ip_configuration_name
frontend_port_name = local.frontend_port_name
protocol = "Http"
}
request_routing_rule {
name = local.request_routing_rule_name
priority = 1
rule_type = "Basic"
http_listener_name = local.listener_name
backend_address_pool_name = local.backend_address_pool_name
backend_http_settings_name = local.http_setting_name
}
lifecycle {
ignore_changes = [
tags,
backend_address_pool,
backend_http_settings,
http_listener,
probe,
request_routing_rule,
]
}
tags = local.tags
}
# AKS Backup Extension
resource "azurerm_kubernetes_cluster_extension" "aks_backup" {
name = var.kubernetes_cluster_extension_name
cluster_id = azurerm_kubernetes_cluster.aks.id
extension_type = "microsoft.dataprotection.kubernetes" # "Microsoft.Azure.Backup" #
#version = "1.0.20"
#release_train = "stable"
configuration_settings = {
"credentials.tenantId" = data.azurerm_client_config.current.tenant_id
"configuration.backupStorageLocation.config.subscriptionId" = data.azurerm_client_config.current.subscription_id
"configuration.backupStorageLocation.config.resourceGroup" = azurerm_storage_account.sa.resource_group_name
"configuration.backupStorageLocation.config.storageAccount" = azurerm_storage_account.sa.name
"configuration.backupStorageLocation.bucket" = azurerm_storage_container.container.name
"backupLocation" = "azure"
"backupInterval" = "1h"
"retentionPolicy" = "30d"
}
depends_on = [azurerm_kubernetes_cluster.aks]
}
Variable.tf
In Terraform werden Variablendateien verwendet, um Werte für die in der Terraform-Konfiguration definierten Variablen zu setzen. Dies ermöglicht es die Konfigurationen flexibel und wiederverwendbar zu gestalten. Hier sind einige Schlüsselpunkte zur Verwendung von Variablendateien in Terraform:
Variablendeklaration: In einer Datei namens variables.tf
werden Variablen deklariert. Hier geben wir den Namen, den Typ und optional einen Standardwert und eine Beschreibung für jede Variable an.
Wertezuweisung: Die tatsächlichen Werte für diese Variablen werden in einer separaten Datei festgelegt, die oft als terraform.tfvars
oder *.auto.tfvars
bezeichnet wird. Terraform lädt automatisch alle Dateien, die auf .auto.tfvars
oder .tfvars.json
enden.
Überschreiben von Werten: Wir können mehrere .tfvars
-Dateien haben und beim Ausführen von Terraform-Befehlen spezifische Dateien mit dem -var-file
Flag angeben, um bestimmte Werte zu überschreiben z.B.: production.auto.tfvars und dev.auto.tfvars.
- Best Practices: Es gibt keine Einheitslösung für die Verwendung von Variablendateien, da dies von den Anforderungen und Präferenzen Ihres Projekts abhängt. Einige allgemeine Richtlinien sind jedoch:
- Halten Sie Ihre Variablendateien so einfach wie möglich.
- Verwenden Sie
.auto.tfvars
für Umgebungs- oder geheime Werte, die nicht in die Versionskontrolle aufgenommen werden sollten. - Nutzen Sie Kommentare, um die Verwendung von Variablen zu dokumentieren.
Die Verwendung von Variablendateien ist ein mächtiges Feature in Terraform, das die Wartbarkeit und Skalierbarkeit der Infrastruktur als Code verbessert. Es lohnt sich, sich mit den verschiedenen Möglichkeiten vertraut zu machen und die für das Projekt am besten geeignete Methode zu wählen. Viele benutzen nur die variable.tf für alles.
Persönlich tendiere ich dazu, zwei Dateien anzulegen: eine variable.tf
für die Deklaration der Variablen und eine variable.auto.tfvars
für die Zuweisung der Werte zu den Variablen.
Es ist sicherzustellen, dass in diesem Artikel ausschließlich Beispielwerte und Standardkonfigurationen verwendet werden.
variable "resource_group_location" {
type = string
default = "West Europe"
description = "Location for all resources."
}
variable "resource_group_name" {
type = string
description = "Resource group name"
default = "Resourcegroup-Name"
}
variable "virtual_network_name" {
type = string
description = "Virtual network name."
default = "Vnet-Name"
}
variable "virtual_network_address_prefix" {
type = string
description = "VNET address prefix."
default = "10.0.0.0/16"
}
variable "aks_subnet_name" {
type = string
description = "Name of the subset."
default = "Subnet-Name"
}
variable "appgw_subnet_name" {
type = string
description = "Name of the subset."
default = "Gateway-Subnet-Name"
}
variable "aks_cluster_name" {
type = string
description = "The name of the Managed Kubernetes Cluster to create."
default = "aks-cluster-name"
}
variable "aks_os_disk_size" {
type = number
description = "(Optional) The size of the OS Disk which should be used for each agent in the Node Pool."
default = 50
}
variable "aks_node_count" {
type = number
description = "(Optional) The initial number of nodes which should exist in this Node Pool."
default = 2
}
variable "max_pods" {
type = number
description = ""
default = 100
}
variable "max_count" {
type = number
description = "(Required only if enable_auto_scaling is set to True)"
default = 4
}
variable "min_count" {
type = number
description = "(Required only if enable_auto_scaling is set to True)"
default = 2
}
variable "aks_sku_tier" {
type = string
description = "(Optional) The SKU tier that should be used for this Kubernetes Cluster. Possible values are Free and Paid (which includes the Uptime SLA)."
default = "Free"
validation {
condition = contains(["Free", "Paid"], var.aks_sku_tier)
error_message = "Invalid SKU tier. The value should be one of the following: 'Free','Paid'."
}
}
variable "aks_vm_size" {
type = string
description = "The size of the virtual machine."
default = "Standard_D3_v2"
}
variable "kubernetes_version" {
type = string
description = "(Optional) Version of Kubernetes specified when creating the AKS managed cluster."
default = "1.19.11"
}
variable "aks_service_cidr" {
type = string
description = "(Optional) The Network Range used by the Kubernetes service."
default = "192.168.0.0/20" # Default Standard AKS IP
}
variable "aks_dns_service_ip" {
type = string
description = "(Optional) IP address within the Kubernetes service address range that will be used by cluster service discovery (kube-dns)."
default = "192.168.0.10" # Default Standard AKS IP
}
variable "aks_private_cluster" {
type = bool
description = "(Optional) Should this Kubernetes Cluster have its API server only exposed on internal IP addresses? This provides a Private IP Address for the Kubernetes API on the Virtual Network where the Kubernetes Cluster is located."
default = true
}
# variable "environment" {
# type = string
# description = "Deployment environment"
# validation {
# condition = contains(["dev", "prod"], var.environment)
# error_message = "Valid value is one of the following: dev, prod."
# }
# }
variable "aks_subnet_address_prefix" {
description = "Subnet address prefix."
type = string
default = "10.0.0.0/22"
}
variable "app_gateway_subnet_address_prefix" {
type = string
description = "Subnet address prefix."
default = "10.0.4.0/27"
}
variable "app_gateway_name" {
description = "Name of the Application Gateway"
type = string
default = "agw-aks-name"
}
variable "node_pool_user" {
description = "Name of the Kubernetes Cluster Node Pool User Name"
type = string
default = "agentpool-name"
}
variable "default_node_pool_name" {
description = "Name of the Kubernetes Cluster Node Pool System Name"
type = string
default = "agentpool-name01"
}
# For a Tier: Basic no charge for the first 10 TB of data processed for a Medium Size
variable "app_gateway_tier" {
description = "Tier of the Application Gateway tier."
type = string
default = "Standard_v2" #Options: "Basic" "Web Application Firewall" "Web Application Firewall V2"
}
variable "app_gateway_tiers" {
type = map(any)
default = {
tier1 = "Standard"
tier2 = "Standard_v2"
tier3 = "WAF"
tier4 = "WAF_v2"
tier5 = "WAF_Medium"
}
}
variable "aks_enable_rbac" {
description = "(Optional) Is Role Based Access Control based on Azure AD enabled?"
type = bool
default = true
}
variable "enable_auto_scaling" {
description = "Default value is false"
type = bool
default = true
}
variable "cluster_admin" {
description = "Cluster Admin User"
type = string
default = "aksadmin"
}
variable "sotrage_account_name" {
type = string
description = "Storage Blob Container"
default = ""
}
variable "sotrage_container_name" {
type = string
description = "Storage Blob Container"
default = ""
}
variable "pip_name" {
type = string
description = "Public IP Name"
default = ""
}
variable "kubernetes_cluster_extension_name" {
type = string
description = "Kubernetes Cluster Extension Name"
default = ""
}
variable.auto.tfvars
resource_group_location = ""
resource_group_name = ""
virtual_network_name = ""
virtual_network_address_prefix = ""
aks_subnet_name = ""
appgw_subnet_name = ""
aks_cluster_name = ""
aks_os_disk_size = 50
aks_node_count = 2
max_pods = 30
max_count = 2
aks_vm_size = "Standard_D3_v2"
kubernetes_version = "1.19.11"
aks_service_cidr = "192.168.0.0/20"
aks_dns_service_ip = "192.168.0.10"
aks_private_cluster = true
aks_subnet_address_prefix = ""
app_gateway_subnet_address_prefix = ""
app_gateway_name = ""
node_pool_user = ""
default_node_pool_name = ""
app_gateway_tier = "Standard_v2"
aks_enable_rbac = true / false
enable_auto_scaling = true / false
cluster_admin = ""
sotrage_container_name = ""
sotrage_account_name = ""
pip_name = ""
kubernetes_cluster_extension_name = "aksbackup"
Erstellen eines GitLab-Projekts und Hochladen des Codes:
Gehen Sie zu Gitlab.com, melden Sie sich mit Ihren Anmeldeinformationen an, wählen Sie „Neues Projekt“ und „Leeres Projekt erstellen“:
Geben Sie ihm einen Namen und klicken Sie auf die Schaltfläche „Projekt erstellen“:
Klonen Sie das Repository auf Ihren Computer, um Ihren Code hochzuladen:
Wir starten unsere Editor-Shell-Sitzung (Visual Studio Code, Powershell, etc..), navigieren zu dem Verzeichnis unserer Wahl, in dem das Repository abgelegt werden soll, und klonen anschließend das neu erstellte Repository mit dem nachstehenden Befehl:
git clone <Repository-URL>
Nachdem das Repository erfolgreich auf dem lokalen System geklont wurde, erfolgt die Ablage der zuvor besprochenen Dateien gemäß der folgenden Anweisungen:
git init # Wir initializieren Git
git add . # Um alle Änderungen hinzuzufügen, die übernommen werden sollen (WICHTIG: der Punkt am Ende, muss mitgegeben werden)
git status # Um anzuzeigen, welche Dateien hochgeladen werden
git commit -m "Terraform-Code hinzugefügt" # Um die Änderungen zu übernehmen
git push # Um die Änderungen in das Repo zu übertragen
Im Portal können wir überprüfen, ob die Dateien vorhanden sind:
Vorbereiten der Gitlab-Pipeline
Die nachstehende YAML-Datei definiert die Pipeline-Konfiguration. In dieser Konfiguration werden die einzelnen Schritte festgelegt, die der Prozess zur Bereitstellung des Terraform-Codes durchlaufen soll.
.gitlab-ci.yaml
stages:
- validate
- plan
- apply
default:
image:
name: hashicorp/terraform:latest
entrypoint:
- /usr/bin/env
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
before_script:
- terraform init -reconfigure
cache:
key: terraform
paths:
- .terraform
terraform_validate:
stage: validate
script:
- terraform validate
terraform_plan:
stage: plan
script:
- terraform plan -out plan
artifacts:
paths:
- plan
terraform_apply:
stage: apply
script:
- terraform apply --auto-approve plan
when: manual
allow_failure: false
only:
refs:
- main
Der erste Teil des Skripts gibt die einzelnen Phasen der Pipeline an:
Der zweite Teil zeigt, welches Image für alle Jobs in der Pipeline ausgeführt werden sollen. In diesem Fall verwenden wir ein Image von Hashicorp, das für Terraform bestimmt ist:
Der dritte Teil zeigt, was vor dem Skript ausgeführt wird. In diesem Fall ist „terraform init“ ein Befehl zum Initialisieren von Terraform, um alle Pakete und Module herunterzuladen, die für die ordnungsgemäße Funktion von Terraform erforderlich sind. Diese Informationen werden in einem Ordner namens .terraform innerhalb des Agenten zwischengespeichert:
Der vierte Teil überprüft, ob das Skript syntaktisch korrekt ist, ob alle Variablen ausgefüllt sind:
Im nächsten Abschnitt wird der Terraform-Plan ausgeführt und die Plandatei als Artefakt generiert, das im nächsten Schritt von Terraform Apply verwendet werden kann:
Der letzte Teil des Skripts wendet die Konfiguration mithilfe der im vorherigen Schritt generierten Plandatei an und muss manuell ausgelöst werden, d. h. jemand muss die Ausführung dieses Auftrags zulassen:
Sobald wir die yaml.datei hochladen werden die Prozesse bzw. die Pipeline gestartet, weil wir die AutoDevops-Funktion aktiviert haben, die jedes Mal eine Pipeline auslöst, wenn ein Commit an diesen Branch erfolgt.