Archiv des Autors: schaedlerdaniel

Über schaedlerdaniel

- ASP.NET - Powershell Core - Terraform

QuickTipp: Terraform sicher in Azure erstellen

In diesem Artikel gehe ich darauf ein, wie man die Ressourcen mit Terraform in Azure in einer Kadenz von 5 Minuten provisionieren und wieder abbauen kann. Dieses Szenario kann durchaus bei BDD-Test vorkommen, bei deinen Ad-Hoc eine Testinfrastruktur hochgefahren werden muss.

Vorausetzungen

Ausgangslage

Währen der Provisionierung in der Pipeline, kann es immer wieder vokommen, dass bereits eine Ressource noch vorhanden sei, wenn diese rasch wieder erstellt werden muss. Dann sieht man folgende Fehlermeldung:

    │ Error: waiting for creation/update of Server: (Name "sqldbserver" / Resource Group "rg-switzerland"): Code="NameAlreadyExists" Message="The name 'sqldbserver.database.windows.net' already exists. Choose a different name."
    │
    │   with azurerm_mssql_server.sqlsrv,
    │   on main.tf line 52, in resource "azurerm_mssql_server" "sqlsrv":
    │   52: resource "azurerm_mssql_server" "sqlsrv" {

Ärgerlich, wenn man sich darauf verlassen möchte, dass die Test-Umgebung immer gleich aufgebaut werden soll.

Die Lösung

Um diesem Problem Herr zu werden, reicht es wenn man den Ressourcen einen zufälligen Namenszusatz vergibt. Dies kann mit dem Terraform Integer eine einfache Abhilfe geschaffen werden. Dazu braucht es im Terraform Script nur folgende Ressource:

    resource "random_integer" "salt"{
        min = 1
        max = 99
    }

Der Umstand, dass Zahlen zwischen 1 und 99 generiert werden, in einer Zufälligkeit, lässt die Wahrscheinlichkeit, dass eine Ressource bereits besteht und des zu einem Fehler kommt, minimieren.

Das Anlegen einer Ressource-Gruppe mit zufälligem Namenssuffix würde dann wie folgt aussehen.

# Create a resource group
    resource "azurerm_resource_group" "rg" {
      name     = "rg-swiss-${random_integer.salt.result}"
      location = "switzerlandnorth"  
    }

Mit einem Testscript dass fünf mal durchläuft mit einer realistischen Pause von 5min während den Ausführungen hat keinen Fehler ausgegeben.

    $totalseconds = 0;
    $stopwatch = New-Object -TypeName System.Diagnostics.Stopwatch
    
    for ($index = 0; $index -lt 5; $index++) {
        $stopwatch.Reset()
        $stopwatch.Start()
    
        $executable = "terraform.exe"
            
        $initargument = "init"
        $plangargument = "plan -out testplan"
        $applyargument = "apply -auto-approve testplan"
        $destroyargeument = "destroy -auto-approve"
        
        Start-Process $executable -ArgumentList $initargument -Wait -NoNewWindow
        Start-Process $executable -ArgumentList $plangargument -Wait -NoNewWindow
        Start-Process $executable -ArgumentList $applyargument -Wait -NoNewWindow
        Start-Process $executable -ArgumentList $destroyargeument -Wait -NoNewWindow
    
        $stopwatch.Stop()
        $totalseconds += $stopwatch.Elapsed.TotalSeconds
        Start-Sleep -Seconds 480
    }
    
    Write-Host "Verstrichene Zeit $totalseconds"

Natürlich kann die Dauer mit der aktuellen Verfügbarkeit von Azure Dienste zusammenhängen. Bei der Vergabe der Namen für die Ressourcen ist es immer ratsam die Azure API der einzelnen Ressourcen zu konsultieren um keinen Fehler in der Länge des Namens zu generieren. Denn im Gegensatz zu früher, wo Namen noch wichtig waren, sind es heute nur noch Ressourcen, die nicht mehr für eine lange Existenz bestimmt sind, in Zeiten von DevOps Praktiken.

Fazit

Mit dieser Lösung kann sichergestellt werden, dass man sich Umgehungslösungen baut, die dann nur einen kleinen Zeitraum funktionieren. Ich hoffe der Artikel hat gefallen.

Quickstart: Bereitstellung statischer Webseite auf Azure

In diesem Artikel möchte ich die Schritte für das Veröffentlichen einer statischen Webseite, zum Beispiel einer "Landing Page" mit Terraform und Azure zeigen.

Voraussetzung

  • Ein Azure Konto ist eingerichtet.
  • Die Azure Cli Tools müssen für das jeweilige Zielsystem installiert sein.
  • Terraform ist installiert und konfiguriert für den Zugriff auf Azure.

Vorgehen

Folgende Schritte werden in der Terraform ausgeführt, damit eine statische Webseite auf Azure veröffentlicht werden kann.

StorageAccount und statische WebApp erstellen

Im ersten Schritt wird ein StorageAccount erstellt.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=3.0.0"
    }
  }
}

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {}

  subscription_id = "YOUR SUBSCRIPTION"
  client_id       = "YOU APPLICATION ID"
  client_secret   = "YOUR APPLICATION SECRET"
  tenant_id       = "YOUR TENANT ID"
}

resource "azurerm_resource_group" "rg" {
  name = "terrfaform-playground"
  # Westeurope da statische Webseiten in der Schweiz
  # noch nicht verfügbar sind.
  location = "westeurope"
}

resource "azurerm_storage_account" "storage" {
  account_tier = "Standard"
  account_kind = "StorageV2"
  account_replication_type = "LRS"
  location = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  name = "schaedldstorage"  
  allow_nested_items_to_be_public = true
  static_website {
    index_document = "index.html"
  }
}

Die Befehle terraform init, terraform plan -out sampleplan, terraform apply sampleplan und terraform destroy (In Produktion eher vorsichtig damit umgehen) ausgeführt. Diese sind durchgängig durch das ganze Beispiel immer wieder anzuwenden.

Terraform init


terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/azurerm versions matching "3.0.0"...
- Installing hashicorp/azurerm v3.0.0...
- Installed hashicorp/azurerm v3.0.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Terraform plan

     terraform plan -out simpleplan

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_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
      + location = "westeurope"
      + name     = "terrfaform-playground"
    }

  # azurerm_static_site.website will be created
  + resource "azurerm_static_site" "website" {
      + api_key             = (known after apply)
      + default_host_name   = (known after apply)
      + id                  = (known after apply)
      + location            = "westeurope"
      + name                = "sample-web-app"
      + resource_group_name = "terrfaform-playground"
      + sku_size            = "Free"
      + sku_tier            = "Free"
    }

  # azurerm_storage_account.storage will be created
  + resource "azurerm_storage_account" "storage" {
      + access_tier                       = (known after apply)
      + account_kind                      = "StorageV2"
      + account_replication_type          = "LRS"
      + account_tier                      = "Standard"
      + allow_nested_items_to_be_public   = true
      + 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                              = "schaedldstorage"
      + 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)
      + queue_encryption_key_type         = "Service"
      + resource_group_name               = "terrfaform-playground"
      + 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)
      + shared_access_key_enabled         = true
      + table_encryption_key_type         = "Service"

      + blob_properties {
          + change_feed_enabled      = (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)
            }
        }

      + 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)
              + versions                        = (known after apply)
            }
        }
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Terraform apply


terraform apply sampleplan    
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 0s [id=/subscriptions/YOUR SUBSCRIPTION/resourceGroups/terrfaform-playground]
azurerm_storage_account.storage: Creating...
azurerm_storage_account.storage: Still creating... [11s elapsed]
azurerm_storage_account.storage: Still creating... [21s elapsed]
azurerm_storage_account.storage: Creation complete after 22s [id=/subscriptions/YOUR SUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Storage/storageAccounts/schaedldstorage]
azurerm_static_site.website: Creating...
azurerm_static_site.website: Creation complete after 3s [id=/subscriptions/YOUR SUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Web/staticSites/sample-web-app]        

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

In Azure ist der StorageAccount erstellt worden.

Azure: StorageAccount erstellt.

Nun können die weiteren Elemente hinzugefügt werden. Gemäss der Anleitung für das Hosten von statischen Webseiten wird noch überprüft ob die Konfiguration von Terraform mit der dokumentierten übereinstimmt.

Azure: Statische Webseite aktiviert.

Hochladen der Landing Page

Azure bietet nicht die Möglichkeit ein Objekt direkt mit Terraform im StorageAccount zu erstellen, sodass ein anderer Weg zur Publizierung gewählt werden muss. Hierzu kann einer der drei dokumentierten Wege gewählt werden:

  • Über das Portal (wenig Automatisierungspotential)
  • Über die Azure Cli Tools
  • Über die Powershell

Das Powershell script ist schnell erläutert:

$storageAccount = Get-AzStorageAccount -Name "schaedldstorage" -ResourceGroupName "terrfaform-playground"
$storageAccountContext = $storageAccount.Context
Set-AzStorageBlobContent -Context $storageAccountContext -Blob "index.html" -File "..\content\index.html" -Container `$web -Properties @{ ContentType="text/html; charset=utf-8;"}

Beim Erstellen des StorageAccounts, wird ein Container automatisch mit dem Namen $web angelegt. Diesen kann man dann für das Hosten verwenden (das Script kopiert die Datei in diesen Container.)

Azure: Web Container

Fazit

Mir nur wenig Aufwand, kann ein erster Kontaktpunkt zu einer neuen Firma auf Azure bereitgestellt werden. Dies ist nur ein Beispiel und hat noch keine Sicherheitsfunktionen aktiviert (vgl. Hosting a static Webseite in Azure Storage). Jedoch ist es weniger simple das Ganze, wie in AWS nur mit Terraform zu bewerkstelligen.

Quickstart mit Azure und Terraform

Was braucht es dazu?

Folgende Voraussetzungen müssen gegeben sein:

  1. Erstellen einer app im Azure Portal.
  2. Kopieren der Schlüssel
  3. Verifizierung des Zugriffes mit den Azure Cli Tools.
  4. Terraform muss installiert sein.

Erstellen einer App im Azure Portal

Um automatisiert Ressourcen auf Azure erstellen zu können, muss vorgängig eine App im Active Directory erstellt werden.

  1. Im Azure Portal Active Directory, App Registrierung auswählen.
Azure: App Registrierung
  1. Nun wählt man neue Registrierung hinzufügen.
Azure: App Registrierung hinzufügen.
  1. Anschliessend im Menü Zertifikate und Geheimnisse ein neuer Geheimer Clientschlüssel erstellen ein.
Azure: Geheimer Schlüssel erstellen.
  1. Nun ist es wichtig, beide Schlüssel zu notieren.
  1. Nun muss noch die Client Id aufgeschrieben werden. Diese findet man in der App Registrierungs-Übersicht.
Azure: Schlüssel erstellen.

Diese können, wenn man sich mit den Azure Cli Tools einmal angemeldet hat mit folgendem Befehl herausgefunden werden:

az account list
  1. Nun muss über das Abonnement und die Zugriffsteuerung der erstellten App eine Rolle zugewiesen werden, damit diese funktioniert. In diesem Beispiel ist die Rolle "Mitwirkender" verwendet worden (Hängt vom Anwendungsfall in der Firma ab, welche tatsächliche Rolle vergeben wird.) Dies geschieht über "Rollenzuweisung hinzufügen".
Azure: Rollenzuweisung hinzufügen.
  1. Anschliessend muss man die zuvor erstellte App hinzufügen. Diese kann mittels Suchfeld gesucht und hinzugefügt werden.
Azure: Applikation hinzufügen.

Verifizierung des Zugriffes mit den Azure Cli Tools

Als erstes müssen die Azure Cli Tools bereits installiert sein.

Sobald die Azure Cli Tools installiert sind, kann man sich mit dem Service Principal versuchen anzumelden.

PS C:\Git-Repos\blogposts> az login --service-principal -u "YOUR APP ID"  -p "APP ID SECRET" --tenant "YOUR TENANT ID"
[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "YOUR TENANT",
    "id": "YOUR SUBSCRIPTION",
    "isDefault": true,
    "managedByTenants": [],
    "name": "Pay-As-You-Go",
    "state": "Enabled",
    "tenantId": "YOUR TENANT",
    "user": {
      "name": "YOUR APP ID",
      "type": "servicePrincipal"
    }
  }
]
PS C:\Git-Repos\blogposts> 

Terraform SetUp

Damit Terraform funktioniert, müssen die zuvor heruntergeladenen Schlüssel eingetragen werden. Diese Konfiguration sieht dann wie folgt aus:

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {}

  subscription_id = "ABONNEMENT ID"
  client_id       = "ZUVOR ERSTELLTE APPLIKATIONS ID"
  client_secret   = "ZUVOR ERSTELLTES GEHEIMNIS IN DER APP"
  tenant_id       = "TENANT ID"
}

Damit auch hier überprüft werden kann ob die Verbindung mit Azure funktioniert kann auch ein StorageAccount erstellt werden mit den Terraform Ressourcen.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=3.0.0"
    }
  }
}

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {}

  subscription_id = "YOURSUBSCRIPTION"
  client_id       = "APP ID"
  client_secret   = "SECRET ID"
  tenant_id       = "YOUR TENANT"
}

resource "azurerm_resource_group" "rg" {
  name = "terrfaform-playground"
  location = "switzerlandnorth"
}

resource "azurerm_storage_account" "storage" {
  account_tier = "Standard"
  account_replication_type = "LRS"
  location = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  name = "schaedldstorage"  
}

Wir terraform dann in seiner Reihenfolge mit

terraform init                

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/azurerm from the dependency lock file
- Using previously-installed hashicorp/azurerm v3.0.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform plan -out sampleplan

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_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
      + location = "switzerlandnorth"
      + name     = "terrfaform-playground"
    }

  # azurerm_storage_account.storage will be created
  + resource "azurerm_storage_account" "storage" {
      + access_tier                       = (known after apply)
      + account_kind                      = "StorageV2"
      + account_replication_type          = "LRS"
      + account_tier                      = "Standard"
      + allow_nested_items_to_be_public   = true
      + 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                          = "switzerlandnorth"
      + min_tls_version                   = "TLS1_2"
      + name                              = "schaedldstorage"
      + 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)
      + queue_encryption_key_type         = "Service"
      + resource_group_name               = "terrfaform-playground"
      + 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)
      + shared_access_key_enabled         = true
      + table_encryption_key_type         = "Service"

      + blob_properties {
          + change_feed_enabled      = (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)
            }
        }

      + 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)
              + versions                        = (known after apply)
            }
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 

Saved the plan to: sampleplan

To perform exactly these actions, run the following command to apply:
    terraform apply "sampleplan"
terraform apply simpleplan    
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 1s [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground]
azurerm_storage_account.storage: Creating...
azurerm_storage_account.storage: Still creating... [10s elapsed]
azurerm_storage_account.storage: Still creating... [20s elapsed]
azurerm_storage_account.storage: Creation complete after 21s [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Storage/storageAccounts/schaedldstorage]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Nach der Kontrolle im Azure Portal sieht man das Ergebnis.

Azure: Ressource Gruppe.

Die Ressource Gruppe ist erstellt und wenn man diese auswählt, sieht man den darin erstellten StorageAccount.

Azure: Storageaccount.

as Abräumen der Ressource kann dann wie folgt geschehen:

terraform destroy
azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground]
azurerm_storage_account.storage: Refreshing state... [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Storage/storageAccounts/schaedldstorage]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  - destroy

Terraform will perform the following actions:

  # azurerm_resource_group.rg will be destroyed
  - resource "azurerm_resource_group" "rg" {
      - id       = "/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground" -> null
      - location = "switzerlandnorth" -> null
      - name     = "terrfaform-playground" -> null
      - tags     = {} -> null
    }

  # azurerm_storage_account.storage will be destroyed
  - resource "azurerm_storage_account" "storage" {
      - access_tier                       = "Hot" -> null
      - account_kind                      = "StorageV2" -> null
      - account_replication_type          = "LRS" -> null
      - account_tier                      = "Standard" -> null
      - allow_nested_items_to_be_public   = true -> null
      - enable_https_traffic_only         = true -> null
      - id                                = "/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Storage/storageAccounts/schaedldstorage" -> null
      - infrastructure_encryption_enabled = false -> null
      - is_hns_enabled                    = false -> null
      - location                          = "switzerlandnorth" -> null
      - min_tls_version                   = "TLS1_2" -> null
      - name                              = "schaedldstorage" -> null
      - nfsv3_enabled                     = false -> null
      - primary_access_key                = (sensitive value)
      - primary_blob_connection_string    = (sensitive value)
      - primary_blob_endpoint             = "https://schaedldstorage.blob.core.windows.net/" -> null
      - primary_blob_host                 = "schaedldstorage.blob.core.windows.net" -> null
      - primary_connection_string         = (sensitive value)
      - primary_dfs_endpoint              = "https://schaedldstorage.dfs.core.windows.net/" -> null
      - primary_dfs_host                  = "schaedldstorage.dfs.core.windows.net" -> null
      - primary_file_endpoint             = "https://schaedldstorage.file.core.windows.net/" -> null
      - primary_file_host                 = "schaedldstorage.file.core.windows.net" -> null
      - primary_location                  = "switzerlandnorth" -> null
      - primary_queue_endpoint            = "https://schaedldstorage.queue.core.windows.net/" -> null
      - primary_queue_host                = "schaedldstorage.queue.core.windows.net" -> null
      - primary_table_endpoint            = "https://schaedldstorage.table.core.windows.net/" -> null
      - primary_table_host                = "schaedldstorage.table.core.windows.net" -> null
      - primary_web_endpoint              = "https://schaedldstorage.z1.web.core.windows.net/" -> null
      - primary_web_host                  = "schaedldstorage.z1.web.core.windows.net" -> null
      - queue_encryption_key_type         = "Service" -> null
      - resource_group_name               = "terrfaform-playground" -> null
      - secondary_access_key              = (sensitive value)
      - secondary_connection_string       = (sensitive value)
      - shared_access_key_enabled         = true -> null
      - table_encryption_key_type         = "Service" -> null
      - tags                              = {} -> null

      - blob_properties {
          - change_feed_enabled      = false -> null
          - last_access_time_enabled = false -> null
          - versioning_enabled       = false -> null
        }

      - network_rules {
          - bypass                     = [
              - "AzureServices",
            ] -> null
          - default_action             = "Allow" -> null
          - ip_rules                   = [] -> null
          - virtual_network_subnet_ids = [] -> null
        }

      - queue_properties {

          - hour_metrics {
              - enabled               = true -> null
              - include_apis          = true -> null
              - retention_policy_days = 7 -> null
              - version               = "1.0" -> null
            }

          - logging {
              - delete                = false -> null
              - read                  = false -> null
              - retention_policy_days = 0 -> null
              - version               = "1.0" -> null
              - write                 = false -> null
            }

          - minute_metrics {
              - enabled               = false -> null
              - include_apis          = false -> null
              - retention_policy_days = 0 -> null
              - version               = "1.0" -> null
            }
        }

      - share_properties {

          - retention_policy {
              - days = 7 -> null
            }
        }
    }

Plan: 0 to add, 0 to change, 2 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

azurerm_storage_account.storage: Destroying... [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Storage/storageAccounts/schaedldstorage]
azurerm_storage_account.storage: Destruction complete after 2s
azurerm_resource_group.rg: Destroying... [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground]
azurerm_resource_group.rg: Still destroying... [id=/subscriptions/YOURSUBSCRIPTION-...4/resourceGroups/terrfaform-playground, 10s elapsed]
azurerm_resource_group.rg: Destruction complete after 16s

Destroy complete! Resources: 2 destroyed.

Nach dessen Ausführung ist dann auch im Azure-Portal nichts mehr zu sehen.

„Azure: Ressourcegruppe entfernt.

Fazit

Ein einfacher Weg Infrastruktur auch in Azure zu erstellen, ohne die dauernde Anmeldung und der Möglichkeit, Terraform automatisiert in einer CI/CD Pipeline laufen zu lassen.

Quickstart: Bereitstellung einer statischen Webseite auf AWS

In diesem Artikel möchte ich die Schritte für das Veröffentlichen einer statischen Webseite, zum Beispiel einer "Landing Page" mit Terraform und AWS zeigen.

Voraussetzung

  • Ein AWS Konto ist eingerichtet.
  • Terraform ist installiert und konfiguriert für den Zugriff auf AWS.

Vorgehen

Folgende Schritte werden in der Terraform ausgeführt, damit eine statische Webseite auf AWS veröffentlicht werden kann.

Bucket erstellen

Im ersten Schritt wird ein Bucket erstellt.

    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 3.0"
        }
      }
    }
    
    # Configure the AWS Provider
    provider "aws" {
      region = "eu-central-1"
      access_key = "DEIN SCHLÜSSEL"
      secret_key = "DEIN SCHLÜSSEL"
    }
    
    resource "aws_s3_bucket" "webapp" {
      bucket = "schaedld-webapp"
      object_lock_enabled = false   
    }

Die Befehle terraform init, terraform plan -out sampleplan, terraform apply sampleplan und terraform destroy (In Produktion eher vorsichtig damit umgehen) ausgeführt. Diese sind durchgängig durch das ganze Beispiel immer wieder anzuwenden.

Terraform init


terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 3.0"...
- Installing hashicorp/aws v3.75.1...
- Installed hashicorp/aws v3.75.1 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

Terraform plan

    terraform plan -out sampleplan
    
    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:
    
      # aws_s3_bucket.webapp will be created
      + resource "aws_s3_bucket" "webapp" {
          + acceleration_status         = (known after apply)
          + acl                         = "private"
          + arn                         = (known after apply)
          + bucket                      = "schaedld-webapp"
          + bucket_domain_name          = (known after apply)
          + bucket_regional_domain_name = (known after apply)
          + force_destroy               = false
          + hosted_zone_id              = (known after apply)
          + id                          = (known after apply)
          + object_lock_enabled         = false
          + region                      = (known after apply)
          + request_payer               = (known after apply)
          + tags_all                    = (known after apply)
          + website_domain              = (known after apply)
          + website_endpoint            = (known after apply)
    
          + object_lock_configuration {
              + object_lock_enabled = (known after apply)
    
              + rule {
                  + default_retention {
                      + days  = (known after apply)
                      + mode  = (known after apply)
                      + years = (known after apply)
                    }
                }
            }
    
          + versioning {
              + enabled    = (known after apply)
              + mfa_delete = (known after apply)
            }
        }
    
    Plan: 1 to add, 0 to change, 0 to destroy.
    
    ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 
    
    Saved the plan to: sampleplan
    
    To perform exactly these actions, run the following command to apply:
        terraform apply "sampleplan"

Terraform apply


    terraform apply sampleplan    
    aws_s3_bucket.webapp: Creating...
    aws_s3_bucket.webapp: Creation complete after 2s [id=schaedld-webapp]
    
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

In AWS ist das Bucket erstellt worden.

AWS: Bucket erstellen erfolgreich.

Nun können die weiteren Elemente hinzugefügt werden.

Erstellen der Webseiten Konfiguration

Damit das Bucket auch als Webseite für die Auslieferung von statischem Inhalt funktioniert, muss eine Webseitenkonfigurationselement in Terraform hinzugefügt werden. (Aus Platzgründen habe ich die vorherigen Schritte weggelassen.)

resource "aws_s3_bucket_website_configuration" "webappcfg" {
  bucket = aws_s3_bucket.webapp.bucket
  
  index_document {
    suffix = "index.html"    
  }  
}

Wird nun terraform apply sampleplan ausgeführt, so ist zu sehen, dass 2 Ressourcen erstellt worden sind.

terraform apply sampleplan
aws_s3_bucket.webapp: Creating...
aws_s3_bucket.webapp: Creation complete after 1s [id=schaedld-webapp]
aws_s3_bucket_website_configuration.webappcfg: Creating...
aws_s3_bucket_website_configuration.webappcfg: Creation complete after 0s [id=schaedld-webapp]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Klickt man im Portal auf das Bucket Objekt, gelangt man in die Verwaltungsseite des Objektes.

AWS: Bucket  Objekt

Wählt man die Option Eigenschaften, so gelangt man in die Einstellungen des Buckets. Navigiert man ans untere Ende der Seite, ist folgender Punkt zu sehen:

Hosten einer statischen Webseite.

Hier zu sehen, ist dass diese Option aktiviert ist. Wenn man nun den bereitgestellten Link anklickt, gelangt man auf eine Seite, die einem den Zugriff verwehrt, da noch kein Objekt für einen öffentlichen Lesezugriff vorhanden ist.

AWS: Access Denied.

Nun kann mit dem nächsten Schritt, dem erstellen eines Objekts für das Bucket fortgefahren werden.

Nun kann mit dem nächsten Schritt, dem erstellen eines Objekts für das Bucket fortgefahren werden.

Erstellen eines Objektes im Bucket

Als letztes Puzzle-Teilchen, ist das Objekt für das Bucket hinzuzufügen. In diesem Beispiel ist es ein einfaches Index.html, dass als "Landing Page" verwendet werden könnte, wenn man frischer Besitzer einer Domain ist und gerade die Webseite aufbaut.

resource "aws_s3_bucket_object" "index" {
  bucket = aws_s3_bucket.webapp.bucket
  content_type = "text/html"
  key = "index.html"
  content = <<EOF
            
            
            <a href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js">https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js</a>
            
              
                <div class="container py-4">
                    <div class="p-5 mb-4 bg-light rounded-3">
                      <div class="container-fluid py-5">
                        <h1 class="display-5 fw-bold">Custom jumbotron</h1>
                        <p class="col-md-8 fs-4">Using a series of utilities, you can create this jumbotron, just like the one in previous versions of Bootstrap. Check out the examples below for how you can remix and restyle it to your liking.</p>
                        Example button
                      </div>
                    </div>
                    </div>
                    <footer class="pt-3 mt-4 text-muted border-top">
                      © 2021
                    </footer>
                  </div>
                
              
  EOF
  acl = "public-read"
}

In diesem Beispiel ist eine Webseite als Multiline Content von Terraform HEREDOC Strings drin, die erstellt wird.

Die Ausführung mit terraform apply zeigt dass eine dritte Ressource erstellt worden ist.

    terraform apply sampleplan
    aws_s3_bucket.webapp: Creating...
    aws_s3_bucket.webapp: Creation complete after 2s [id=schaedld-webapp]
    aws_s3_bucket_website_configuration.webappcfg: Creating...
    aws_s3_bucket_object.index: Creating...
    aws_s3_bucket_object.index: Creation complete after 0s [id=index.html]
    aws_s3_bucket_website_configuration.webappcfg: Creation complete after 0s [id=schaedld-webapp]
    
    Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Die Kontrolle im AWS Portal, des Buckets offenbar, dass diese Datei angelegt worden ist.

AWS: Bucket Objekt erstellt.

Wir der Link, in den Eigenschaften des Buckets unter "Hosten einer statischen Webseite" angeklickt so ist nicht mehr die Access Denied Meldung zu sehen, sondern die bereitgestellte Webseite.

AWS: Landing Page erstellt.

Die komplette Terraform Konfiguration sieht dann wie folgt aus:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&gt; 3.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "eu-central-1"
  access_key = "AKIAXEP7TRWQSFUAJI65"
  secret_key = "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"
}

resource "aws_s3_bucket" "webapp" {
  bucket = "schaedld-webapp"
  object_lock_enabled = false   
}


resource "aws_s3_bucket_website_configuration" "webappcfg" {
  bucket = aws_s3_bucket.webapp.bucket
  
  index_document {
    suffix = "index.html"    
  }  
}

resource "aws_s3_bucket_object" "index" {
  bucket = aws_s3_bucket.webapp.bucket
  content_type = "text/html"
  key = "index.html"
  content = &lt;&lt;EOT
            
            
            <a href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js">https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js</a>
            
              
                <div class="container py-4">
                    <div class="p-5 mb-4 bg-light rounded-3">
                      <div class="container-fluid py-5">
                        <h1 class="display-5 fw-bold">Willkommen</h1>
                        <p class="col-md-8 fs-4">Willkommen auf der Landing Page der Firma Software Sorglos.</p>                        
                      </div>
                    </div>
                    </div>
                    <footer class="pt-3 mt-4 text-muted border-top">
                      © 2021
                    </footer>
                  </div>
                
              
  EOT
  acl = "public-read"
}

Fazit

Mir nur wenig Aufwand, kann ein erster Kontaktpunkt zu einer neuen Firma auf AWS bereitgestellt werden. Dies ist nur ein Beispiel und hat noch keine Sicherheitsfunktionen aktiviert (vgl. Hosting a static Webseite using Amazon S3).

Quickstart mit AWS und Terraform

In diesem Artikel gehe ich darauf ein, wie man sich vorbereitet, um mit Terraform und AWS arbeiten zu können.

Was braucht es dazu?

Folgende Voraussetzungen müssen erfüllt sein:

  1. Erstellen eines Benutzers im AWS Account, der dann über die API zugreifen kann.
  2. VS Code AWS Extensions runterladen und konfigurieren für den Zugriff auf AWS.
  3. Zugriffsschlüssel erstellen und als CSV herunterladen.
  4. Verifizierung des Zugriffes über die Extension aus Visual Studio Code.
  5. Terraform muss installiert sein.

Erstellen eines IAM Benutzers in AWS

Um automatisiert Ressourcen auf AWS erstellen zu können, muss vorgängig ein sogenannter IAM-Benutzer erstellt werden. Hier kann wie folgt vorgegangen werden.

  1. Im AWS Konto auf die Identity und Accessmanagement navigieren. Man sollte nun schon auf der richtigen Maske landen.
IAM Benutzer in AWS erstellen.
  1. Mit dem drücken des Knopfes "neuer Benutzer" gelangt man die nachfolgende Ansicht für die Parametrisierung des Benutzers.
Neuer Benutzer hinzufügen.

Wichtig hierbei ist, dass die der Haken bei den CLI Tools gesetzt wird, damit man später mit Terraform darauf zugreifen kann.

  1. Nun können die notwendigen Berechtigungen hinzugefügt werden.
Berechtigungen hinzufügen.

  1. Wenn der Benutzer erstellt worden ist, kopiert euch den Access Key und den private Key oder ladet diesen als CSV herunter, damit diese in den nächsten Schritten weiter verwendet werden können.

AWS Extension Visual Studio Code

Als erstes muss im Marketplace von Visual Studio Code nach der Extension für AWS gesucht werden um diese dann installieren zu können.

AWS Extension hinzufügen.

Nun sind die Erweiterungen für AWS installiert. Diese müssen nun konfiguriert werden. Dies kann mit Hilfe der AWS-Erweiterung durchgeführt werden die durch den SetUp Prozess führt. Hierbei ist es wichtig, dass der Access Key und der private Schlüssel notiert worden sind.

Um zu testen ob eine Verbindung mit den Tools auf AWS gemacht werden kann, reicht es nach deren Konfiguration einfach in der Menüleiste eine Ressource zu erstellen um zu sehen ob die Verbindung geklappt hat.

Ich habe mit einem S3 Bucket getestet und bin wie folgt vorgegangen.

  1. Bucket Option in den Erweiterungen auswählen.
AWS Extension Bucket erstellen.
  1. Anschliessend muss nur noch ein Name eingegeben werden, der eineindeutig sein muss.
AWS Extensions Bucket Name eingeben.

  1. Hat man Erfolg und einen eineindeutigen Namen erwischt so kann ein Bucket erstellt werden und man erhält eine Erfolgsmeldung.
AWS Extension Bucket erfolgreich erstellt.

Nun sind alle Schritte gemacht und die Verbindung zu AWS funktioniert.

Terraform SetUp

Damit Terraform funktioniert, müssen die zuvor heruntergeladenen Schlüssel eingetragen werden. Diese Konfiguration sieht dann wie folgt aus:

provider "aws" {
  region = "eu-central-1"
  access_key = "DEINACCESSKEY"
  secret_key = "DEINSECRETKEY"
}

Damit auch hier überprüft werden kann ob die Verbindung mit AWS funktioniert kann auch ein Bucket erstellt werden mit den Terraform Ressourcen.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&gt; 3.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "eu-central-1"
  access_key = "DEINACCESSKEY"
  secret_key = "DEINSECRETKEY"
}

resource "aws_s3_bucket" "samplebucket" {
  bucket = "schaedlds-sample-bucket"
  object_lock_enabled = false   
}

Wir terraform dann in seiner Reihenfolge mit

terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~&gt; 3.0"...
- Installing hashicorp/aws v3.75.1...
- Installed hashicorp/aws v3.75.1 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform plan -out sampleplan

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:

  # aws_s3_bucket.samplebucket will be created
  + resource "aws_s3_bucket" "samplebucket" {
      + acceleration_status         = (known after apply)
      + acl                         = "private"
      + arn                         = (known after apply)
      + bucket                      = "schaedlds-sample-bucket"
      + bucket_domain_name          = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = false
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + object_lock_configuration {
          + object_lock_enabled = (known after apply)

          + rule {
              + default_retention {
                  + days  = (known after apply)
                  + mode  = (known after apply)
                  + years = (known after apply)
                }
            }
        }

      + versioning {
          + enabled    = (known after apply)
          + mfa_delete = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────── 

Saved the plan to: simpleplan
terraform apply sampleplan

aws_s3_bucket.samplebucket: Creating...
aws_s3_bucket.samplebucket: Creation complete after 2s [id=schaedlds-sample-bucket]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Nach der Kontrolle im AWS Dashboard sieht man das Ergebnis.

AWS Portal Bucket erfolgreich erstellt mit Terraform.

Das Abräumen der Ressource kann dann wie folgt geschehen:

terraform destroy 

aws_s3_bucket.samplebucket: Refreshing state... [id=schaedlds-sample-bucket]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated    
with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_s3_bucket.samplebucket will be destroyed
  - resource "aws_s3_bucket" "samplebucket" {
      - acl                         = "private" -&gt; null
      - arn                         = "arn:aws:s3:::schaedlds-sample-bucket" -&gt; null
      - bucket                      = "schaedlds-sample-bucket" -&gt; null
      - bucket_domain_name          = "schaedlds-sample-bucket.s3.amazonaws.com" -&gt; null
      - bucket_regional_domain_name = "schaedlds-sample-bucket.s3.eu-central-1.amazonaws.com" -&gt; null
      - force_destroy               = false -&gt; null
      - hosted_zone_id              = "Z21DNDUVLTQW6Q" -&gt; null
      - id                          = "schaedlds-sample-bucket" -&gt; null
      - object_lock_enabled         = false -&gt; null
      - region                      = "eu-central-1" -&gt; null
      - request_payer               = "BucketOwner" -&gt; null
      - tags                        = {} -&gt; null
      - tags_all                    = {} -&gt; null

      - versioning {
          - enabled    = false -&gt; null
          - mfa_delete = false -&gt; null
        }
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_s3_bucket.samplebucket: Destroying... [id=schaedlds-sample-bucket]
aws_s3_bucket.samplebucket: Destruction complete after 0s

Destroy complete! Resources: 1 destroyed.

Nach dessen Ausführung ist dann auch im AWS-Portal nichts mehr zu sehen.

AWS Portal Ressource mit Terraform abgeräumt.

Fazit

Ein einfacher Weg Infrastruktur auch in AWS zu erstellen und zu löschen aber mit anderen Konzepten als in Azure.

Eine Kurzgeschichte über Pfade

Eine Kurzgeschichte über Pfade

In diesem Kurzbeitrag erläutere ich euch wie man sicher mit Pfaden in der Cross-Plattform Entwicklung umgeht.

Voraussetzungen

Folgende Voraussetzungen sind gegeben.

Eine appsettings.json Datei die wie folgt aussieht:

    "PlantUmlSettings": {
      "PlantUmlTheme": "plain",
      "PlantUmlArgs": "-jar plantuml.jar {0}\*{1}*{2} -o {3} -{4}",
      "PlantUmlEndTag": "@enduml",
      "PlantUmlExe": "plantUml\bin\java.exe",
      "PlantUmlFileSuffix": ".plantuml",
      "PlantUmlStartTag": "@startuml",
      "PlantUmlThemeTag": "!theme",
      "PlantUmlWorkDir": "plantUml\bin"
    }

Eine .NET Anwendung, die sowohl auf einem Windows, wie auch interaktiv im GitLab Runner gestartet werden kann.

Leider hatte ich dann immer folgende Fehlermeldung, als die Applikation im GitLab Runner gestartet worden ist:

/src/Sample.Cli\plantUml//bin//java.exe' with working directory '/builds/Sample/src/Sample.Cli\plantUml//bin'. No such file or directory

Detailbetrachtung

Um dem Ganzen ein wenig weiter auf die Spur zu gehen, habe ich mir eine Beispiel-Applikation geschrieben um das Verhalten auf beiden System zu betrachten.

        static void Main(string[] args)
        {
            var environmentVariable = Environment.ExpandEnvironmentVariables("tmp");
            var tempPath = Path.GetTempPath();

            Console.WriteLine($"Value for Environment Variable {environmentVariable}");
            Console.WriteLine($"Value for {nameof(Path.GetTempPath)} {tempPath}");

            Console.ReadKey();
        }

Lässt man dann das Ganze auf einem Windows System mit dotnet wie folgt laufen, sieht das Ergebnis dann so aus:

PS C:\Users\schae> dotnet run --project D:\_Development_Projects\Repos\ConsoleApp1\ConsoleApp1\ConsoleApp1.csproj
Value for Environment Variable tmp
Value for GetTempPath C:\Users\schae\AppData\Local\Temp\

Das Ergebnis ist wie gewünscht. Nun schauen wir uns das auf der WSL2 an.

root@GAMER-001:~/.dotnet# ./dotnet run --project /mnt/d/_Development_Projects/Repos/ConsoleApp1/ConsoleApp1/ConsoleApp1.
csproj
Value for Environment Variable tmp
Value for GetTempPath /tmp/

Schauen wir doch nun ob der Pfad auch existiert:

root@GAMER-001:~/.dotnet# cd /tmp/
root@GAMER-001:/tmp#

Und das ist der erhoffte Pfad.

Die Lösung

Nach ein wenig Recherchieren in der Dokumentation von Microsoft bin ich auf diesen Artikel DirectorySeperatorChar gestossen. Nicht dass er mir mit dieser Methode geholfen hätte, sondern vielmehr mit dem Auszug

The following example displays Path field values on Windows and on Unix-based systems. Note that Windows supports either the forward slash (which is returned by the AltDirectorySeparatorChar field) or the backslash (which is returned by the DirectorySeparatorChar field) as path separator characters, while Unix-based systems support only the forward slash

dass Windows auch Forward-Slashes unterstützt. Manchem wird das sicherlich schon bekannt gewesen sein aber ich selber werde wohl meine Arbeit mit Pfaden, auch in Windows in Zukunft nur noch mit Forwar-Slashes machen.

Nun habe ich das natürlich auch getestet und zwar in der powershell core.

PS C:\Temp> cd C:/Users
PS C:\Users>

Interessant ist der Umstand, dass wenn der Pfad bekannt ist, man den Tabulator betätigt, Windows automatisch Backslashes macht.

Nun sind überall wo Pfade verwendet werden, die Backward-Slashes durch Forward-Slashes zu ersetzen. Die appsettings.json sieht dann nun so aus:

    "PlantUmlSettings": {
      "PlantUmlTheme": "plain",
      "PlantUmlArgs": "-jar plantuml.jar {0}/*{1}*{2} -o {3} -{4}",
      "PlantUmlEndTag": "@enduml",
      "PlantUmlExe": "plantUml/bin/java.exe",
      "PlantUmlFileSuffix": ".plantuml",
      "PlantUmlStartTag": "@startuml",
      "PlantUmlThemeTag": "!theme",
      "PlantUmlWorkDir": "plantUml\bin"
    }

Nun sind auch keine Fehlermeldungen vorhanden, dass der Pfad nicht mehr gefunden werden kann.

Ein weiterer Punkt den ich mitnehmen werde, ist der, dass in Zukunft alles klein geschrieben wird. Auch Variablen im Windows.

Fazit

Mit einfachen Mitteln lassen sich unter Umständen Stunden des Debuggens oder der Fehlersuche vermeiden. Ich hoffe Dir hat der Beitrag gefallen.

Verwendung von Certbot und Azure

In diesem Artikel will ich zeigen, wie man den Certbot einsetzt um ein Zertifikat zu erhalten um dieses anschliessend auf Azure zu installieren.

Voraussetzungen

Folgende Voraussetzungen müssen erfüllt sein:

  • Linux Subsystem für Windows muss installiert sein mit Ubuntu.
  • Auf dem Linux Subsystem für Windows muss certbot installiert sein.
  • Eine eigene Domain und eine Webseite müssen existieren.

Vorbereitung der Webseite

Damit die Anfragen für Dateien ohne Endung auf einer ASP.NET Applikation ankommen, muss folgende Einstellung vorgenommen werden:

Das Beispiel, zeigt den WebHost der verwendet wird, für die Konfiguration der statischen Dateien.

app.UseStaticFiles(new StaticFileOptions
{
    ServeUnknownFileTypes = true, // serve extensionless files

    OnPrepareResponse = staticFileResponseContext =>
    {
        // Cache Header für Optimierung für Page Speed
        const int durationInSeconds = 60 * 60 * 24 * 365;
        staticFileResponseContext.Context.Response.Headers[HeaderNames.CacheControl] =
            "public,max-age=" + durationInSeconds;
    }
});

Durchführung

Die Durchführung lässt sich in folgende Schritte gliedern:

  1. Verbinden auf die Azure Webseite mit den Azure Cli Tools
  2. Vorbereitung des Certbots local
  3. Dateien und Ordner in der Webseite erstellen
  4. Weiterfahren mit Cerbot
  5. Konvertierung des Zertifikates
  6. Installation des Zertifikates auf Azure

Verbindung auf die Azure Webseite herstellen

Die Verbindung zu Azure und der Webseite geschieht wie folgt.

az login --use-device-code

az subscription --set <%subscriptionId%>

az webapp ssh -n <%webseiten-name%> -g <%resourcegruppenname%>

Nach erfolgtem einloggen sieht man folgenden Azure Willkommensbilschirm:

Last login: Wed Apr  6 18:38:26 2022 from 169.254.130.3
  _____                               
  /  _  \ __________ _________   ____  
 /  /_\  \___   /  |  \_  __ \_/ __ \ 
/    |    \/    /|  |  /|  | \/\  ___/ 
\____|__  /_____ \____/ |__|    \___  >
        \/      \/                  \/ 
A P P   S E R V I C E   O N   L I N U X

Documentation: http://aka.ms/webapp-linux
Dotnet quickstart: https://aka.ms/dotnet-qs
ASP .NETCore Version: 6.0.0
Note: Any data outside '/home' is not persisted
root@bcd2c665073e:~/site/wwwroot# ^C
root@bcd2c665073e:~/site/wwwroot# 

Nun muss in folgenden Ordner navigiert werden:

cd /home/wwwroot/wwwroot

Nun weiter mit dem nächsten Schritt.

### Vorbereitung des Certbots local

Sollte der Certbot noch nicht installiert sein, so kann dies mittels folgendem Befehl durchgeführt werden:

```bash
sudo apt-get install certbot

Ist der Certbot installiert, kann dieser gestartet werden, wie nachfolgen beschrieben:

sudo certbot certonly -d www.dnug-bern.ch -d dnug-bern.ch --manual

Der Certbot startet und man sieht folgende Meldungen:

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for www.dnug-bern.ch

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o:

Mit Y bestätigen und man erhält die Instruktionen, wie weiter vorzugehen ist.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Create a file containing just this data:

JxOqa2wKiSbAq5R_o66Gs5_sEE9xhuDwyPbv6pfEOJ8.RN326tu-yly1wbWDsnoT5mbba-NazH6fhba6WeEfA2s

And make it available on your web server at this URL:

http://www.dnug-bern.ch/.well-known/acme-challenge/JxOqa2wKiSbAq5R_o66Gs5_sEE9xhuDwyPbv6pfEOJ8

Achtung: Hier nicht bestätigen, ansonsten wird die Validierung fehlschlagen

Dateien und Ordner in der Webseite erstellen

Nun können wir in der bereits geöffneten Webseite in Azure in dem Ordner weiterfahren, in welchem wir vorher schon navigiert sind.

Nun wird der Ordner erstellt, den Certbot verlangt. Um dies zu bewerkstelligen muss wie folgt vorgegangen werden:

mkdir .well-known
mkdir acme-challenge

Nun muss die Datei erstellt werden. In unserem Fall soll die Datei so heissen: JxOqa2wKiSbAq5R_o66Gs5_sEE9xhuDwyPbv6pfEOJ8

Um dies zu erreichen muss zuerst vim gestartet werden. Hier muss dann die folgende Zeile eingefügt werden: JxOqa2wKiSbAq5R_o66Gs5_sEE9xhuDwyPbv6pfEOJ8.RN326tu-yly1wbWDsnoT5mbba-NazH6fhba6WeEfA2s. Anschliessend ist die Datei und dem folgenden Namen zu speicher (mit :w in vim) JxOqa2wKiSbAq5R_o66Gs5_sEE9xhuDwyPbv6pfEOJ8

Anschliessend kann die Webseite für das Testen einmal mit der URL die der Certbot angegeben hat, aufgerufen werden. Ist dies erfolgreich, so wird die Datei aufgerufen und man sieht die erfasste Zeichenfolge.

Weiterfahren mit Certbot

Da der Certbot noch darauf wartet eine Bestätigung für die Validierung zu erhalten, drücken wir nun im noch geöffneten Dialog die ENTER-Taste. Ist alles in Ordnung, so erhält man eine Erfolgsmeldung, dass die Validierung erfolgreich war.

Konvertierung des Zertifikates

Anschliessend muss die PEM Datei in eine PFX-Datei umgewandelt werden. Dies erfolgt wie nachfolgend beschrieben:

openssl pkcs12 -inkey privkey.pem -in cert.pem -export -out dnug.bern.pfx

Installation des Zertifikates auf Azure

Anschliessend muss nach nach folgender Anleitung das Zertifikat in Azure hochgeladen werden.

Fazit

So kann in einfachen Schritten das Zertifikat mit Certbot, zwar manuel aktualisiert werden und es entstehen keine weiteren Kosten. Der Nachteil ist, dass in kurzen Intervallen die Aktualisierung durchgeführt werden muss. Ich hoffe Dir hat Dieser Blogbeitrag gefallen.

Meine ersten Schritte mit Terraform

In diesem Artikel ist die Vorgehensweise beschrieben wie man eine Terraform Umgebung für Tests auf- und wieder abbauen kann. Hierzu habe ich Microsoft Azure verwendet. Diese ist im Rahmen der dotnet user group Seite geschehen um so die Tests mit Browserstack auf einer virtuellen Umgebung durchführen zu können.

Vorausetzungen

Ziel

Das Ziel soll sein, dass eine Testumgebung bestehend aus folgenden Komponenten

  • SQL Server
  • App Service hochfährt, die automatisierten Tests durchläuft und danach gelöscht wird.

Vorgehensweise

Die nachfolgenden Schritte beschreiben die Vorgehensweise in 3 Schritten.

  • Identifizieren der benötigten Ressourcen
  • Identifizieren der Terraform Ressourcen
  • Zusammenführen

Identifizieren der Terraform Ressourcen

Wirf man ein Auge auf die Azure Automation, so sticht die sehr grosse JSON-Datei ins Auge. Sie kann als Anhaltspunkt verwendet werden, muss aber nicht. Schaut man sich das so an, dann sind das eine Menge an Ressourcen, die da Verwendung finden. Nun stellt Terraform unter folgendem Link, die aktuellen Ressourcen für Azure zur Verfügung. Folgende Ressourcen können dafür Verwendung finden:

Zusammenführen

Nun muss das Ganze in einer Terraform Datei zusammengeführt werden. Hierzu wird ein Ordner erstellt, zum Beispiel dotnet-usergroup-bern-terraform-configuration. In diesem Ordner ist dann eine main.tf Datei zu erstellen, die die ganze Terraform Konfiguration beinhaltet. Diese Datei sieht dann wie so aus:

# Configure the dotnet-user-group test environment

terraform{
    required_providers{
        azurerm = {
            source = "hashicorp/azurerm"
            version = ">=2.26"
        }
    }
}

provider "azurerm"{
    features{}
}

resources "azurerm_resource_group" "bddtest"{
    name = "bddtest-dotnet-usergroup"
    location = "switzerlandnorth"
}

// SQL configuration
resource "azurerm_storage_account" "bddtest"{
    name = "bdd-test-dotnet-usergroup"
    resource_group_name = azurerm_resource_group.bddtest.name
    location = azurerm_resource_group.bddtest.location
    account_tier = "Free"
    account_replication_type = "GRS"
}

resource "azurerm_sql_server" "bddtest" {
  name                         = "bddtest-sqlserver"
  resource_group_name          = azurerm_resource_group.bddtest.name
  location                     = azurerm_resource_group.bddtest.location
  version                      = "12.0"
  administrator_login          = "4dm1n157r470r"
  administrator_login_password = "4-v3ry-53cr37-p455w0rd"
}

resource "azurerm_mssql_database" "bddtest" {
  name           = "bddtest-dnugberndev"
  server_id      = azurerm_sql_server.bddtest.id
  collation      = "SQL_Latin1_General_CP1_CI_AS"
  license_type   = "LicenseIncluded"
  max_size_gb    = 4
  read_scale     = false
  sku_name       = "Basic"
  zone_redundant = false

  extended_auditing_policy {
    storage_endpoint                        = azurerm_storage_account.example.primary_blob_endpoint
    storage_account_access_key              = azurerm_storage_account.example.primary_access_key
    storage_account_access_key_is_secondary = true
    retention_in_days                       = 6
  }

// App Service configuration (Web Apps)
resource azurerm_app_service_plan "bddtest"{
    name = "bddtest-appserviceplan"
    location = azurerm_resource_group.bddtest.location
    resource_groupe_name = azurerm_resource_group.bddtest.name

    sku{
        tier = "Basic"
        size = "F1"
        name = "F1"
    }
}
 

Natürlich sind hier noch nich alle definitiven Werte eingetragen, sodass während der Build-Zeit diese Konfiguration erstellt wird. Diesen Punkt werde ich in Zukunft behandeln.

Erstellung der Ressourcen
Nun ist es an der Zeit, zu sehen ob die Konfiguration, die da entstanden ist, auch ohne Fehler angewendet werden kann.
Hierzu bin ich wie folgt vorgegangen:
1. Anmelden in Azure (Diese Methode habe ich gewählt, weil bei mir der Browser oder die Browser ein wenig gezickt haben.)

az login --use-device-code

Nach erfolgter Anmeldung, sind die Azure-Abos ersichtlich.

[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "YYYYYYYY",
    "id": "0000000-11111-2222-3333-555555555555",
    "isDefault": true,
    "managedByTenants": [],
    "name": "Pay-As-You-Go",
    "state": "Enabled",
    "tenantId": "ZZZZZZZ",
    "user": {
      "name": "hansmustter@windowslive.com",
      "type": "user"
    }
  },
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "XXXXXXX",
    "id": "0000000-11111-2222-3333-444444444444",
    "isDefault": false,
    "managedByTenants": [],
    "name": "Visual Studio Enterprise mit MSDN",
    "state": "Enabled",
    "tenantId": "XXXXXX",
    "user": {
      "name": "hansmuster@windowslive.com",
      "type": "user"
    }
  }
]

Nun, wenn man die Subscription wechseln will, muss man mit den Azure CLI-Werkzeugen folgenden Befehl ausführen:

az account list

[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "YYYYYYYY",
    "id": "0000000-11111-2222-3333-555555555555",
    "isDefault": false,
    "managedByTenants": [],
    "name": "Pay-As-You-Go",
    "state": "Enabled",
    "tenantId": "ZZZZZZZ",
    "user": {
      "name": "hansmustter@windowslive.com",
      "type": "user"
    }
  },
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "XXXXXXX",
    "id": "0000000-11111-2222-3333-444444444444",
    "isDefault": true,
    "managedByTenants": [],
    "name": "Visual Studio Enterprise mit MSDN",
    "state": "Enabled",
    "tenantId": "XXXXXX",
    "user": {
      "name": "hansmuster@windowslive.com",
      "type": "user"
    }
  }
]

das die Default Subscription geändert hat.

  1. Nun muss der Plan von Terraform initiiert werden.
.\terraform.exe plan -out .\myplan
terraform init
  1. Anschliessend muss
.\terraform.exe plan -out .\myplan

ausgeführt werden. Wobei der Out Parameter optional ist. Bei der Ausführung speichert Terraform mit diesem Parameter den Plan als Datei im aktuellen Verzeichnis oder in dem angegeben im out-Parameter, ab. Verlief alles nach Plan, so zeigt Terraform den Plan an. Die + Zeichen deuten darauf hin, dass diese Ressourcen erstellt werden. Der nachfolgende Plan ist der Übersicht halber symbolisch dargestellt. In Wirklichkeit ist dieser um einiges länger.

------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_app_service.bddtest will be created
  + resource "azurerm_app_service" "bddtest" {
      + app_service_plan_id            = (known after apply)
      + app_settings                   = {
          + "SOME_KEY" = "some-value"
        }
      + client_affinity_enabled        = false
      + client_cert_enabled            = false
      + default_site_hostname          = (known after apply)
      + enabled                        = true
      + https_only                     = false
      + id                             = (known after apply)
      + location                       = "switzerlandnorth"
      + name                           = "bddtest-dnug-bern"
      + outbound_ip_addresses          = (known after apply)
      + possible_outbound_ip_addresses = (known after apply)
      + resource_group_name            = "bddtest-dotnet-usergroup"
      + site_credential                = (known after apply)

      + auth_settings {
          + additional_login_params        = (known after apply)
          + allowed_external_redirect_urls = (known after apply)
          + default_provider               = (known after apply)
          + enabled                        = (known after apply)
          + issuer                         = (known after apply)
          + runtime_version                = (known after apply)
          + token_refresh_extension_hours  = (known after apply)
          + token_store_enabled            = (known after apply)
          + unauthenticated_client_action  = (known after apply)

          + active_directory {
              + allowed_audiences = (known after apply)
              + client_id         = (known after apply)
              + client_secret     = (sensitive value)
            }          
....

Plan: 7 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

This plan was saved to: .\MyPlan

To perform exactly these actions, run the following command to apply:
    terraform apply ".\\MyPlan"
  1. Nun wird der Plan mit
terraform apply

zur Ausführung gebracht.

  1. Eine kurze Rechenphase und der Plan der ausgeführt werden wird, wird angezeigt. Die Bestätigung mit „Yes“ lässt Terraform nun seine Arbeit verrichten.
terraform apply
  1. Sobald die Bestätigung mit „YES“ erfolgte, schreitet Terraform zur Tat. Die Fortschritte sind dann wie folgt:
terraform progress indikator
  1. Wenn alles richtig gemacht worden ist, dann zeigt Terraform den Status der Operation an.
terraform status

Hier noch die erstellten Ressourcen im Azure Portal.

azure ressourcen nach der terraform applizierung

Löschen der erstellten Ressourcen

Der Aufbau einer Umgebung ist das Eine. Das Abbauen das Andere. In der Dotnet User Group Bern, sollt diese Umgebung für die BDD (Behavior Driven Tests) hochgefahren und dann wieder abgebaut werden. Der Abbau geht dann wie nachfolgend beschrieben, sehr schnell.

  1. Mit dem Befehl
terraform.exe destroy

werden die Ressourcen dann abgebaut.

2. Nach einer kurzen Verarbeitungszeit auf dem ausführenden Client sieht man dann Ausführungsplan des oben bereits dargestellten Ausführungsplanes. Der Unterschied hier, bei jeder Resource ist nun ein vorangestellt, dass angibt das die Ressource gelöscht werden wird. Auch hier muss mit YES die Aktion bestätigt werden.

3. Sind alle Aktionen erfolgreich verlaufen so teilt terraform mit, dass die Ressourcen zerstört sind.

terraform destory

Auch im Azure Portal sind die Ressourcen nicht mehr zu finden.

azure nach dem abbau durch terraform destroy

Fazit

Mit Terrform ist es relativ einfach eine automatisierte Infrastruktur zu instantiieren und so den Prozess der Bereitstellung zu Beschleunigen. Wenn Dir dieser Artiekl gefallen hat, dann würde ich mich über ein Like freuen und nehme gerne Verbesserungsvorschläge an. Meine Reise mit Terraform ist noch nicht zu Ende und das hier gezeigt, einfache Beispiel, kann noch weiter verbessert werden. Weiter will ich zudem die Möglichkeiten erkunden, diese Lösung auch onPremise einzusetzen, da nicht in jeder Umgebung die Cloud als definierte Zielplattform gewünscht ist.

Weiterführende Links

Desired State Configuration und Group Managed Service Accounts

Ausgangslage

In meinem Unternehmen setzen wir IBM Urban Code Deploy ein für die Provisionierung der Server. Nun da dies einen manuellen Mehraufwand bedeutet muss dieses Vorgehen effizienter gestaltet werden. Aus diesem Grund habe ich mit für die Konfiguration mittels Desired State Configuration (DSC) entschieden.

Ziel

Durchführung

Für die Durchführung orientieren wir uns an den gesteckten Zielen.

Ist JAVA instaliert?

Hat man JAVA installiert, so wird dies meistens in ein Verzeichnis wie JDK oder JAVA gemacht und im Pfad wird das entsprechende angegeben. Die Konfiguration wird nicht gestartet, sollte JAVA nicht installiert sein. Dies kann man auf einfache Art und Weise überprüfen.

if(($env:Path.Split(';') | Where-Object { Where-Object { $_ -like "*JDK*" -or $_ -like "*JAVA*" } ))

Nun gut, wenn alles korrekt verläuft dann kann die Konfiguration beginnen.

Installation des Agenten

Der Agent wird mittels XCOPY Installation auf dem System installiert. Das hat den Vorteil, dass der Agent bereits als Vorlage "gezipped" zur Verfügung steht und dann noch an die richtige Stelle kopiert wird. Um diesen Vorgang auszuführen braucht es die Resource Archive, die bereits standardmässig von Windows zur Verfügung steht. Die Resource sieht wie folgt aus:

            Archive UrbanCodeBinaries
            {
                Destination = "C:\Program Files"
                Ensure =  "Present"
                Path = $agentSource
                Force = $true                
            }

Die Destination ist das Ziel in welche die binären Dateien extrahiert werden. Der Path wird durch das Aufrufen des Konfigurationsscriptes mitgegeben.

Konfiguration des Dienstes für den Agenten

Für die Konfiguration des Agenten ist die Service Resource verwendet worden, die wie folgt konfiguriert wird.


            # Preconfigure the properties for the service
            $agentName = ("ucd-agent-{0}" -f $env:COMPUTERNAME)
            $servicePathArguments = "//RS//{0}" -f $agentName
            $servicePath = Join-Path -Path $env:ProgramFiles -ChildPath "ibm-ucd-agent\native\x64\winservice.exe"

            Service UrbanCodeAgentService
            {
                Ensure = "Present"
                Name = $agentName
                StartupType = "Manual"
                Path = ("`"{0}`" {1}" -f $servicePath, $servicePathArguments)
                Description =  "IBM Urban Code Agent"                
                DisplayName = $agentName  
                State = "Stopped"                             
            }

Vor der Ressource sind die notwendigen Parameter vorkonfiguriert worden, da dieser JAVA Agent über einen Windows-Dienst Wrappter mit Argumenten gestartet wird. Der Dienst ist nun konfiguriert nur noch nicht so wie gewünscht.

Konfiguration des Dienstes für den Agent

Die Dienst Resource lässt nur zu einen Benutzer-Account mit Passwort zu verknüpfen. Da kein Object PSCredential Objekt ohne Passwort erstellt werden kann und das Passwort für den Group Managed Service Account unbekannt ist, muss man sich mittels WMI den Dienst wie gewünscht konfigurieren. Dies geschieht mittels der Script Resource

            # Third Set the Service with a script Resource
            Script ServiceScript{
                DependsOn = "[Service]UrbanCodeAgentService"
                GetScript = {                    
                    return Get-WmiObject -Class win32_Service | Where-Object -Property Name -like ("*{0}*" -f $using:agentName)
                }
                TestScript = {                    
                    $service = [scriptblock]::Create($GetScript).Invoke()                    
                    if($service){
                        return ($service.StartName -eq $using:groupManagedServiceAccount)
                    }
                    return $false
                }
                SetScript = {                    
                    $service = [scriptblock]::Create($GetScript).Invoke()
                    if($service){
                        $service.Change($null, $null, $null, $null, $null, $null, $using:groupManagedServiceAccount, $null, $null, $null, $null)
                    }                    
                }
            }

Wichtig an dieser Stelle ist zu erwähnen, dass diese Script-Resource mit dem DependsOn versehen ist. Das bedeutet, dass diese Ressource erst durchgeführt wird, wenn die angegeben Ressource erfolgreich appliziert werden konnte.

Hier noch das ganze Script

param([Parameter(Mandatory=$true,HelpMessage="The full path to the template ucd agent zip")]
      [ValidateNotNullOrEmpty()]
      [string]$agentSource="C:\Temp\ibm-ucd-agent.zip",
      [Parameter(Mandatory=$true,HelpMessage="The Group Managed Account that is used as service account.")]
      [ValidateNotNullOrEmpty()]
      [string]$groupManagedServiceAccount="IFC1\srvgp-ucd-r$"
      )

Configuration UrbanCodeAgentConfiguration{

    Import-DscResource -ModuleName PSDesiredStateConfiguration 

    if(($env:Path.Split(';') | Where-Object { $_ -like "*JDK*" -or $_ -like "*JAVA*" } )){
        Node $env:COMPUTERNAME
        {            
            # First Extract the service binaries to the Destination
            Archive UrbanCodeBinaries
            {
                Destination = "C:\Program Files"
                Ensure =  "Present"
                Path = $agentSource
                Force = $true                
            }

            # Preconfigure the properties for the service
            $agentName = ("ucd-agent-{0}" -f $env:COMPUTERNAME)
            $servicePathArguments = "//RS//{0}" -f $agentName
            $servicePath = Join-Path -Path $env:ProgramFiles -ChildPath "ibm-ucd-agent\native\x64\winservice.exe"

            # Second configure the service
            Service UrbanCodeAgentService
            {
                Ensure = "Present"
                Name = $agentName
                StartupType = "Manual"
                Path = ("`"{0}`" {1}" -f $servicePath, $servicePathArguments)
                Description =  "IBM Urban Code Agent"                
                DisplayName = $agentName  
                State = "Stopped"                             
            }  
            
            # Third Set the Service with a script Resource
            Script ServiceScript{
                DependsOn = "[Service]UrbanCodeAgentService"
                GetScript = {                    
                    return Get-WmiObject -Class win32_Service | Where-Object -Property Name -like ("*{0}*" -f $using:agentName)
                }
                TestScript = {                    
                    $service = [scriptblock]::Create($GetScript).Invoke()                    
                    if($service){
                        return ($service.StartName -eq $using:groupManagedServiceAccount)
                    }
                    return $false
                }
                SetScript = {                    
                    $service = [scriptblock]::Create($GetScript).Invoke()
                    if($service){
                        $service.Change($null, $null, $null, $null, $null, $null, $using:groupManagedServiceAccount, $null, $null, $null, $null)
                    }                    
                }
            }
        }        
    }else {
        Write-Host "Java is not installed"
    }
}

UrbanCodeAgentConfiguration

Eine Bemerkung zum Node: Started man die Desired State Configuration, so ist eine MOF-Datei das Resultat. Will man diese archivieren, so würde diese standardmässig "localhost" heissen, was nicht hilfreich wäre. Aus diesem Grund verwende ich immer die Variable $env:COMPUTERNAME um so eine sprechende MOF-Datei zu erhalten.

Fazit

Nicht alles was bereits standardmässig zur Verüfung steht, kann gleich für jeden Anwendungsfall verwendet werden. Mit der Script-Ressource ist es möglich eine gewisse Flexibilität gegeben und man kann auch ohne dedizierte Scripts Aktionen ausführen, die von keiner Ressource bereitgestellt werden. Für Fragen und Anregungen bin ich offen und wenn Dir der Artikel gefallen hat, dann freue ich mich über ein like

Erstellen eines Windows Dienstes mit Desired State Configuration

In diesem Blog-Post soll gezeigt werden, wie man einfach einen Dienst mittels DSC (Desired State Configuration) installiert und wieder deinstalliert. Die meisten Beispiele zeigen nur kleine Schnipsel an Code, sodass kein Praxisnahes Beispiel herangezogen werden kann.

Ziel

  • Die Installation eines Windows Dienstes mittels DSC auf einem Windows Server 2019 Core

Durchführung

Um die Voraussetzungen zu erfüllen, um DSC verwenden zu können, kann hier nachgelesen werden.

Schritte zur Durchführung

  1. Die Quelldateien des Dienstes müssen von einem Quellverzeichnis in das Zielverzeichnis des Dienstes kopiert werden.
  2. Der Dienst muss dann mittels DSC installiert werden können.

Für diese Operation sind die zwei Windows Resourcen notwendig, die bereits fixfertig vom Betriebssystem geliefert werden.

Das komplette Script nachfolgend aufgeführt:

param([Parameter(Mandatory=$true)][string]$source,
      [Parameter(Mandatory=$true)][string]$destination,
      [Parameter(Mandatory=$true)][string]$servicename,
      [Parameter(Mandatory=$true)][string]$pathtoServiceExecutable)

Configuration ServiceDemo
{

    Import-DscResource -ModuleName PSDesiredStateConfiguration    

    Node $env:COMPUTERNAME
    {    
        File ServiceDirectoryCopy
        {
            Ensure = "Present"
            Type = "Directory"
            Recurse = $true
            SourcePath = $source
            DestinationPath = $destination
        }

        Service DemoService
        {
            Ensure = "Present"
             Name = $servicename
             StartupType = "Manual"
             State = "Stopped"
             Path = $pathtoServiceExecutable
        }
    }
}

Die Variable $env:COMPUTERNAME wird verwendet, damit die generierte MOF-Datei den Namen des Servers aufweist und die Konfiguration so zu anderen unterschieden werden kann.

Die ServiceDirectoryCopy Aktion führt das Kopieren in rekursiver Form der Dienst Dateien aus.

Die Operation DemoService konfiguriert und installiert den Dienst.

Nun muss mittels Aufruf der Scriptes und den Parametern die MOF-Datei erzeugt werden. In diesem Beispiel sieht der Aufruf wie folgt aus:

.\CreateService.ps1 -source "C:\_install\DemoService" -destination "C:\Program Files\DemoService" -servicename "DemoService" -pathtoServiceExecutable "C:\Program Files\DemoService\DemoService.exe"

Ist alles korrekt gelaufen, so sieht man das nachfolgende Resultat:

Erzeugte MOF-Datei
Die Erezugte MOF-Datei

Der Inhalt dieser Datei sieht dann so aus:

/*
@TargetNode='WIN-MFVO0VQ8PB7'
@GeneratedBy=Administrator
@GenerationDate=09/08/2020 12:07:47
@GenerationHost=WIN-MFVO0VQ8PB7
*/

instance of MSFT_FileDirectoryConfiguration as $MSFT_FileDirectoryConfiguration1ref
{
ResourceID = "[File]ServiceDirectoryCopy";
 Type = "Directory";
 Ensure = "Present";
 DestinationPath = "C:\\Program Files\\DemoService";
 ModuleName = "PSDesiredStateConfiguration";
 SourceInfo = "C:\\Users\\Administrator\\Documents\\DemoServiceConfiguration.ps1::14::9::File";
 Recurse = True;
 SourcePath = "C:\\_install\\DemoService\\";

ModuleVersion = "1.0";

 ConfigurationName = "ServiceDemo";

};
instance of MSFT_ServiceResource as $MSFT_ServiceResource1ref
{
ResourceID = "[Service]DemoService";
 State = "Stopped";
 SourceInfo = "C:\\Users\\Administrator\\Documents\\DemoServiceConfiguration.ps1::23::9::Service";
 Name = "DemoService";
 StartupType = "Manual";
 ModuleName = "PSDesiredStateConfiguration";
 Path = "C:\\Program Files\\DemoService\\notepad.exe";

ModuleVersion = "1.0";

 ConfigurationName = "ServiceDemo";

};
instance of OMI_ConfigurationDocument


                    {
 Version="2.0.0";
 

                        MinimumCompatibleVersion = "1.0.0"; 

                        CompatibleVersionAdditionalProperties= {"Omi_BaseResource:ConfigurationName"}; 

                        Author="Administrator"; 

                        GenerationDate="09/08/2020 12:07:47"; 

                        GenerationHost="WIN-MFVO0VQ8PB7";

                        Name="ServiceDemo";

                    };

Nun muss die Konfiguration appliziert werden. Dies geschieht mit dem folgenden Befehl:

Start-DscConfiguration .\ServiceDemo

Das Ausführen erzeugt einen Powershell Job der ausgeführt wird.

Ausführung der Konfiguration

Nun kann überprüft werden, ob die Konfiguration unseren Wünschen entspricht.

Get-Service *Demo*

liefert dann das Ergebnis, das erwartet wird.

Installierter Beispiel-Dienst

Anmerkung: Zum Demonstrationszweck dient bei mir notepad.exe als Demo-Dienst. Fragen wir die Dienst-Details mittels

Get-CimInstance -Class Win32_Service -Property * | Where-Object -Property Name -like "*Demo*"

so sehen wir alle konfigurierten Werte, dieses Dienstes und sehen, dass die Konfiguration angewendet worden ist.

Dienst Details

Fazit

Mit einfachen Mitteln lassen sich so ohne Komplexe Scripts gewünschte Zustände eines Systems festlegen und die Konfiguration ist einfacher zu lesen und zu interpretieren. Ein weiterer Vorteil liegt in der Idempotenz. Ein Nachteil dieser Methode ist, dass die Anmeldeinformationen nicht mitgegeben werden können und diese anschliessend konfiguriert werden müssen. Eine Alternative hierfür könnnte sein, dass man selber eine Resource schreibt und dies implementiert oder wie bereits erwähnt über Set-CimInstance, die entsprechende Eigenschaft des Dienst-Objektes setzt.

Falls der Artikel Gefallen gefunden hat, freue ich mich über einen Like und bin für Rückmeldungen offen.

Weiterführende Links