Automatisering af cloudressourcer med Python og Pulumi: Adskillelse af konfiguration fra kode

Adskil konfigurationsdata i en YAML-fil, der fungerer som input til Pulumi-programmet. Safespring er en cloudplatform bygget på OpenStack.

Jarle Bjørgeengen

Jarle Bjørgeengen

Former Chief Product Officer

Denne tekst er automatisk oversat for din bekvemmelighed. Du kan læse teksten på:

.

I infrastrukturkode (og også i anden kode) er det god praksis at adskille programlogik fra dens inputdata (konfiguration). På den måde, for at ændre tilstanden af vores infrastruktur, behøver vi kun at ændre inputdataene og ikke selve programmet, medmindre programmets logik ændrer sig.

I det forrige blogindlæg gennemgik vi en grundlæggende opsætning af Pulumi med Python-skabelonen til at bruge det til at administrere OpenStack-ressourcer i Safespring. Det er et godt udgangspunkt for at forstå det grundlæggende i, hvordan man kan bruge Python sammen med Pulumi til deklarativt at administrere infrastrukturressourcer uden at skulle skrive al ressourcegrafhåndtering helt fra bunden, hvilket naturligvis også ville være muligt med Python eller et hvilket som helst moderne programmeringssprog for den sags skyld.

Et problem med det første eksempel er, at konfigurationen (instansnavn, flavor, netværk osv.) er indlejret i selve Python-koden.

Selvom den tilgang fungerer som et fint, selvstændigt eksempel, bliver det hurtigt både fejlbehæftet og besværligt at skulle ændre Python-programmet hver gang et nyt objekt (en instans for eksempel ;-)) skal tilføjes, ændres, rekonfigureres eller fjernes.

Hvis konfigurationen blev gemt uden for programmet og indlæst ved kørsel, for eksempel i det mest udbredte “menneskevenlige dataserialiseringssprog” i IT i dag: YAML, så ville det være en forbedring i forhold til den oprindelige tilgang, ikke sandt?

Forudsætninger

Læsning af instanskonfiguration fra en YAML-fil

Overvej følgende Python-kode:

"""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 dette eksempel har vi taget det samme minimale sæt parametre, der kræves for at definere en instans som i eksempel 1, men i stedet for at angive parametrene i koden læser vi dem fra en ordbog, som igen kommer fra deserialisering af data fra filen pulumi-config.yaml. Derudover opretter vi en løkke, der itererer over en liste af instanser, med parametre i hvert listeelement fra yaml-filen.

Og filen pulumi-config.yaml ser sådan ud:

---
- 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 bare køre pulumi up og iterere over listen af instanser i YAML-filen for at bringe den ønskede tilstand i overensstemmelse med den faktiske tilstand? Nå, først skal vi faktisk opdatere den virtualenv, som Pulumi-programmet bruger, for at kunne bruge modulet ruamel.yaml. For at sikre, at ændringen persisterer, når opsætningen replikeres andre steder (for eksempel i en pipeline), bør vi tilføje modulet ruamel.yaml til filen requirements.txt og derefter køre venv/bin/pip install -r requirements.txt for at opdatere de installerede Python-biblioteker i henhold til kravene.

Nu kan vi anvende den ønskede tilstand ved:

(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$

Lad os gennemgå, hvad der blev oprettet ved hjælp af 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 ser ud til, at Pulumi holdt sit løfte.

Tilføjelse af sikkerhedsgrupper for adgang

Det er ikke særlig sjovt at klargøre (og betale for) instanser, der ikke kan nås, så lad os udvide opsætningen med nogle sikkerhedsgrupper og regler, så tjenesterne på instanserne kan nås.

Derfor laver vi ændringer i Pulumi-programmet, så det accepterer konfigurationen af sikkerhedsgrupper og regler fra konfigurations-YAML-filen og tilføjer listen over medlemskaber i sikkerhedsgrupper som parametre til instanserne.

Den nye Python-kode afspejler også en anden struktur i YAML- konfigurationsfilen; vi flyttede listen over instanser ned under et nyt undertræ kaldet instances, og, ikke overraskende, placerede sikkerhedsgrupperne under security_groups-undertræet med regler for hver sikkerhedsgruppe som “bladnoder” under hver sikkerhedsgruppe.

Sådan her:

---
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

Og derefter det opdaterede Pulumi-program, der implementerer den logiske struktur 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"])

Lad os køre Pulumi-programmet og se, hvordan den ønskede tilstand for vores IaaS ændrer sig i henhold til 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$

Under anvendelsen af tilstanden ser vi, at en af de planlagte handlinger fejlede på grund af en API-timeout på OpenStack-API’et. Det sker indimellem, og når det gør, er det rart at have et værktøj, der holder styr på den aktuelle tilstand og hvad der blev gjort, selv hvis nogle handlinger fejlede. I den henseende opfører Pulumi sig på samme måde som Terraform og vil opsamle de resterende ændringer ved næste anvendelse af tilstanden. Så lad os køre Pulumi-programmet igen og se, hvad der sker:

(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$

Og præcis som forventet var der kun én opdatering tilbage, og den blev hurtigt bragt i overensstemmelse med den ønskede tilstand beskrevet i YAML-konfigurationsfilen. Nu burde den ønskede tilstand være lig med den faktiske tilstand.

Lad os kontrollere det.

(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 ser ud til, at Pulumi endnu en gang har holdt, hvad det lovede. Bemærk, at vi med det samme kan nå instansens RFC1918-adresse på default-netværket. Hvis du undrer dig over, hvorfor dette ‘bare virker’, så læs blogindlægget om Safespring-netværksmodellen.

Konklusion

Med udgangspunkt i, hvor vi slap i vores første og meget grundlæggende Pulumi-eksempel, har vi fortsat med at vise værdien af at kombinere Python-biblioteket ruamel.yaml med et Python-drevet Pulumi-program for hurtigt at generalisere Python-kode ved at adskille kode og konfigurationsdata.