Automatisering av skyressurser med Python og Pulumi: skille konfigurasjon fra kode

Å skille konfigurasjonsdataene ut i en yaml-fil som fungerer som inndata til Pulumi-programmet. Safespring er en skyplattform 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 annen kode også) er det god praksis å skille programlogikken fra inndataene (konfigurasjonen). På den måten trenger vi bare å endre inndataene for å endre tilstanden til infrastrukturen vår, og ikke programmet, med mindre logikken i programmet endres.

I det forrige blogginnlegget gikk vi gjennom et grunnleggende oppsett av Pulumi med Python-malen for å bruke det til å administrere OpenStack-ressurser i Safespring. Dette er et godt utgangspunkt for å forstå det grunnleggende i hvordan man kan bruke Python sammen med Pulumi til å deklarativt administrere infrastrukturressurser uten å måtte skrive all håndtering av ressursgrafen fra bunnen av, noe som selvsagt også ville vært mulig med Python eller ethvert moderne programmeringsspråk, for den saks skyld.

Et problem med det første eksempelet er at konfigurasjonen (instansnavn, flavor, nettverk og så videre) er innebygd i Python-koden.

Selv om den tilnærmingen fungerer som et fint, selvstendig eksempel, blir det raskt både feilutsatt og tungvint å måtte endre Python-programmet hver gang et nytt objekt (en instans, for eksempel ;-)) skal legges til, endres, rekonfigureres eller fjernes.

Om konfigurasjonen ble lagret utenfor programmet og lastet inn ved kjøring, for eksempel i det mest utbredte “menneskevennlig dataserialiseringsspråk” i IT i dag: YAML, så ville det være en forbedring sammenlignet med den opprinnelige tilnærmingen, ikke sant?

Forutsetninger

Lese instanskonfigurasjonen fra en YAML-fil

Se på 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 eksempelet har vi tatt det samme minimale settet med parametere som trengs for å definere en instans som i eksempel 1, men i stedet for å spesifisere parameterne i koden, leser vi dem fra en ordbok, som igjen kommer fra deserialisering av data fra filen pulumi-config.yaml. I tillegg lager vi en løkke som itererer over en liste med instanser, der parameterne i hvert listeelement kommer fra yaml-filen.

Og filen pulumi-config.yaml ser slik ut:

---
- 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å nå kan vi bare kjøre pulumi up og iterere over listen av instanser i YAML-filen for å konvergere ønsket tilstand til faktisk tilstand? Vel, først må vi faktisk oppdatere virtualenv som Pulumi-programmet bruker for å kunne ta i bruk ruamel.yaml-modulen. For å sikre at endringen vedvarer når oppsettet replikeres andre steder (for eksempel i en pipeline), bør vi legge til ruamel.yaml-modulen i requirements.txt-filen og deretter kjøre venv/bin/pip install -r requirements.txt for å oppdatere de installerte Python-bibliotekene i henhold til kravene.

Nå kan vi anvende ønsket 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$

La oss undersøke hva som ble opprettet ved hjelp 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 ser ut til at Pulumi holdt løftet sitt.

Legge til sikkerhetsgrupper for tilgang

Det er ikke særlig gøy å provisjonere (og betale for) instanser som ikke kan nås, så la oss utvide oppsettet med noen sikkerhetsgrupper og regler slik at tjenestene på instansene blir tilgjengelige.

Derfor gjør vi endringer i Pulumi-programmet slik at det godtar konfigurasjonen av sikkerhetsgrupper og regler fra YAML-konfigurasjonsfilen og legger til listen over hvilke sikkerhetsgrupper instansene er medlem av som parametere til instansene.

Den nye Python-koden gjenspeiler også en annen struktur i YAML-konfigurasjonsfilen; vi har flyttet listen over instanser under et nytt undertre kalt instances, og, ikke overraskende, plassert sikkerhetsgruppene under undertreet security_groups, med regler for hver sikkerhetsgruppe som «bladnoder» under den aktuelle sikkerhetsgruppen.

Slik:

---
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 deretter det oppdaterte Pulumi-programmet som vil implementere den logiske 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"])

La oss kjøre Pulumi-programmet og se hvordan den ønskede tilstanden til IaaS-en vår endres i henhold til YAML-konfigurasjonsfilens 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$

Mens vi anvender tilstanden, ser vi at én av de planlagte handlingene feilet på grunn av en API-timeout mot OpenStack-API-et. Dette skjer av og til, og når det gjør det, er det fint å ha et verktøy som holder styr på gjeldende tilstand og hva som ble gjort, selv om noen handlinger feilet. I så måte oppfører Pulumi seg likt som Terraform og vil plukke opp de resterende endringene ved neste anvendelse av tilstanden. Så, la oss kjøre Pulumi-programmet på nytt og se hva som skjer:

(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 som forventet var det bare én oppdatering igjen, og den ble raskt brakt i samsvar med den ønskede tilstanden beskrevet i YAML-konfigurasjonsfilen. Nå bør ønsket tilstand være lik den faktiske tilstanden.

La oss sjekke for å bekrefte 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 ut til at Pulumi nok en gang har holdt det de lovet. Merk at vi umiddelbart kan nå RFC1918-adressen til instansen på default-nettverket. Hvis du lurer på hvorfor dette «bare fungerer», les blogginnlegget om Safespring-nettverksmodellen.

Konklusjon

Med utgangspunkt i der vi avsluttet vårt første og svært grunnleggende Pulumi-eksempel, har vi fortsatt å vise verdien av å kombinere Python-biblioteket ruamel.yaml med et Python-drevet Pulumi-program for raskt å generalisere Python-kode ved å skille mellom kode og konfigurasjonsdata.