Software Development

Infrastruktura jako kod z wykorzystaniem terraform

Kwiecień 27, 2021 0
Podziel się:

Całkiem niedawno na naszym blogu pojawił się artykuł dot. koncepcji infrastruktura jako kod” (infrastructure as code, IaC). Znając już zalety takiego podejścia do zarządzania infrastrukturą, chciałbym trochę ten temat rozwinąć i przedstawić jedno z narzędzi, które możemy w tym celu wykorzystać — terraform.

Zanim jednak przejdę do przedstawiania terraform, chciałbym wspomnieć o różnych kategoriach narzędzi IaC:

  • narzędzia do zarządzania konfiguracją (configuration management)
  • narzędzia instrumentacji (orchestration)
  • narzędzia do tworzenia zasobów/infrastruktury chmurowej (provisioning)

Zarządzanie konfiguracją (configuration management)

Do najpopularniejszych narzędzi zarządzania konfiguracją należą Ansible, Puppet, Chef, SaltStack. Są to narzędzia, które najczęściej służą do instalowania oprogramowania, zarządzania usługami i ogólnej konfiguracji systemu. Cechą charakterystyczną narzędzi do zarządzania konfiguracją (jednocześnie ich wielką zaletą) jest powtarzalność, tzn. to, że kod raz stworzony możemy uruchamiać wielokrotnie i za każdym razem otrzymamy ten sam rezultat (tę samą zadeklarowaną konfigurację). Jeżeli chcielibyśmy napisać skrypt w bashu, który tworzyłby nam nowego użytkownika, użylibyśmy:

#!/bin/bash
useradd newuser

Przy pierwszym uruchomieniu zostanie stworzony użytkownik ”newuser”. Przy kolejnym uruchomieniu skryptu zostanie zwrócony błąd:

useradd: user 'newuser' already exists

Aby naprawić nasz skrypt, musielibyśmy dodać logikę, która sprawdzałaby, czy użytkownik już istnieje, czy nie. Narzędzia do zarządzania konfiguracją mają tę przewagę, że nie oczekują od nas podawania kroków, które mają być wykonane (dla przykładu: sprawdź czy folder istnieje, stwórz folder, sprawdź, czy użytkownik istnieje, stwórz użytkownika), a oczekują od nas jedynie konfiguracji, którą chcemy osiągnąc. Poniżej podaję przykładowy kod ansible na stworzenie użytkownika:

  - name: Dodawanie nowego użytkownika "newuser"
  user:
    name: newuser
    shell: /bin/bash
    groups: admins,developers
    append: yes

Przy pierwszym uruchomieniu Ansible zorientuje się, że taki użytkownik nie istnieje i go stworzy. Przy kolejnym uruchomieniu Ansible sprawdzi, że taki użytkownik już został stworzony i nie ma potrzeby na wykonanie kroku dodania nowego użytkownika. To jest właśnie przewaga narzędzi configuration management, które nie oczekują od nas dokładnego podawania kroków, które mają nas zaprowadzić do celu. Wystarczy, że zdefiniujemy nasz cel, a narzędzia te postarają się ten cel osiągnąć.

Jeśli chodzi o różnice pomiędzy różnymi narzędziami, to poza formą deklarowania zmian w konfiguracji (Ansible — YAML; Puppet — własny język), różnią się też tym, czy ich działanie opiera się na pracy z agentami instalowanymi na docelowej maszynie. Wygodny pod tym względem wydaje się Ansible, który nie wymaga od nas instalowania żadnego agenta. Zaletami podejścia wykorzystującego agenta jest to, że taki agent jest w stanie nieustannie kontrolować stan naszego serwera i go poprawiać w przypadku ręcznych modyfikacji, które odbiegają od tych zadeklarowanych. Jeżeli agent monitoruje nasz serwer, a jeden z adminów ręcznie wyłączył usługę, która wcześniej została zadeklarowana jako włączona, to taki agent automatycznie tę usługę włączy.

Instrumentacja (orchestration)

Do tej grupy możemy zaliczyć narzędzia, które wspierają pracę z kontenerami, takie jak Kubernetes, Amazon ECS/Fargate, Docker Swarm. Do zadań tych narzędzi należy m.in.:

  • nadzorowanie aktualnego stanu naszej aplikacji, a w przypadku wykrycia zmian, doprowadzenia stanu, który zdefiniowaliśmy i którego oczekujemy (desired state)
  • monitorowanie prawidłowego działania naszych kontenerów i zastępowanie ich w przypadku wykrycia nieprawidłowości
  • zapewnianie komunikacji pomiędzy naszymi kontenerami / maszynami.

Tworzenie zasobów (provisioning)

Zapewne każdy na początku swojej przygody z którąkolwiek chmurą, zaczynał od tworzenia zasobów, wykorzystując do tego interfejs webowy — dobry na początek, przejrzysty, łatwy. Z czasem pojawia się jednak konieczność zautomatyzowania wielu czynności i ułatwienia sobie pracy. Czas na kolejny krok, którym jest wykorzystanie gotowych narzędzi CLI. Szybko dochodzimy jednak do wniosku, że takie zarządzanie infrastrukturą za pomocą, czy to AWS CLI czy Azure CLI, zaczyna przypomninać zarządzanie konfiguracją serwera za pomocą basha. Nadchodzi czas na wykorzystanie dedykowanego narzędzia, które zostało stworzone właśnie w tym celu. W przypadku pracy z AWS mamy do dyspozycji CloudFormation, w przypadku Azure bedą to ARM Templates. Jeśli jednak chcielibyśmy, aby nasz zainswestowany czas nie ograniczał nas tylko do jednego dostawcy, powinniśmy zdecydować się na terraform.

Terraform

Terraform pobieramy ze strony terraform.io. Dzięki temu, że jest napisany w języku Go, mamy do czynienia z pojedynczym plikiem binarnym, który umieszczamy w dowolnym miejscu w naszej ścieżce systemowej PATH. (Wskazówka: jeśli pracujemy z chmurą Azure, możemy korzystać z terraform wykorzystując do tego ”cloud shell”, czyli terminal uruchamiany w przeglądarce dla naszego konta).
Konfigurację naszej infrastruktury definiujemy w języku HCL, Hashicorp Configuration Language, w plikach z rozszerzeniem *.tf, które w wielkim uproszczeniu mają następującą składnię:

  resource "rodzaj_zasobu" "nazwa_dla_naszego_zasobu_w_terraform" {
    name     = "nazwa_zasobu_w_chmurze"
    location = "westeurope" # atrybut
  }

Przykład dla tworzenia “Grupy zasobów” (Resource group) w chmurze Azure:

  resource "azurerm_resource_group" "moja_grupa" {
    name     = "ResourceGroup"
    location = "West Europe"
  }

Przykład dla wirtualnej maszyny w AWS:

  resource "aws_instance" "vm1" {
    ami           = "ami-830c94e3"
    instance_type = "t2.micro"
  }

Jedną z wielu zalet terraform jest fakt, że za jego pomocą możemy tworzyć zasoby po stronie różnych dostawców usług chmurowych i to nie tylko tych największych jak AWS, Azure, GCP, ale również tych mniejszych (np. DigitalOcean). Terraform komunikuje się bezpośrednio z API danego dostawcy usług, tworząc zasoby, które zdefiniowaliśmy w plikach *.tf. Aby terraform wiedział czy ma doczynienia z AWS, Azure czy GCP, definiujemy dostawcę (provider) dla naszego kodu:

Przykład dla Azure:

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

  provider "azurerm" {
    subscription_id = "id_naszej_subskrypcji"
    features {}
  }

i dla AWS:

  terraform {
    required_providers {
      aws = {
        source  = "hashicorp/aws"
        version = "~> 3.0"
      }
    }
  }

  provider "aws" {
    region = "region"
    features {}
  }

Jak można zauważyć, w żadnym miejscu dotychczas nie podaliśmy informacji uwierzytelniających.
Jeżeli pracujemy z chmurą Azure, mamy lokalnie zainstalowany “azure cli” i chcemy z naszej lokalnej maszyny wykonać kod terraform, to nie potrzebujemy podawać żadnych dodatkowych informacji. Terraform automatycznie wykorzysta azure cli, żeby się uwierzytelnić. W przypadku serwerów CI (lub braku azure cli) możemy stworzyć w chmurze “Jednostkę Usługi” (ang. Service Principal) i ustawić na danym hoście zmienne środowiskowe:

ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
ARM_CLIENT_SECRET="00000000-0000-0000-0000-000000000000"
ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
ARM_TENANT_ID="00000000-0000-0000-0000-000000000000"

W przypadku AWS, jednym ze sposobów uwierzytelnienia będzie zdefiniowanie zmiennych środowiskowych:

AWS_ACCESS_KEY_ID="xxx"
AWS_SECRET_ACCESS_KEY="zzz"

Terraform – przykład

Mając już skonfigurowanego terraforma, przedstawię poniżej przykład dla Azure’a który tworzy:

  • Grupę Zasobów
  • App Service Plan
  • App Service
  // Nasza konfiguracja dla Azure
  terraform {
    required_providers {
      azurerm = {
        source  = "hashicorp/azurerm"
        version = "~> 2.39"
      }
    }
  }

  provider "azurerm" {
    subscription_id = "id_naszej_subskrypcji"
    features {}
  }

  // Definiujemy nowy zasób typu azurerm_resource_group (Grupa Zasobów),
  // przypisujemy mu nazwę "example", której będziemy mogli użyć aby odwołać się do tego zasobu
  resource "azurerm_resource_group" "example" {
    name     = "example-resources" // Nazwa pod jaką dany zasób zostanie stworzony w chmurze
    location = "West Europe"       // Region w którym dany zasób zostanie stworzony
  }

  // Definiujemy kolejny zasób typu azurerm_app_service_plan (App Service Plan)
  resource "azurerm_app_service_plan" "example" {
    name                = "example-appserviceplan"
    location            = azurerm_resource_group.example.location
    // Jako że każdy zasób musi być umieszczony w grupie zasobów,
    // a w naszym przypadku grupa zasobów jest tworzona wyżej w kodzie,
    // odwołujemy się do nazwy tej nowo tworzonej grupy i "pobieramy" wartość atrybutu "name"
    resource_group_name = azurerm_resource_group.example.name

    // Definiujemy parametry dla App Service Planu
    sku {
      tier = "Standard"
      size = "S1"
    }
  }

  // Definiujemy nowy zasób typu azurerm_app_service (App Service)
  resource "azurerm_app_service" "example" {
    name                = "example-app-service"
    // Poniżej odwołujemy się do grupy zasobów, pobieramy wartość dla regionu i ustawiamy
    // taką samą wartość dla naszego app service'u.
    location            = azurerm_resource_group.example.location
    resource_group_name = azurerm_resource_group.example.name
    // Definiujemy w jakim planie powinien zostać stworzony nasz app service
    app_service_plan_id = azurerm_app_service_plan.example.id

    // Definiujemy dodatkową konfigurację
    site_config {
      dotnet_framework_version = "v4.0"
    }

    app_settings = {
      "SOME_KEY" = "some-value"
    }

    connection_string {
      name  = "Database"
      type  = "SQLServer"
      value = "Server=some-server.mydomain.com;Integrated Security=SSPI"
    }
  }

Mając w jednym katalogu plik *.tf z powyższą treścią, wykonujemy kolejno:

  • terraform init (zainicjalozowanie terraforma; pobranie odpowiednich wtyczek pod odpowiedniego dostawcę — w naszym przypadku azure)
  • terraform plan (porównanie naszej zdefiniowanej konfiguracji w plikach *.tf ze stanem faktycznym zasobów w chmurze i przedstawienie zmian):
  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.example will be created
    + resource "azurerm_app_service" "example" {
        + app_service_plan_id               = (known after apply)
        + app_settings                      = {
            + "SOME_KEY" = "some-value"
          }
  ------------
  wycięte informacje na temat zasobów i zmian
  ------------

  Plan: 3 to add, 0 to change, 0 to destroy.
  • terraform apply (wprowadzenie zmian, które zostały nam przedstawione w wyniku komendy terraform plan).

Po wykonaniu ostatniej komendy powinniśmy widzieć nasze nowo zdefiniowane zasoby w chmurze.
Jeśli chcemy pozbyć się zasobów zarządzanych przez terraform, możemy użyć polecenia terraform destroy.

Terraform – co dalej

Niestety niemożliwe jest kompletne przedstawienie narzędzia terraform w jednym wpisie. Nie został poruszony temat plików stanu (*.tfstate), bezpieczeństwa konfiguracji i integracji terraform z systemami CI/CD. Jeśli na co dzień jesteś odpowiedzialny za zarządzanie zasobami w chmurze lub po prostu zaciekawiło cię to narzędzie, to zachęcam do udania się na oficjalną stronę terraform.io po więcej informacji i przykładów.

5 / 5

Imię i nazwisko (wymagane)

Adres email (wymagane)

Temat

Treść wiadomości

Zostaw komentarz