Automatisera molnresurser med Python och Pulumi: separera konfiguration från kod

Att separera konfigurationsdata till en YAML-fil som fungerar som indata till Pulumi-programmet. Safespring är en molnplattform byggd på OpenStack.

Jarle Bjørgeengen

Jarle Bjørgeengen

Former Chief Product Officer

Texten är automatiskt översatt för din bekvämlighet, du kan läsa texten på:

.

I infrastrukturkod (och annan kod också) är det god praxis att separera programlogiken från dess indata (konfiguration). På så sätt behöver vi, för att ändra tillståndet i vår infrastruktur, bara ändra indatan och inte programmet, såvida inte programmets logik förändras.

I det föregående blogginlägget gick vi igenom en grundläggande setup av Pulumi med Python-mallen för att hantera OpenStack-resurser i Safespring. Detta är en bra utgångspunkt för att förstå grunderna i hur man kan använda Python tillsammans med Pulumi för att deklarativt hantera infrastrukturresurser utan att behöva skriva all hantering av resursgrafen från grunden, vilket förstås också vore möjligt med Python eller något modernt programmeringsspråk för den delen.

Ett problem med det första exemplet är att konfigurationen (instansnamn, flavor, nätverk och så vidare) är inbäddad i Python-koden.

Även om det angreppssättet fungerar som ett trevligt, självbärande exempel blir det snabbt både felbenäget och omständligt att behöva ändra Python-programmet varje gång ett nytt objekt (en instans, till exempel ;-)) ska läggas till, ändras, konfigureras om eller tas bort.

Om konfigurationen lagrades utanför programmet och lästes in vid körning, till exempel i det mest spridda “mänskligt läsbara dataserialiseringsspråket” inom IT i dag: YAML, så vore det en förbättring jämfört med det ursprungliga angreppssättet, eller hur?

Förutsättningar

Läsa instanskonfigurationen från en YAML-fil

Betrakta följande Python-kod:

"""An OpenStack Python Pulumi program"""

import pulumi
from pulumi_openstack import compute
from pulumi_openstack import networking
from ruamel.yaml import YAML
import os.path

# Configure the behavior for the yaml module
yaml=YAML(typ='safe')
yaml.default_flow_style = False

# Load config data from YAML file representation
# into Python dictionary representation

config_data_file = "pulumi-config.yaml"
if os.path.isfile(config_data_file):
  fh = open(config_data_file, "r")
  config_dict = yaml.load(fh)
else:
  print(f'The file {config_data_file} does not exist!')
  exit(1)


instances = {}
for i in config_dict:
  instances[i['name']] = compute.Instance(i['name'],
        name = i['name'],
        flavor_name = i['flavor'],
        networks = [{"name": i['network']}],
        image_name = i["image"])

I det här exemplet har vi tagit samma minimala uppsättning parametrar som behövs för att definiera en instans som i exempel 1, men i stället för att ange parametrarna i koden läser vi dem från en ordbok, som i sin tur kommer från att deserialisera data från filen pulumi-config.yaml. Dessutom skapar vi en loop som itererar över en lista med instanser, med parametrar i varje listelement från YAML-filen.

Och pulumi-config.yaml-filen ser ut så här:

---
- name: pulumi-snipp
  flavor: l2.c2r4.100
  image: ubuntu-22.04
  network: default
- name: pulumi-snapp
  flavor: l2.c2r4.500
  image: ubuntu-22.04
  network: public

Så nu kan vi bara köra pulumi up och iterera över listan av instanser i YAML-filen för att få det önskade tillståndet att överensstämma med det faktiska tillståndet? Tja, först behöver vi faktiskt uppdatera den virtualenv som Pulumi-programmet använder för att kunna använda modulen ruamel.yaml. För att säkerställa att ändringen består när vi replikerar uppsättningen på andra ställen (till exempel i en pipeline) bör vi lägga till modulen ruamel.yaml i filen requirements.txt och sedan köra venv/bin/pip install -r requirements.txt för att uppdatera de installerade Python-biblioteken enligt kraven.

Nu kan vi tillämpa det önskade tillståndet genom att:

(oscli) ubuntu@demo-jumphost:~/pulumi$ pulumi up
Previewing update (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/JarleB/pulumi-demo/dev/previews/9709238f-9230-4029-8bd5-0c6d9a55664d

     Type                           Name             Plan
     pulumi:pulumi:Stack            pulumi-demo-dev
 +   ├─ openstack:compute:Instance  pulumi-snapp     create
 +   └─ openstack:compute:Instance  pulumi-snipp     create


Resources:
    + 2 to create
    1 unchanged

Do you want to perform this update? yes
Updating (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/JarleB/pulumi-demo/dev/updates/24

     Type                           Name             Status
     pulumi:pulumi:Stack            pulumi-demo-dev
 +   ├─ openstack:compute:Instance  pulumi-snapp     created (15s)
 +   └─ openstack:compute:Instance  pulumi-snipp     created (14s)


Resources:
    + 2 created
    1 unchanged

Duration: 17s

(oscli) ubuntu@demo-jumphost:~/pulumi$

Låt oss granska vad som skapades med hjälp av OpenStack CLI:

(oscli) ubuntu@demo-jumphost:~/pulumi$ openstack server list |grep pulu
| 48d1cb9f-d732-4684-82e8-aa89ca05c5b9 | pulumi-snapp                          | ACTIVE  | public=212.162.147.53, 2a09:d400:0:1::2b1  | ubuntu-22.04             | l2.c2r4.500  |
| 5870d687-5aac-40b8-8f23-e54755e0fc62 | pulumi-snipp                          | ACTIVE  | default=10.68.3.95, 2a09:d400:0:2::82      | ubuntu-22.04             | l2.c2r4.100  |
(oscli) ubuntu@demo-jumphost:~/pulumi$

Det verkar som att Pulumi höll sitt löfte.

Lägga till säkerhetsgrupper för åtkomst

Det är inte särskilt roligt att provisionera (och betala för) instanser som inte går att nå, så låt oss utöka uppsättningen genom att lägga till några säkerhetsgrupper och regler så att tjänsterna på instanserna blir åtkomliga.

Därför gör vi ändringar i Pulumi-programmet så att det accepterar konfiguration av säkerhetsgrupper och regler från YAML-konfigurationsfilen och lägger till listan över säkerhetsgruppsmedlemskap som parametrar till instanserna.

Den nya Python-koden speglar också en annan struktur i YAML-konfigurationsfilen; vi har flyttat listan över instanser under ett nytt delträd som heter instances och, föga förvånande, placerat säkerhetsgrupperna under delträdet security_groups med regler för varje säkerhetsgrupp som ”bladnoder” under respektive säkerhetsgrupp.

Så här:

---
security_groups:
  ssh-from-the-world:
    ssh:
      direction: ingress
      ethertype: IPv4
      protocol: tcp
      port_range_min: 22
      port_range_max: 22
      remote_ip_prefix: 0.0.0.0/0
  web:
    https:
      direction: ingress
      ethertype: IPv4
      protocol: tcp
      port_range_min: 443
      port_range_max: 443
      remote_ip_prefix: 0.0.0.0/0
    http:
      direction: ingress
      ethertype: IPv4
      protocol: tcp
      port_range_min: 80
      port_range_max: 80
      remote_ip_prefix: 0.0.0.0/0

instances:
  - name: pulumi-snipp
    flavor: l2.c2r4.100
    image: ubuntu-22.04
    network: default
    security_groups:
      - ssh-from-the-world
  - name: pulumi-snapp
    flavor: l2.c2r4.500
    image: ubuntu-22.04
    network: public
    security_groups:
      - ssh-from-the-world

Och sedan det uppdaterade Pulumi-programmet som kommer att implementera den logiska strukturen i YAML-filen:

"""An OpenStack Python Pulumi program"""

import pulumi
from pulumi_openstack import compute
from pulumi_openstack import networking
from ruamel.yaml import YAML
import os.path

# Configure the behavior for the yaml module
yaml=YAML(typ='safe')
yaml.default_flow_style = False

# Load config data from YAML file representation
# into Python dictionary representation

config_data_file = "pulumi-config.yaml"
if os.path.isfile(config_data_file):
  fh = open(config_data_file, "r")
  config_dict = yaml.load(fh)
else:
  print(f'The file {config_data_file} does not exist!')
  exit(1)


security_groups = {}
for sg in config_dict['security_groups']:
  security_groups[sg] = networking.SecGroup(sg,
        name = sg)
  for sgr in config_dict['security_groups'][sg]:
    rule = {}
    rule = config_dict['security_groups'][sg][sgr]
    security_groups[sgr] =  networking.SecGroupRule(sgr,
      direction = rule['direction'],
      ethertype = rule['ethertype'],
      protocol = rule['protocol'],
      port_range_min = rule['port_range_min'],
      port_range_max = rule['port_range_max'],
      security_group_id = security_groups[sg].id)



instances = {}
for i in config_dict['instances']:
  instances[i['name']] = compute.Instance(i['name'],
    name = i['name'],
	flavor_name = i['flavor'],
	networks = [{"name": i['network']}],
    security_groups = i['security_groups'],
	image_name = i["image"])

Låt oss köra Pulumi-programmet och se hur det önskade tillståndet för vår IaaS ändras enligt YAML-konfigurationsfilens struktur:

(oscli) ubuntu@demo-jumphost:~/pulumi$ pulumi up
Previewing update (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/JarleB/pulumi-demo/dev/previews/ce560731-1889-42bb-821d-9003e1acfc1e

     Type                                  Name                Plan       Info
     pulumi:pulumi:Stack                   pulumi-demo-dev
 +   ├─ openstack:networking:SecGroup      web                 create
 +   ├─ openstack:networking:SecGroup      ssh-from-the-world  create
 ~   ├─ openstack:compute:Instance         pulumi-snipp        update     [diff: ~securityGroups]
 ~   ├─ openstack:compute:Instance         pulumi-snapp        update     [diff: ~securityGroups]
 +   ├─ openstack:networking:SecGroupRule  https               create
 +   ├─ openstack:networking:SecGroupRule  http                create
 +   └─ openstack:networking:SecGroupRule  ssh                 create


Resources:
    + 5 to create
    ~ 2 to update
    7 changes. 1 unchanged

Do you want to perform this update? yes
Updating (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/JarleB/pulumi-demo/dev/updates/29

     Type                                  Name                Status                  Info
     pulumi:pulumi:Stack                   pulumi-demo-dev     **failed**              1 error
 +   ├─ openstack:networking:SecGroup      web                 created (1s)
 +   ├─ openstack:networking:SecGroup      ssh-from-the-world  created (1s)
 ~   ├─ openstack:compute:Instance         pulumi-snipp        **updating failed**     [diff: ~securityGroups]; 1 error
 ~   ├─ openstack:compute:Instance         pulumi-snapp        updated (5s)            [diff: ~securityGroups]
 +   ├─ openstack:networking:SecGroupRule  https               created (0.88s)
 +   ├─ openstack:networking:SecGroupRule  http                created (1s)
 +   └─ openstack:networking:SecGroupRule  ssh                 created (1s)


Diagnostics:
  openstack:compute:Instance (pulumi-snipp):
    error: 1 error occurred:
    	* updating urn:pulumi:dev::pulumi-demo::openstack:compute/instance:Instance::pulumi-snipp: 1 error occurred:
    	* Gateway Timeout

  pulumi:pulumi:Stack (pulumi-demo-dev):
    error: update failed

Resources:
    + 5 created
    ~ 1 updated
    6 changes. 1 unchanged

Duration: 1m5s

(oscli) ubuntu@demo-jumphost:~/pulumi$

När vi tillämpar tillståndet ser vi att en av de planerade åtgärderna misslyckades på grund av en API-timeout mot OpenStack-API:t. Detta händer ibland, och när det gör det är det bra att ha ett verktyg som håller reda på det aktuella tillståndet och vad som har gjorts även om vissa åtgärder misslyckades. I det här avseendet beter sig Pulumi likadant som Terraform och kommer att ta hand om de återstående ändringarna vid nästa tillämpning av tillståndet. Så, låt oss köra Pulumi-programmet igen och se vad som händer:

(oscli) ubuntu@demo-jumphost:~/pulumi$ pulumi up
Previewing update (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/JarleB/pulumi-demo/dev/previews/e85ac2cd-53d0-40d1-8f74-8ea1dba35be8

     Type                           Name             Plan       Info
     pulumi:pulumi:Stack            pulumi-demo-dev
 ~   └─ openstack:compute:Instance  pulumi-snipp     update     [diff: +securityGroups]


Resources:
    ~ 1 to update
    7 unchanged

Do you want to perform this update? yes
Updating (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/JarleB/pulumi-demo/dev/updates/30

     Type                           Name             Status           Info
     pulumi:pulumi:Stack            pulumi-demo-dev
 ~   └─ openstack:compute:Instance  pulumi-snipp     updated (1s)     [diff: +securityGroups]


Resources:
    ~ 1 updated
    7 unchanged

Duration: 4s

(oscli) ubuntu@demo-jumphost:~/pulumi$

Och precis som väntat fanns det bara en uppdatering kvar, och den konvergerade snabbt till det önskade tillståndet som beskrivs i YAML-konfigurationsfilen. Nu, bör det önskade tillståndet motsvara det faktiska tillståndet.

Låt oss kontrollera för att bekräfta.

(oscli) ubuntu@demo-jumphost:~/pulumi$ openstack security group list |grep pul
| 33765832-f1a8-4afa-a542-c087994fd1a3 | pulumi-ssh             |                        | 74cf3e20e55345d29935625c7b3e5618 | []   |
| 58bc1279-3548-41cb-b918-15430cc983f1 | pulumi-web             |                        | 74cf3e20e55345d29935625c7b3e5618 | []   |
(oscli) ubuntu@demo-jumphost:~/pulumi$

(oscli) ubuntu@demo-jumphost:~/pulumi$ openstack server show -c instance_name -c addresses -c security_groups pulumi-snapp
+-----------------+--------------------------------------------+
| Field           | Value                                      |
+-----------------+--------------------------------------------+
| addresses       | public=212.162.147.166, 2a09:d400:0:1::140 |
| instance_name   | None                                       |
| security_groups | name='pulumi-ssh'                          |
|                 | name='pulumi-web'                          |
+-----------------+--------------------------------------------+
(oscli) ubuntu@demo-jumphost:~/pulumi$ openstack server show -c instance_name -c addresses -c security_groups pulumi-snipp
+-----------------+-----------------------------------------+
| Field           | Value                                   |
+-----------------+-----------------------------------------+
| addresses       | default=10.68.1.105, 2a09:d400:0:2::26a |
| instance_name   | None                                    |
| security_groups | name='pulumi-ssh'                       |
+-----------------+-----------------------------------------+
(oscli) ubuntu@demo-jumphost:~/pulumi$ nc -w 1 212.162.147.166 22
SSH-2.0-OpenSSH_8.9p1 Ubuntu-3
(oscli) ubuntu@demo-jumphost:~/pulumi$ nc -w 1 10.68.1.105 22
SSH-2.0-OpenSSH_8.9p1 Ubuntu-3
(oscli) ubuntu@demo-jumphost:~/pulumi$

Det verkar som att Pulumi höll sina löften igen. Observera att vi omedelbart kan nå RFC1918-adressen till instansen på nätverket default. Om du undrar varför detta “bara fungerar” kan du läsa blogginlägget om Safesprings nätverksmodell.

Slutsats

Med utgångspunkt i vårt första och mycket grundläggande Pulumi-exempel, har vi fortsatt att visa värdet av att kombinera Python-biblioteket ruamel.yaml med ett Pythondrivet Pulumi-program för att snabbt generalisera Python-kod genom att separera kod och konfigurationsdata.