14.3. 手順


  1. Openshift ユーザーアラートルーティングを有効にします。

    コマンド:

    oc apply -f - << EOF
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: user-workload-monitoring-config
      namespace: openshift-user-workload-monitoring
    data:
      config.yaml: |
        alertmanager:
          enabled: true
          enableAlertmanagerConfig: true
    EOF
    oc -n openshift-user-workload-monitoring rollout status --watch statefulset.apps/alertmanager-user-workload

  2. Lambda ウェブフックの認証に使用するユーザー名とパスワードの組み合わせを決定し、そのパスワードを保存する AWS シークレットを作成します。

    コマンド:

    aws secretsmanager create-secret \
      --name webhook-password \ 1
      --secret-string changeme \ 2
      --region eu-west-1 3

    1
    シークレットの名前
    2
    認証に使用するパスワード
    3
    シークレットをホストする AWS リージョン
  3. Lambda の実行に使用するロールを作成します。

    コマンド:

    FUNCTION_NAME= 1
    ROLE_ARN=$(aws iam create-role \
      --role-name ${FUNCTION_NAME} \
      --assume-role-policy-document \
      '{
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Principal": {
              "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
          }
        ]
      }' \
      --query 'Role.Arn' \
      --region eu-west-1 \ 2
      --output text
    )

    1
    Lambda および関連リソースに関連付ける任意の名前
    2
    Kubernetes クラスターをホストする AWS リージョン
  4. Lambda が AWS Secrets にアクセスできるように、'LambdaSecretManager' ポリシーを作成してアタッチします。

    コマンド:

    POLICY_ARN=$(aws iam create-policy \
      --policy-name LambdaSecretManager \
      --policy-document \
      '{
          "Version": "2012-10-17",
          "Statement": [
              {
                  "Effect": "Allow",
                  "Action": [
                      "secretsmanager:GetSecretValue"
                  ],
                  "Resource": "*"
              }
          ]
      }' \
      --query 'Policy.Arn' \
      --output text
    )
    aws iam attach-role-policy \
      --role-name ${FUNCTION_NAME} \
      --policy-arn ${POLICY_ARN}

  5. ElasticLoadBalancingReadOnly ポリシーをアタッチして、Lambda がプロビジョニングされたネットワークロードバランサーに対してクエリーを実行できるようにします。

    コマンド:

    aws iam attach-role-policy \
      --role-name ${FUNCTION_NAME} \
      --policy-arn arn:aws:iam::aws:policy/ElasticLoadBalancingReadOnly

  6. GlobalAcceleratorFullAccess ポリシーをアタッチして、Lambda が Global Accelerator EndpointGroup を更新できるようにします。

    コマンド:

    aws iam attach-role-policy \
      --role-name ${FUNCTION_NAME} \
      --policy-arn arn:aws:iam::aws:policy/GlobalAcceleratorFullAccess

  7. 必要なフェンシングロジックを含む Lambda ZIP ファイルを作成します。

    コマンド:

    LAMBDA_ZIP=/tmp/lambda.zip
    cat << EOF > /tmp/lambda.py
    
    from urllib.error import HTTPError
    
    import boto3
    import jmespath
    import json
    import os
    import urllib3
    
    from base64 import b64decode
    from urllib.parse import unquote
    
    # Prevent unverified HTTPS connection warning
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    
    
    class MissingEnvironmentVariable(Exception):
        pass
    
    
    class MissingSiteUrl(Exception):
        pass
    
    
    def env(name):
        if name in os.environ:
            return os.environ[name]
        raise MissingEnvironmentVariable(f"Environment Variable '{name}' must be set")
    
    
    def handle_site_offline(labels):
        a_client = boto3.client('globalaccelerator', region_name='us-west-2')
    
        acceleratorDNS = labels['accelerator']
        accelerator = jmespath.search(f"Accelerators[?(DnsName=='{acceleratorDNS}'|| DualStackDnsName=='{acceleratorDNS}')]", a_client.list_accelerators())
        if not accelerator:
            print(f"Ignoring SiteOffline alert as accelerator with DnsName '{acceleratorDNS}' not found")
            return
    
        accelerator_arn = accelerator[0]['AcceleratorArn']
        listener_arn = a_client.list_listeners(AcceleratorArn=accelerator_arn)['Listeners'][0]['ListenerArn']
    
        endpoint_group = a_client.list_endpoint_groups(ListenerArn=listener_arn)['EndpointGroups'][0]
        endpoints = endpoint_group['EndpointDescriptions']
    
        # Only update accelerator endpoints if two entries exist
        if len(endpoints) > 1:
            # If the reporter endpoint is not healthy then do nothing for now
            # A Lambda will eventually be triggered by the other offline site for this reporter
            reporter = labels['reporter']
            reporter_endpoint = [e for e in endpoints if endpoint_belongs_to_site(e, reporter)][0]
            if reporter_endpoint['HealthState'] == 'UNHEALTHY':
                print(f"Ignoring SiteOffline alert as reporter '{reporter}' endpoint is marked UNHEALTHY")
                return
    
            offline_site = labels['site']
            endpoints = [e for e in endpoints if not endpoint_belongs_to_site(e, offline_site)]
            del reporter_endpoint['HealthState']
            a_client.update_endpoint_group(
                EndpointGroupArn=endpoint_group['EndpointGroupArn'],
                EndpointConfigurations=endpoints
            )
            print(f"Removed site={offline_site} from Accelerator EndpointGroup")
    
            take_infinispan_site_offline(reporter, offline_site)
            print(f"Backup site={offline_site} caches taken offline")
        else:
            print("Ignoring SiteOffline alert only one Endpoint defined in the EndpointGroup")
    
    
    def endpoint_belongs_to_site(endpoint, site):
        lb_arn = endpoint['EndpointId']
        region = lb_arn.split(':')[3]
        client = boto3.client('elbv2', region_name=region)
        tags = client.describe_tags(ResourceArns=[lb_arn])['TagDescriptions'][0]['Tags']
        for tag in tags:
            if tag['Key'] == 'site':
                return tag['Value'] == site
        return false
    
    
    def take_infinispan_site_offline(reporter, offlinesite):
        endpoints = json.loads(INFINISPAN_SITE_ENDPOINTS)
        if reporter not in endpoints:
            raise MissingSiteUrl(f"Missing URL for site '{reporter}' in 'INFINISPAN_SITE_ENDPOINTS' json")
    
        endpoint = endpoints[reporter]
        password = get_secret(INFINISPAN_USER_SECRET)
        url = f"https://{endpoint}/rest/v2/container/x-site/backups/{offlinesite}?action=take-offline"
        http = urllib3.PoolManager(cert_reqs='CERT_NONE')
        headers = urllib3.make_headers(basic_auth=f"{INFINISPAN_USER}:{password}")
        try:
            rsp = http.request("POST", url, headers=headers)
            if rsp.status >= 400:
                raise HTTPError(f"Unexpected response status '%d' when taking site offline", rsp.status)
            rsp.release_conn()
        except HTTPError as e:
            print(f"HTTP error encountered: {e}")
    
    
    def get_secret(secret_name):
        session = boto3.session.Session()
        client = session.client(
            service_name='secretsmanager',
            region_name=SECRETS_REGION
        )
        return client.get_secret_value(SecretId=secret_name)['SecretString']
    
    
    def decode_basic_auth_header(encoded_str):
        split = encoded_str.strip().split(' ')
        if len(split) == 2:
            if split[0].strip().lower() == 'basic':
                try:
                    username, password = b64decode(split[1]).decode().split(':', 1)
                except:
                    raise DecodeError
            else:
                raise DecodeError
        else:
            raise DecodeError
    
        return unquote(username), unquote(password)
    
    
    def handler(event, context):
        print(json.dumps(event))
    
        authorization = event['headers'].get('authorization')
        if authorization is None:
            print("'Authorization' header missing from request")
            return {
                "statusCode": 401
            }
    
        expectedPass = get_secret(WEBHOOK_USER_SECRET)
        username, password = decode_basic_auth_header(authorization)
        if username != WEBHOOK_USER and password != expectedPass:
            print('Invalid username/password combination')
            return {
                "statusCode": 403
            }
    
        body = event.get('body')
        if body is None:
            raise Exception('Empty request body')
    
        body = json.loads(body)
        print(json.dumps(body))
    
        if body['status'] != 'firing':
            print("Ignoring alert as status is not 'firing', status was: '%s'" % body['status'])
            return {
                "statusCode": 204
            }
    
        for alert in body['alerts']:
            labels = alert['labels']
            if labels['alertname'] == 'SiteOffline':
                handle_site_offline(labels)
    
        return {
            "statusCode": 204
        }
    
    
    INFINISPAN_USER = env('INFINISPAN_USER')
    INFINISPAN_USER_SECRET = env('INFINISPAN_USER_SECRET')
    INFINISPAN_SITE_ENDPOINTS = env('INFINISPAN_SITE_ENDPOINTS')
    SECRETS_REGION = env('SECRETS_REGION')
    WEBHOOK_USER = env('WEBHOOK_USER')
    WEBHOOK_USER_SECRET = env('WEBHOOK_USER_SECRET')
    
    EOF
    zip -FS --junk-paths ${LAMBDA_ZIP} /tmp/lambda.py

  8. Lambda 関数を作成します。

    コマンド:

    aws lambda create-function \
      --function-name ${FUNCTION_NAME} \
      --zip-file fileb://${LAMBDA_ZIP} \
      --handler lambda.handler \
      --runtime python3.12 \
      --role ${ROLE_ARN} \
      --region eu-west-1 1

    1
    Kubernetes クラスターをホストする AWS リージョン
  9. 関数 URL を公開して、Lambda を Webhook としてトリガーできるようにします。

    コマンド:

    aws lambda create-function-url-config \
      --function-name ${FUNCTION_NAME} \
      --auth-type NONE \
      --region eu-west-1 1

    1
    Kubernetes クラスターをホストする AWS リージョン
  10. 関数 URL のパブリック呼び出しを許可します。

    コマンド:

    aws lambda add-permission \
      --action "lambda:InvokeFunctionUrl" \
      --function-name ${FUNCTION_NAME} \
      --principal "*" \
      --statement-id FunctionURLAllowPublicAccess \
      --function-url-auth-type NONE \
      --region eu-west-1 1

    1
    Kubernetes クラスターをホストする AWS リージョン
  11. Lambda の環境変数を設定します。

    1. 各 Kubernetes クラスターで、公開された Data Grid URL エンドポイントを取得します。

      oc -n ${NAMESPACE} get route infinispan-external -o jsonpath='{.status.ingress[].host}' 1
      1
      ${NAMESPACE} を、Data Grid サーバーが含まれる namespace に置き換えます。
    2. 必要な環境変数をアップロードします。

      ACCELERATOR_NAME= 1
      LAMBDA_REGION= 2
      CLUSTER_1_NAME= 3
      CLUSTER_1_ISPN_ENDPOINT= 4
      CLUSTER_2_NAME= 5
      CLUSTER_2_ISPN_ENDPOINT= 6
      INFINISPAN_USER= 7
      INFINISPAN_USER_SECRET= 8
      WEBHOOK_USER= 9
      WEBHOOK_USER_SECRET= 10
      
      INFINISPAN_SITE_ENDPOINTS=$(echo "{\"${CLUSTER_NAME_1}\":\"${CLUSTER_1_ISPN_ENDPOINT}\",\"${CLUSTER_2_NAME}\":\"${CLUSTER_2_ISPN_ENDPOINT\"}" | jq tostring)
      aws lambda update-function-configuration \
          --function-name ${ACCELERATOR_NAME} \
          --region ${LAMBDA_REGION} \
          --environment "{
            \"Variables\": {
              \"INFINISPAN_USER\" : \"${INFINISPAN_USER}\",
              \"INFINISPAN_USER_SECRET\" : \"${INFINISPAN_USER_SECRET}\",
              \"INFINISPAN_SITE_ENDPOINTS\" : ${INFINISPAN_SITE_ENDPOINTS},
              \"WEBHOOK_USER\" : \"${WEBHOOK_USER}\",
              \"WEBHOOK_USER_SECRET\" : \"${WEBHOOK_USER_SECERT}\",
              \"SECRETS_REGION\" : \"eu-central-1\"
            }
          }"
      1
      デプロイメントで使用される AWS Global Accelerator の名前
      2
      Kubernetes クラスターと Lambda 関数をホストしている AWS リージョン
      3
      Data Grid Operator を使用した HA 用の Data Grid のデプロイ で定義されている 1 つの Data Grid サイトの名前
      4
      CLUSER_1_NAME サイトに関連付けられた Data Grid エンドポイント URL
      5
      2 番目の Data Grid サイトの名前
      6
      CLUSER_2_NAME サイトに関連付けられた Data Grid エンドポイント URL
      7
      サーバー上で REST リクエストを実行するのに十分な権限を持つ Data Grid ユーザーのユーザー名
      8
      Data Grid ユーザーに関連付けられたパスワードが含まれる AWS シークレットの名前
      9
      Lambda 関数へのリクエストの認証に使用されるユーザー名
      10
      Lambda 関数へのリクエストの認証に使用されるパスワードが含まれる AWS シークレットの名前
  12. Lambda 関数 URL を取得します。

    コマンド:

    aws lambda get-function-url-config \
      --function-name ${FUNCTION_NAME} \
      --query "FunctionUrl" \
      --region eu-west-1 \1
      --output text

    1
    Lambda が作成された AWS リージョン

    出力:

    https://tjqr2vgc664b6noj6vugprakoq0oausj.lambda-url.eu-west-1.on.aws

  13. 各 Kubernetes クラスターで、スプリットブレイン発生時に Lambda をトリガーする Prometheus アラートルーティングを設定します。

    コマンド:

    NAMESPACE= # The namespace containing your deployments
    oc apply -n ${NAMESPACE} -f - << EOF
    apiVersion: v1
    kind: Secret
    type: kubernetes.io/basic-auth
    metadata:
      name: webhook-credentials
    stringData:
      username: 'keycloak' 1
      password: 'changme' 2
    ---
    apiVersion: monitoring.coreos.com/v1beta1
    kind: AlertmanagerConfig
    metadata:
      name: example-routing
    spec:
      route:
        receiver: default
        groupBy:
          - accelerator
        groupInterval: 90s
        groupWait: 60s
        matchers:
          - matchType: =
            name: alertname
            value: SiteOffline
      receivers:
        - name: default
          webhookConfigs:
            - url: 'https://tjqr2vgc664b6noj6vugprakoq0oausj.lambda-url.eu-west-1.on.aws/' 3
              httpConfig:
                basicAuth:
                  username:
                    key: username
                    name: webhook-credentials
                  password:
                    key: password
                    name: webhook-credentials
                tlsConfig:
                  insecureSkipVerify: true
    ---
    apiVersion: monitoring.coreos.com/v1
    kind: PrometheusRule
    metadata:
      name: xsite-status
    spec:
      groups:
        - name: xsite-status
          rules:
            - alert: SiteOffline
              expr: 'min by (namespace, site) (vendor_jgroups_site_view_status{namespace="default",site="site-b"}) == 0' 4
              labels:
                severity: critical
                reporter: site-a 5
                accelerator: a3da6a6cbd4e27b02.awsglobalaccelerator.com 6

    1
    Lambda リクエストの認証に必要なユーザー名
    2
    Lambda リクエストの認証に必要なパスワード
    3
    Lambda 関数 URL
    4
    namespace の値は Infinispan CR をホストする namespace、サイトは Infinispan CR の spec.service.sites.locations[0].name で定義されたリモートサイトでなければなりません。
    5
    Infinispan CR の spec.service.sites.local.name で定義されたローカルサイトの名前
    6
    Global Accelerator の DNS
Red Hat logoGithubRedditYoutubeTwitter

詳細情報

試用、購入および販売

コミュニティー

Red Hat ドキュメントについて

Red Hat をお使いのお客様が、信頼できるコンテンツが含まれている製品やサービスを活用することで、イノベーションを行い、目標を達成できるようにします。

多様性を受け入れるオープンソースの強化

Red Hat では、コード、ドキュメント、Web プロパティーにおける配慮に欠ける用語の置き換えに取り組んでいます。このような変更は、段階的に実施される予定です。詳細情報: Red Hat ブログ.

会社概要

Red Hat は、企業がコアとなるデータセンターからネットワークエッジに至るまで、各種プラットフォームや環境全体で作業を簡素化できるように、強化されたソリューションを提供しています。

© 2024 Red Hat, Inc.