Article
· Juin 23, 2023 8m de lecture

VIP dans l'AWS

Si vous utilisez IRIS dans une configuration miroir pour HA dans AWS, la question de la fourniture d'un Miroir VIP (IP virtuelle) devient pertinente. L'IP virtuelle 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 à travailler.

Le principal problème, lors du déploiement sur AWS, est qu'un VIP IRIS exige que les deux membres du miroir soient dans le même sous-réseau, d'après les docs :

Pour utiliser un miroir VIP, les deux membres du basculement doivent être configurés dans le même sous-réseau et le VIP doit appartenir au même sous-réseau que l'interface réseau sélectionnée sur chaque système.

Cependant, pour obtenir l'HA, les membres du miroir IRIS doivent être déployés dans des zones de disponibilité différentes, ce qui signifie des sous-réseaux différents (car les sous-réseaux ne peuvent être que dans un seul az). L'une des solutions pourrait être les équilibreurs de charge, mais ils (A) coûtent cher, et (B) si vous devez acheminer du trafic non-HTTP (comme TCP pour HL7), vous devrez utiliser des équilibreurs de charge de réseau avec une limite de 50 ports au total.

Dans cet article, je voudrais fournir un moyen de configurer un Mirror VIP sans utiliser l'équilibrage de la charge du réseau suggéré dans la plupart des autres architectures de référence AWS. En production, nous avons trouvé des limitations qui entravaient les solutions avec le coût, les limites de 50 auditeurs, les dépendances DNS et la nature dynamique des deux adresses IP qu'AWS fournit à travers les zones de disponibilité.

Architecture

Architecture(4)

Nous avons un VPC avec trois sous-réseaux privés (je simplifie ici - bien sûr, vous aurez probablement des sous-réseaux publics, un arbitre dans une autre AZ, et ainsi de suite, mais c'est un minimum absolu suffisant pour démontrer cette approche). Le VPC se voit allouer des IPs : 10.109.10.1 à 10.109.10.254 ; les sous-réseaux (dans différents AZs) sont : 10.109.10.1 à 10.109.10.62, 10.109.10.65 à 10.109.10.126, et 10.109.10.224 à 10.109.10.254.

Implementation d'un VIP

  1. Sur chaque instance EC2 (SourceDestCheck doit être définie sur false), nous allons allouer la même adresse IP sur l'interface réseau eth0:1. Cette adresse IP se trouve dans la plage CIDR du VPC - dans une zone VIP spéciale. Par exemple, nous pouvons utiliser la dernière IP d'une plage - 10.109.10.254 :
cat << EOFVIP >> /etc/sysconfig/network-scripts/ifcfg-eth0:1
          DEVICE=eth0:1
          ONPARENT=on
          IPADDR=10.109.10.254
          PREFIX=27
          EOFVIP
sudo chmod -x /etc/sysconfig/network-scripts/ifcfg-eth0:1
sudo ifconfig eth0:1 up

En fonction du système d'exploitation, vous pouvez avoir besoin d'exécuter :

ifconfig eth0:1
systemctl restart network
  1. En cas de basculement du miroir, mettre à jour la table de routage pour qu'elle pointe vers l'eni sur le nouveau primaire. Nous utiliserons un rappel ZMIRROR pour mettre à jour le tableau de routage après que le membre du miroir actuel soit devenu le primaire. Ce code utilise Python intégré pour :
  • Obtenir l'IP sur eth0:1
  • Obtenir l'InstanceId et la Région à partir des métadonnées de l'instance
  • Trouver le tableau des routes principales pour le VPC EC2
  • Supprimer l'ancienne route, s'il y en a une
  • Ajouter une nouvelle route pointant vers elle-même

Le code:

import os
import urllib.request
import boto3
from botocore.exceptions import ClientError

PRIMARY_INTERFACE = 'eth0'
VIP_INTERFACE = 'eth0:1'

eth0_addresses = [
    line
    for line in os.popen(f'ip -4 addr show dev {PRIMARY_INTERFACE}').read().split('\n')
    if line.strip().startswith('inet')
]

VIP = None
for address in eth0_addresses:
    if address.split(' ')[-1] == VIP_INTERFACE:
        VIP = address.split(' ')[5]

if VIP is None:
    raise ValueError('Échec de la récupération d'un VIP valide !')

# Recherche de l'ID de l'instance du membre du miroir actuel
instanceid = (
    urllib.request.urlopen('http://169.254.169.254/latest/meta-data/instance-id')
    .read()
    .decode()
)

region = (
    urllib.request.urlopen('http://169.254.169.254/latest/meta-data/placement/region')
    .read()
    .decode()
)

session = boto3.Session(region_name=region)
ec2Resource = session.resource('ec2')
ec2Client = session.client('ec2')
instance = ec2Resource.Instance(instanceid)

# Recherche de l'ID du tableau de routage principal pour ce VPC
vpc = ec2Resource.Vpc(instance.vpc.id)

for route_table in vpc.route_tables.all():
    # Mise à jour du tableau de routage principal pour pointer vers cette instance
    try:
        ec2Client.delete_route(
            DestinationCidrBlock=VIP, RouteTableId=str(route_table.id)
        )
    except ClientError as exc:
        if exc.response['Error']['Code'] == 'InvalidRoute.NotFound':
            print('Rien à supprimer, continuer')
        else:
            raise exc
    # Ajout de la nouvelle route
    ec2Client.create_route(
        DestinationCidrBlock=VIP,
        NetworkInterfaceId=instance.network_interfaces[0].id,
        RouteTableId=str(route_table.id),
    )

et le même code comme routine ZMIRROR :

NotifyBecomePrimary() PUBLIC {
  try {
    set dir = $system.Util.ManagerDirectory()_ "python"
    do ##class(%File).CreateDirectoryChain(dir)

    try {
      set boto3 = $system.Python.Import("boto3")
    } catch {
      set cmd = "pip3"
      set args($i(args)) = "install"
      set args($i(args)) = "--target"
      set args($i(args)) = dir
      set args($i(args)) = "boto3"
      set sc = $ZF(-100,"", cmd, .args)
      // pour python précédant la version 3.7, installer également dataclasses
      set boto3 = $system.Python.Import("boto3")
    }
    kill boto3

    set code =  "import os" _ $c(10) _
                "import urllib.request" _ $c(10) _
                "import boto3" _ $c(10) _
                "from botocore.exceptions import ClientError" _ $c(10) _
                "PRIMARY_INTERFACE = 'eth0'" _ $c(10) _
                "VIP_INTERFACE = 'eth0:1'" _ $c(10) _
                "eth0_addresses = [" _ $c(10) _
                "    line" _ $c(10) _
                "    for line in os.popen(f'ip -4 addr show dev {PRIMARY_INTERFACE}').read().split('\n')" _ $c(10) _
                "    if line.strip().startswith('inet')" _ $c(10) _
                "]" _ $c(10) _
                "VIP = None" _ $c(10) _
                "for address in eth0_addresses:" _ $c(10) _
                "    if address.split(' ')[-1] == VIP_INTERFACE:" _ $c(10) _
                "        VIP = address.split(' ')[5]" _ $c(10) _
                "if VIP is None:" _ $c(10) _
                "    raise ValueError('Échec de la récupération d'un VIP valide !')" _ $c(10) _
                "# Recherche de l'ID de l'instance du membre du miroir actuel" _ $c(10) _
                "instanceid = (" _ $c(10) _
                "    urllib.request.urlopen('http://169.254.169.254/latest/meta-data/instance-id')" _ $c(10) _
                "    .read()" _ $c(10) _
                "    .decode()" _ $c(10) _
                ")" _ $c(10) _
                "region = (" _ $c(10) _
                "    urllib.request.urlopen('http://169.254.169.254/latest/meta-data/placement/region')" _ $c(10) _
                "    .read()" _ $c(10) _
                "    .decode()" _ $c(10) _
                ")" _ $c(10) _
                "session = boto3.Session(region_name=region)" _ $c(10) _
                "ec2Resource = session.resource('ec2')" _ $c(10) _
                "ec2Client = session.client('ec2')" _ $c(10) _
                "instance = ec2Resource.Instance(instanceid)" _ $c(10) _
                "# Recherche de l'ID du tableau de routage principal pour ce VPC" _ $c(10) _
                "vpc = ec2Resource.Vpc(instance.vpc.id)" _ $c(10) _
                "for route_table in vpc.route_tables.all():" _ $c(10) _
                "    # Mise à jour du tableau de routage principal pour pointer vers cette instance" _ $c(10) _
                "    try:" _ $c(10) _
                "        ec2Client.delete_route(" _ $c(10) _
                "            DestinationCidrBlock=VIP, RouteTableId=str(route_table.id)" _ $c(10) _
                "        )" _ $c(10) _
                "    except ClientError as exc:" _ $c(10) _
                "        if exc.response['Error']['Code'] == 'InvalidRoute.NotFound':" _ $c(10) _
                "            print('Rien à supprimer, continuer')" _ $c(10) _
                "        else:" _ $c(10) _
                "            raise exc" _ $c(10) _
                "    # Ajout de la nouvelle route" _ $c(10) _
                "    ec2Client.create_route(" _ $c(10) _
                "        DestinationCidrBlock=VIP," _ $c(10) _
                "        NetworkInterfaceId=instance.network_interfaces[0].id," _ $c(10) _
                "        RouteTableId=str(route_table.id)," _ $c(10) _
                "    )"


    set rc = $system.Python.Run(code)
    set sc = ##class(%SYS.System).WriteToConsoleLog("Attribution VIP " _ $case(rc, 0:"successful", :"error"), , $case(rc, 0:0, :1), "NotifyBecomePrimary:ZMIRROR")

  } catch ex {
    #dim ex As %Exception.General
    do ex.Log()
    set sc = ##class(%SYS.System).WriteToConsoleLog("Une exception a été détectée lors de I'attribution d'un VIP : " _ ex.DisplayString(), , 1, "NotifyBecomePrimary:ZMIRROR")
  }
  quit 1
}

Démarrage initial

NotifyBecomePrimary est aussi appelé automatiquement au démarrage du système (après la reconnexion des miroirs), mais si vous voulez que vos environnements non-miroirs acquièrent VIP de la même manière, utilisez la routine ZSTART routine:

SYSTEM() PUBLIC {
  if '$SYSTEM.Mirror.IsMember() {
    do NotifyBecomePrimary^ZMIRROR()
  }
  quit 1
}

Suppression

Si vous utilisez des outils de provisionnement automatique, comme CloudFormation, cette route doit être supprimée avant de pouvoir supprimer le sous-réseau. Vous pouvez ajouter le code de suppression à ^%ZSTOP, mais n'oubliez pas de vérifier $SYSTEM.Mirror.IsPrimary() parce que lorsque le miroir primaire s'arrête, pendant ^%ZSTOP il est toujours primaire. De manière générale, je recommanderais la suppression des routes externes dans le cadre d'un script d'outils de provisionnement.

Conclusion

Et c'est tout ! Dans le tableau de routage, nous obtenons une nouvelle route pointant vers un miroir primaire Primary actuel lorsque l'événement NotifyBecomePrimary se produit.

image

L'auteur tient à remercier Jared Trog et @Ron Sweeney pour la création de cette approche.

L'auteur tient à remercier @Tomohiro Iwamoto pour avoir testé cette approche et déterminé toutes les conditions requises pour qu'elle fonctionne.

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