Article
· Août 13 17m de lecture

VIP dans le cadre de GCP

Si vous exécutez IRIS dans une configuration miroir pour HA dans GCP, la question de la fourniture de Mirror VIP (adresse IP virtuelle) devient pertinente. L'adresse IP virtuel permet aux systèmes en aval d'interagir avec IRIS en utilisant une seule adresse IP. Même en cas de basculement, les systèmes en aval peuvent se reconnecter à la même adresse IP et continuer à fonctionner.

Le principal problème, lors du déploiement sur GCP, est qu'un VIP IRIS doit être essentiellement un administrateur de réseau, conformément aux docs.

Pour obtenir l'HA, les membres du miroir IRIS doivent être déployés dans différentes zones de disponibilité d'un sous-réseau (ce qui est possible dans GCP car les sous-réseaux couvrent toujours toute la région). L'une des solutions pourrait être les équilibreurs de charge, mais ils coûtent bien sûr plus cher et nécessitent d'être administrés.

Dans cet article, j'aimerais fournir un moyen de configurer un VIP miroir sans utiliser les équilibreurs de charge suggérés dans la plupart des autres architectures de référence GCP.

Architecture

GCP VIP

Nous avons un sous-réseau qui s'étend sur la région de disponibilité (je simplifie ici - bien sûr, vous aurez probablement des sous-réseaux publics, un arbitre dans une autre zone, et ainsi de suite, mais il s'agit d'un minimum absolu suffisant pour démontrer cette approche). La notation CIRD du sous-réseau est 10.0.0.0/24, ce qui signifie que les adresses IP 10.0.0.1 à 10.0.0.255 lui sont allouées. En tant que GCP réserve les deux premières et dernières adresses, nous pouvons utiliser 10.0.0.2 'à10.0.0.253'`.

Nous mettrons en œuvre des VIP publics et privés en même temps. Si vous voulez, vous pouvez implémenter uniquement le VIP privé.

Idée

Les machines virtuelles dans GCP ont des Interfaces réseau. Ces interfaces réseau ont des Plages IP Alias IP Ranges qui sont des adresses IP privées. Des adresses IP publiques peuvent être ajoutées en spécifiant la configuration d' accès Access Config
La configuration d'interfaces réseau est une combinaison d'IP publiques et/ou privées, et elle est acheminée automatiquement vers la Machine virtuelle associée à l'Interface réseau. Il n'est donc pas nécessaire de mettre à jour les routes. Lors d'un basculement de miroir, nous allons supprimer la configuration IP VIP de l'ancien primaire et la créer pour un nouveau primaire. Toutes les opérations nécessaires à cette fin prennent de 5 à 20 secondes pour une IP VIP privée uniquement, de 5 secondes à une minute pour une combinaison d'IP VIP publique/privée.

Mise en œuvre de VIP

  1. Allouez l'adresse IP à utiliser en tant que VIP public. Ignorez cette étape si vous souhaitez uniquement un VIP privé.
  2. Choisissez une valeur VIP privée. Je vais utiliser '10.0.0.250'`
  3. Provisionnez vos instances IRIS avec un compte de service

- compute.instances.get
- compute.addresses.use
- compute.addresses.useInternal
- compute.instances.updateNetworkInterface
- compute.subnetworks.use

Pour les VIP externes, vous aurez également besoin de:
- compute.instances.addAccessConfig
- compute.instances.deleteAccessConfig
- compute.networks.useExternalIp
- compute.subnetworks.useExternalIp
- compute.addresses.list

  1. Lorsqu'un membre miroir actuel devient primaire, nous utilisons un callback ZMIRROR pour supprimer une configuration IP VIP sur l'interface réseau d'un autre membre miroir et créer une configuration IP VIP pointant sur lui-même :

C'est tout!

ROUTINE ZMIRROR

NotifyBecomePrimary() PUBLIC {
    #include %occMessages
    set sc = ##class(%SYS.System).WriteToConsoleLog("Setting Alias IP instead of Mirror VIP"_$random(100))
    set sc = ##class(%SYS.Python).Import("set_alias_ip")
    quit sc
}

Et voici set_alias_ip.py qui doit être placé dans le répertoire mgr\python:

"""
Ce script ajoute l'Alias IP (https://cloud.google.com/vpc/docs/alias-ip) à l'interface réseau de la VM.

Vous pouvez allouer des plages d'Alias IP à partir de la plage de sous-réseau primaire, ou vous pouvez ajouter une plage secondaire au sous-réseau 
et allouer des plages d'alias IP à partir de la plage secondaire.
Pour simplifier, nous utilisons la plage de sous-réseau primaire.

En utilisant Google cli, gcloud, cette action pourrait être effectuée de la manière suivante:
$ gcloud compute instances network-interfaces update <instance_name> --zone=<subnet_zone> --aliases="10.0.0.250/32"

Notez que la commande de suppression des alias est similaire - fournissez simplement un `alias`vide:
$ gcloud compute instances network-interfaces update <instance_name> --zone=<subnet_zone> --aliases=""

Nous utilisons l'API de métadonnées de Google Compute Engine pour récupérer <instance_name> ainsi que <subnet_zone>.

Notez également https://cloud.google.com/vpc/docs/subnets#unusable-ip-addresses-in-every-subnet.

Google Cloud utilise les deux premières et les deux dernières adresses IPv4 de chaque plage d'adresses IPv4 primaires pour héberger le sous-réseau.
Google Cloud vous permet d'utiliser toutes les adresses des plages IPv4 secondaires, c'est-à-dire:
- 10.0.0.0 - Adresse réseau
- 10.0.0.1 - Adresse de la passerelle par défaut
- 10.0.0.254 - Avant-dernière adresse. Réservée pour une utilisation future potentielle
- 10.0.0.255 - Adresse de diffusion

Après avoir ajouté l'adresse de l'Alias IP, vous pouvez vérifier son existence à l'aide de l'utilitaire "ip" :
$ ip route ls table local type local dev eth0 scope host proto 66
local 10.0.0.250
"""

import subprocess
import requests
import re
import time
from google.cloud import compute_v1

ALIAS_IP = "10.0.0.250/32"
METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/"
METADATA_HEADERS = {"Metadata-Flavor": "Google"}
project_path = "project/project-id"
instance_path = "instance/name"
zone_path = "instance/zone"
network_interface = "nic0"
mirror_public_ip_name = "isc-mirror"
access_config_name = "isc-mirror"
mirror_instances = ["isc-primary-001", "isc-backup-001"]


def get_metadata(path: str) -> str:
    return requests.get(METADATA_URL + path, headers=METADATA_HEADERS).text


def get_zone() -> str:
    return get_metadata(zone_path).split('/')[3]


client = compute_v1.InstancesClient()
project = get_metadata(project_path)
availability_zone = get_zone()


def get_ip_address_by_name():
    ip_address = ""
    client = compute_v1.AddressesClient()
    request = compute_v1.ListAddressesRequest(
        project=project,
        region='-'.join(get_zone().split('-')[0:2]),
        filter="name=" + mirror_public_ip_name,
    )
    response = client.list(request=request)
    for item in response:
        ip_address = item.address
    return ip_address


def get_zone_by_instance_name(instance_name: str) -> str:
    request = compute_v1.AggregatedListInstancesRequest()
    request.project = project
    instance_zone = ""
    for zone, response in client.aggregated_list(request=request):
        if response.instances:
            if re.search(f"{availability_zone}*", zone):
                for instance in response.instances:
                    if instance.name == instance_name:
                        return zone.split('/')[1]
    return instance_zone


def update_network_interface(action: str, instance_name: str, zone: str) -> None:
    if action == "create":
        alias_ip_range = compute_v1.AliasIpRange(
            ip_cidr_range=ALIAS_IP,
        )
    nic = compute_v1.NetworkInterface(
        alias_ip_ranges=[] if action == "delete" else [alias_ip_range],
        fingerprint=client.get(
            instance=instance_name,
            project=project,
            zone=zone
        ).network_interfaces[0].fingerprint,
    )
    request = compute_v1.UpdateNetworkInterfaceInstanceRequest(
        project=project,
        zone=zone,
        instance=instance_name,
        network_interface_resource=nic,
        network_interface=network_interface,
    )
    response = client.update_network_interface(request=request)
    print(instance_name + ": " + str(response.status))


def get_remote_instance_name() -> str:
    local_instance = get_metadata(instance_path)
    mirror_instances.remove(local_instance)
    return ''.join(mirror_instances)


def delete_remote_access_config(remote_instance: str) -> None:
    request = compute_v1.DeleteAccessConfigInstanceRequest(
        access_config=access_config_name,
        instance=remote_instance,
        network_interface="nic0",
        project=project,
        zone=get_zone_by_instance_name(remote_instance),
    )
    response = client.delete_access_config(request=request)
    print(response)


def add_access_config(public_ip_address: str) -> None:
    access_config = compute_v1.AccessConfig(
        name = access_config_name,
        nat_i_p=public_ip_address,
    )
    request = compute_v1.AddAccessConfigInstanceRequest(
        access_config_resource=access_config,
        instance=get_metadata(instance_path),
        network_interface="nic0",
        project=project,
        zone=get_zone_by_instance_name(get_metadata(instance_path)),
    )
    response = client.add_access_config(request=request)
    print(response)


# Obtention du nom et de la zone de l'instance d'un autre membre du basculement
remote_instance = get_remote_instance_name()
print(f"Alias IP is going to be deleted at [{remote_instance}]")

# Supprimer l'Alias IP de l'interface réseau d'un membre de basculement distant
#
# TODO : Effectuer les étapes suivantes lorsqu'un problème https://github.com/googleapis/google-cloud-python/issues/11931 sera clôturé:
# - mettre à jour le paquet google-cloud-compute pip vers une version contenant un correctif (>1.15.0)
# - supprimer une ligne ci-dessous appelant gcloud avec un sous-processus subprocess.run()
# - décommenter la fonction update_network_interface()
subprocess.run([
    "gcloud",
    "compute",
    "instances",
    "network-interfaces",
    "update",
    remote_instance,
    "--zone=" + get_zone_by_instance_name(remote_instance),
    "--aliases="
])
# update_network_interface("delete",
#                          remote_instance,
#                          get_zone_by_instance_name(remote_instance)


# Ajouter un Alias IP à l'interface réseau d'un membre du basculement local
update_network_interface("create",
                         get_metadata(instance_path),
                         availability_zone)


# Gérer la commutation IP publique
public_ip_address = get_ip_address_by_name()
if public_ip_address:
    print(f"Public IP [{public_ip_address}] is going to be switched to [{get_metadata(instance_path)}]")
    delete_remote_access_config(remote_instance)
    time.sleep(10)
    add_access_config(public_ip_address)

Démo

Déployons maintenant cette architecture IRIS dans GCP en utilisant Terraform et Ansible. Si vous utilisez déjà IRIS dans GCP ou un autre outil, le script ZMIRROR est disponible ici.

Outils

Nous aurons besoin des outils suivants. Ansible étant réservé à Linux, je recommande vivement de l'exécuter sur Linux, même si j'ai confirmé qu'il fonctionnait également sur Windows dans WSL2.

gcloud:

$ gcloud version
Google Cloud SDK 459.0.0
...

terraform:

$ terraform version
Terraform v1.6.3

python:

$ python3 --version
Python 3.10.12

ansible:

$ ansible --version
ansible [core 2.12.5]
...

ansible-playbook:

$ ansible-playbook --version
ansible-playbook [core 2.12.5]
...

WSL2

Si vous utilisez WSL2 sous Windows, vous devrez redémarrer l'agent ssh en exécutant:

eval `ssh-agent -s`

Il arrive également que l'horloge du WSL ne soit pas synchronisée (lorsque Windows passe en mode veille/hibernation et vice-versa), vous devrez peut-être la synchroniser explicitement:

sudo hwclock -s

Serveurs sans affichage

Si vous utilisez un serveur sans affichage, utilisez gcloud auth login --no-browser pour vous authentifier auprès de GCP.

IaC

Nous nous appuyons sur Terraform et stockons son état dans un espace de stockage en nuage. Voir les détails ci-dessous sur la façon dont ce stockage est créé.

Définition des variables requises

$ export PROJECT_ID=<project_id>
$ export REGION=<region> # For instance, us-west1
$ export TF_VAR_project_id=${PROJECT_ID}
$ export TF_VAR_region=${REGION}
$ export ROLE_NAME=MyTerraformRole
$ export SA_NAME=isc-mirror

Remarque: Si vous souhaitez ajouter une VIP publique qui expose publiquement les ports IRIS Mirror (ce qui n'est pas recommandé), vous pouvez l'activer avec:

$ export TF_VAR_enable_mirror_public_ip=true

Préparation du registre des artefacts

Il est recommandé d'utiliser Google Artifact Registry au lieu de Container Registry. Créons donc d'abord le registre:

$ cd <root_repo_dir>/terraform
$ cat ${SA_NAME}.json | docker login -u _json_key --password-stdin https://${REGION}-docker.pkg.dev
$ gcloud artifacts repositories create --repository-format=docker --location=${REGION} intersystems

Préparation des images Docker

Supposons que les instances de VM n'aient pas accès au dépôt de conteneurs ISC. Mais vous y avez un accès personnel et vous ne voulez pas mettre vos informations d'identification sur les machines virtuelles.

Dans ce cas, vous pouvez extraire les images Docker IRIS du registre de conteneurs ISC et les pousser vers le registre de conteneurs Google auquel les machines virtuelles ont accès:

$ docker login containers.intersystems.com
$ <Put your credentials here>

$ export IRIS_VERSION=2023.2.0.221.0

$ cd docker-compose/iris
$ docker build -t ${REGION}-docker.pkg.dev/${PROJECT_ID}/intersystems/iris:${IRIS_VERSION} .

$ for IMAGE in webgateway arbiter; do \
    docker pull containers.intersystems.com/intersystems/${IMAGE}:${IRIS_VERSION} \
    && docker tag containers.intersystems.com/intersystems/${IMAGE}:${IRIS_VERSION} ${REGION}-docker.pkg.dev/${PROJECT_ID}/intersystems/${IMAGE}:${IRIS_VERSION} \
    && docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/intersystems/${IMAGE}:${IRIS_VERSION}; \
done

$ docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/intersystems/iris:${IRIS_VERSION}

Mise en place de la licence IRIS

Mettez le fichier de clé de licence IRIS, iris.key dans <root_repo_dir>/docker-compose/iris/iris.key. Notez qu'une licence doit supporter le Mirroring.

Création d'un rôle Terraform

Ce rôle sera utilisé par Terraform pour gérer les ressources GCP nécessaires:

$ cd <root_repo_dir>/terraform/
$ gcloud iam roles create ${ROLE_NAME} --project ${PROJECT_ID} --file=terraform-permissions.yaml

Remarque: utiliser update pour une utilisation ultérieure:

$ gcloud iam roles update ${ROLE_NAME} --project ${PROJECT_ID} --file=terraform-permissions.yaml

Création d' un compte de service avec le rôle Terraform

$ gcloud iam service-accounts create ${SA_NAME} \
    --description="Terraform Service Account for ISC Mirroring" \
    --display-name="Terraform Service Account for ISC Mirroring"

$ gcloud projects add-iam-policy-binding ${PROJECT_ID} \
    --member="serviceAccount:${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
    --role=projects/${PROJECT_ID}/roles/${ROLE_NAME}

Génération de la clé du compte de service

Générer la clé du compte de service et stocker sa valeur dans une certaine variable d'environnement:

$ gcloud iam service-accounts keys create ${SA_NAME}.json \
    --iam-account=${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com

$ export GOOGLE_APPLICATION_CREDENTIALS=<absolute_path_to_root_repo_dir>/terraform/${SA_NAME}.json

Génération d'une paire de clés SSH

Stockez une partie privée localement sous .ssh/isc_mirror et rendez-la visible pour ssh-agent. Mettre une partie publique dans un fichier isc_mirror.pub:

$ ssh-keygen -b 4096 -C "isc" -f ~/.ssh/isc_mirror
$ ssh-add  ~/.ssh/isc_mirror
$ ssh-add -l # Check if 'isc' key is present
$ cp ~/.ssh/isc_mirror.pub <root_repo_dir>/terraform/templates/

Création d'un stockage en nuage

Le stockage dans le nuage est utilisé pour stocker l'état de Terraform à distance. Vous pouvez consulter Store Terraform state in a bucket Cloud Storage comme exemple.

Remarque: Le Cloud Storage créé aura un nom comme isc-mirror-demo-terraform-<project_id>:

$ cd <root_repo_dir>/terraform-storage/
$ terraform init
$ terraform plan
$ terraform apply

Création de ressources avec Terraform

$ cd <root_repo_dir>/terraform/
$ terraform init -backend-config="bucket=isc-mirror-demo-terraform-${PROJECT_ID}"
$ terraform plan
$ terraform apply

Remarque 1: Quatre machines virtuelles seront créées. Une seule d'entre elles a une adresse IP publique et joue le rôle d'hôte bastion. Cette machine est appelée isc-client-001. Vous pouvez trouver l'adresse IP publique de l'instance isc-client-001 en exécutant la commande suivante:

$ export ISC_CLIENT_PUBLIC_IP=$(gcloud compute instances describe isc-client-001 --zone=${REGION}-c --format=json | jq -r '.networkInterfaces[].accessConfigs[].natIP')

Remarque 2: Parfois Terraform échoue avec des erreurs comme:

Failed to connect to the host via ssh: kex_exchange_identification: Connection closed by remote host...

Dans ce cas, essayez de purger un fichier local ~/.ssh/known_hosts:

$ for IP in ${ISC_CLIENT_PUBLIC_IP} 10.0.0.{3..6}; do ssh-keygen -R "[${IP}]:2180"; done

et répétez terraform apply.

Quick test

Accès aux instances miroir IRIS avec SSH

Toutes les instances, à l'exception de isc-client-001, sont créées dans un réseau privé pour augmenter le niveau de sécurité. Mais vous pouvez y accéder en utilisant la fonctionnalité SSH ProxyJump. Obtenez d'abord l'IP publique de isc-client-001:

$ export ISC_CLIENT_PUBLIC_IP=$(gcloud compute instances describe isc-client-001 --zone=${REGION}-c --format=json | jq -r '.networkInterfaces[].accessConfigs[].natIP')

Then connect to, for example, isc-primary-001 with a private SSH key. Remarquez que nous utilisons un port SSH personnalisé, 2180:

$ ssh -i ~/.ssh/isc_mirror -p 2180 isc@10.0.0.3 -o ProxyJump=isc@${ISC_CLIENT_PUBLIC_IP}:2180

Après la connexion, vérifions que le membre miroir principal a un Alias IP:

[isc@isc-primary-001 ~]$ ip route ls table local type local dev eth0 scope host proto 66
local 10.0.0.250

[isc@isc-primary-001 ~]$ ping -c 1 10.0.0.250
PING 10.0.0.250 (10.0.0.250) 56(84) bytes of data.
64 bytes from 10.0.0.250: icmp_seq=1 ttl=64 time=0.049 ms

Accès aux portails de gestion des instances miroir IRIS

Pour ouvrir des instances miroirs des portails de gestion situés dans un réseau privé, nous utilisons SSH Socks Tunneling.

Connectons-nous à l'instance isc-primary-001. Remarquez qu'un tunnel restera en arrière-plan après la prochaine commande:

$ ssh -f -N  -i ~/.ssh/isc_mirror -p 2180 isc@10.0.0.3 -o ProxyJump=isc@${ISC_CLIENT_PUBLIC_IP}:2180 -L 8080:10.0.0.3:8080

Le port 8080, au lieu de l'habituel 52773, est utilisé puisque nous démarrons IRIS avec une passerelle WebGateway dédiée fonctionnant sur le port 8080.

Une fois la connexion établie, ouvrez http://127.0.0.1:8080/csp/sys/UtilHome.csp dans un navigateur. Vous devriez voir un portail de gestion. Les informations d'identification sont typiques : _system/SYS.

La même approche fonctionne pour toutes les instances : primaire (10.0.0.3), de sauvegarde (10.0.0.4) et d'arbitre (10.0.0.5). Établissez d'abord une connexion SSH avec eux.

Test

Connectons-nous à ' isc-client-001`:

$ ssh -i ~/.ssh/isc_mirror -p 2180 isc@${ISC_CLIENT_PUBLIC_IP}

Vérifier la disponibilité du portail de gestion du membre miroir primaire sur l'adresse de l'Alias IP:

$ curl -s -o /dev/null -w "%{http_code}\n" http://10.0.0.250:8080/csp/sys/UtilHome.csp
200

Connectons-nous à 'isc-primary-001' sur une autre console:

$ ssh -i ~/.ssh/isc_mirror -p 2180 isc@10.0.0.3 -o ProxyJump=isc@${ISC_CLIENT_PUBLIC_IP}:2180

Et désactivez l'instance principale actuelle. Remarquez qu'IRIS ainsi que son portail Web s'exécutent dans Docker:

[isc@isc-primary-001 ~]$ docker-compose -f /isc-mirror/docker-compose.yml down

Vérifions à nouveau la disponibilité du portail de gestion du membre miroir sur l'adresse IP Alias à partir de isc-client-001 :

[isc@isc-client-001 ~]$ curl -s -o /dev/null -w "%{http_code}\n" http://10.0.0.250:8080/csp/sys/UtilHome.csp
200

Cela devrait fonctionner car l'Alias IP a été déplacé vers l'instance ' isc-backup-001`:

$ ssh -i ~/.ssh/isc_mirror -p 2180 isc@10.0.0.4 -o ProxyJump=isc@${ISC_CLIENT_PUBLIC_IP}:2180
[isc@isc-backup-001 ~]$ ip route ls table local type local dev eth0 scope host proto 66
local 10.0.0.250

Nettoyage

Suppression de l'infrastructure

$ cd <root_repo_dir>/terraform/
$ terraform init -backend-config="bucket=isc-mirror-demo-terraform-${PROJECT_ID}"
$ terraform destroy

Suppression du registre des artefacts

$ cd <root_repo_dir>/terraform
$ cat ${SA_NAME}.json | docker login -u _json_key --password-stdin https://${REGION}-docker.pkg.dev

$ for IMAGE in iris webgateway arbiter; do \
    gcloud artifacts docker images delete ${REGION}-docker.pkg.dev/${PROJECT_ID}/intersystems/${IMAGE}
done
$ gcloud artifacts repositories delete intersystems --location=${REGION}

Suppression du stockage en nuage

Supprimez le stockage en nuage où Terraform stocke son état. Dans notre cas, il s'agit d'un isc-mirror-demo-terraform-<project_id>.

Suppression du rôle Terraform

Supprimez le rôle Terraform créé dans Création d'un rôle Terraform.

Conclusion

Et c'est tout! We change networking configuration pointing to a current mirror Primary when the NotifyBecomePrimary event happens.

L'auteur souhaite remercier @Mikhail.Khomenko, @Vadim Aniskin, et @Evgeny Shvarov pour le Programme d'idées de la Communauté (Community Ideas Program) qui a rendu cet article possible.

Discussion (2)0
Connectez-vous ou inscrivez-vous pour continuer