SaaS-Modernisierung mit Amazon EKS, Teil 4 Onboarding neuer Mandanten im SaaS-Produkt

Ein Gastbeitrag von Markus Kokott *

Dank GitOps lassen sich SaaS-Mandanten über Kubernetes automatisiert bereitstellen, wie wir im vorigen Beitrag dieser Serie grundlegend besprochen haben. Nun betrachten wir noch den Onboarding-Prozess für neue Mandanten.

Anbieter zum Thema

Vereinfachte Darstellung des Mandanten-Onboarding-Prozesses.
Vereinfachte Darstellung des Mandanten-Onboarding-Prozesses.
(Bild: AWS Deutschland)

Entscheiden sich Kunden für die SaaS-Variante einer Softwarelösung, erwarten sie eine schnelle Time-to-Value. Das heißt, sie wollen möglichst wenig Zeit damit verbringen, die Software für sich nutzbar zu machen, sondern direkt die eigentliche Arbeit mit der Software beginnen.

Ein interessierter Kunde registriert sich idealerweise selbst für die SaaS-Lösung, erhält umgehend seine Zugangsdaten und kann seinen Mandanten in wenigen für den Einsatz vorbereiten. Dies ist nur dann möglich, wenn die Self-Service-Registrierung das Onboarding des Mandanten unverzüglich startet.

Die Onboarding-Phase besteht dabei aus einer Reihe von Schritten. Viele dieser Schritte sind unabhängig voneinander und sollten parallel gestartet werden, um die Time-to-Value zu minimieren. Typischerweise wird …

  • der neue Mandant in den Systemen zur Rechnungsstellung und Abwicklung hinterlegt,
  • es wird ein erster administrativer Nutzer angelegt,
  • betriebliche Systeme werden über den neuen Mandanten und die gültigen SLAs informiert und schließlich
  • werden die notwendigen Ressourcen provisioniert.

Vereinfachte Darstellung des Mandanten-Onboarding-Prozesses.
Vereinfachte Darstellung des Mandanten-Onboarding-Prozesses.
(Bild: AWS Deutschland)

Der Onboarding-Prozess lässt sich also als Event-basiertes System mit einem Fan-Out interpretieren. Dies bedeutet, dass ein Ereignis im System an einen oder mehrere Konsumenten zur Verarbeitung gesendet wird. In der Softwarearchitektur werden hierfür typischerweise Topics in einem Event Bus verwendet. Vereinfacht sieht der Onboarding-Prozess so aus, wie im Bild vorne.

Amazon Simple Notification Service

Standard-Topic für ein automatisch angetriggertes User-Onboarding.
Standard-Topic für ein automatisch angetriggertes User-Onboarding.
(Bild: AWS Deutschland)

AWS bietet mit dem Amazon Simple Notification Service einen verwalteten Messaging-Dienst, der einfach und kostengünstig nutzbar und sehr gut in das AWS-Ökosystem integriert ist. Für unser Onboarding-Beispiel werden wir das Erzeugen eines ersten Users und das Anlegen der GitOps-Konfiguration für den Tenant durch ein Event in SNS auslösen. Dazu legen wir ein Standard-Topic tenant-onboarding an.

Empfänger dieses Topics erhalten Events mit einem festen Schema. Der für unseren Fall interessante Teil des Events ist der Message-Block. Hier können wir beliebige Key-Value-Paare übermitteln. Wir nehmen an, dass die durch das Registration-Form erzeugten Nachrichten die Felder name, email, service-tier und tenant-id enthalten:

{
  "Records": [
    {
      "EventSource": "aws:sns",
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:us-east-1:{{{accountId}}}:ExampleTopic",
      "Sns": {
        "Type": "Notification",
        "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
        "TopicArn": "arn:aws:sns:us-east-1:123456789012:ExampleTopic",
        "Subject": "new tenant registered",
        "Message": {
          "name": "Tim Test",
          "email": "tim.test@example.com",
          "service-tier": "premium",
          "tenant-id": "test-tenant"
          },
        "Timestamp": "1970-01-01T00:00:00.000Z",
        "SignatureVersion": "1",
        "Signature": "EXAMPLE",
        "SigningCertUrl": "EXAMPLE",
        "UnsubscribeUrl": "EXAMPLE"
      }
    }
  ]
}

Wir können nun Konsumenten implementieren, die Nachrichten aus diesem Topic erhalten, und Nutzer in Amazon Cognito anlegen sowie Konfigurationsdateien im Git ablegen.

AWS Lambda

Event-basierte Systeme lassen sich mit Serverless-Technologien in der Cloud besonders kosteneffizient und skalierbar bauen. Statt langlebiger Prozesse, die 24/7 laufen und auf die Registrierung neuer Kunden warten, werden wir in unserem Beispiel die Onboarding-Prozesse dynamisch bei eingehenden Events starten und im Anschluss wieder stoppen.

Für solche Zwecke bietet sich AWS Lambda an. Es handelt sich hierbei um einen Serverless Compute Service, bei dem kurzlebige Funktionen hinterlegt werden können, die nur bei Bedarf provisioniert und millisekundengenau abgerechnet wird. Der Service unterstützt eine Vielzahl von Programmiersprachen wie zum Beispiel Java, Python oder Go mit vorgefertigten Laufzeitumgebungen.

AWS Lambda ermöglicht es aber auch, eigene Laufzeitumgebungen zu definieren. Die Paketierung erfolgt entweder über ein Zip-Archiv oder Docker Container. Wir werden Container nutzen, um Bash-Skripte als Lambda Funktionen auszuführen. In der Praxis werden viele Wartungs- und Onboarding-Prozesse durch Betriebsteams in Skripten automatisiert, sodass eine Neuimplementierung oft nicht notwendig ist.

Übersicht über die verwendeten Komponenten und Zuständigkeiten.
Übersicht über die verwendeten Komponenten und Zuständigkeiten.
(Bild: AWS Deutschland)

AWS stellt ein Basis-Image für eigene AWS-Lambda-Laufzeitumgebungen bereit: amazon/aws-lambda-provided:al2 – dieses Image kann durch die Verwendung in einem Dockerfile erweitert werden, sodass die notwendigen Abhängigkeiten im Image vorhanden sind. Die Integration in den AWS Lambda Service selbst erfolgt dann durch das Hinzufügen zweier Skripte in das Image: einem Initialisierungsskript, das auf Events durch den Lambda Service lauscht, und einem in der gewünschten Sprache geschriebenem Handler-Skript, das die Geschäftslogik enthält.

Wir wollen wie erwähnt Bash-Skripte verwenden und implementieren das Erstellen des ersten administrativen Nutzers in Amazon Cognito sowie die Bereitstellung der Anwendung über einen GitOps-Prozess. Beginnen wir zunächst mit dem für beide Funktionen identischen Initialisierungsskript. Im Custom Runtime Tutorial für AWS Lambda wird ein Skript verwendet, das wir ohne Änderungen übernehmen können:

Jetzt Newsletter abonnieren

Täglich die wichtigsten Infos zu Softwareentwicklung und DevOps

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung.

Aufklappen für Details zu Ihrer Einwilligung
#!/bin/shset -euo pipefail# Initialization - load function handler
source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"
# Processing
while true
do
  HEADERS="$(mktemp)"
  # Get an event. The HTTP request will block until one is received
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET \
    "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
  # Extract request ID by scraping response headers received above
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" \
    | tr -d '[:space:]' | cut -d: -f2)
  # Run the handler function from the script
  RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")
  # Send the response
  curl -X POST \
    "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" \
    -d "$RESPONSE"
done

Das Skript nimmt Events über das Interface mit dem AWS-Lambda-Service entgegen, welche es an das in $_HANDLER definierte Handler-Skript übergibt. Dieses Skript wiederum enthält die Geschäftslogik für den jeweiligen Prozess.

Den ersten Nutzer automatisch erzeugen

Im ersten Fall möchten wir über die AWS CLI einen neuen Nutzer im User Pool von Amazon Cognito aus dem zweiten Teil dieser Reihe anlegen. Wir parsen dazu das oben beschriebene SNS Event mit dem Kommandozeilen-Tool jq und übergeben die Parameter an die AWS CLI. Um das Image wiederverwertbar zu machen, wird zudem die $COGNITO_USERPOOL_ID als Umgebungsvariable vorausgesetzt:

function createFirstTenantUser () {  EVENT_DATA=$(echo $1 | jq '.Records[0].Sns.Message')
  # removing first and last double quote + escape characters
  # because Mesaage is delivered as String by SNS
  EVENT_DATA=$(echo ${EVENT_DATA:1:${#EVENT_DATA}-2} \
    | sed 's@\\n@@g' | sed 's@\\"@"@g' | jq -r .)
  USER_NAME=$(echo $EVENT_DATA | jq -r '.name')
  USER_EMAIL=$(echo $EVENT_DATA | jq -r '.email')
  USER_ADDRESS=$(echo $EVENT_DATA | jq -r '.address' | tr ',' ';')
  USER_PHONE=$(echo $EVENT_DATA | jq -r '.phone')
  USER_TIER=$(echo $EVENT_DATA | jq -r '."service-tier"')
  USER_TENANT=$(echo $EVENT_DATA | jq -r '."tenant-id"')
  USER_ROLE="admin"
  USER_STATUS="inactive"
  aws cognito-idp admin-create-user \
    --user-pool-id $COGNITO_USERPOOL_ID \
    --username $USER_EMAIL \
    --user-attributes \
      Name="name",Value="$USER_NAME" \
      Name="email",Value="$USER_EMAIL" \
      Name="address",Value="$USER_ADDRESS" \
      Name="custom:service-tier",Value="$USER_TIER" \
      Name="custom:tenant-id",Value="$USER_TENANT" \
      Name="custom:status",Value="inactive" \
      Name="custom:tenant-role",Value="admin" 1>&2;
}

Das Bash Skript benötigt die AWS CLI und jq. Unser Dockerfile für den Cognito-Tenant-Manager sieht dann wie folgt aus:

# Start from the AWS provided AWS Lambda image
FROM amazon/aws-lambda-provided:al2
# Install AWS CLI v2 & helpers
RUN yum install -y unzip jq
ADD https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip /tmp/awscliv2.zip
RUN unzip /tmp/awscliv2.zip -d /tmp
RUN /tmp/aws/install
RUN yum erase -y unzip
# /var/runtime/bootstrap is the ENTRYPOINT in amazon/aws-lambda-provided:al2
COPY bootstrap /var/runtime/bootstrap
RUN chmod 755 /var/runtime/bootstrap
# adding the actual handler script
COPY function.sh /var/task/function.sh
RUN chmod 755 /var/task/function.sh
# Set the handler
# by convention <fileName>.<handlerName>
CMD [ "function.createFirstTenantUser" ]

Nun können wir das Docker-Image bauen und in dem zugehörigen ECR-Repository „cognito-manager“ ablegen:

docker build -t cognito-manager . \
  && docker tag cognito-manager:latest \
    <ecr-repository>/cognito-manager:latest \
  && docker push \
    <ecr-repository>/cognito-manager:latest

Jetzt können wir die Lambda-Funktionen einrichten. Wie das geht, zeigt die folgende, kurze Bildergalerie (Navigation mit den Pfeiltasten (← / →) oder den Schaltflächen im Bild).

Bildergalerie
Bildergalerie mit 7 Bildern

Den Tenant im EKS Cluster anlegen

Die Kommandozeilenaufrufe, um einen neuen Mandaten in unserem GitOps-Workflow aus dem dritten Teil dieser Reihe anzulegen, werden im Handler-Skript für den GitOps-Tenant-Manager ausgeführt. Auch hier bedienen wir uns wieder jq, um das eingehende Event zu parsen:

function createTenant () {  EVENT_DATA=$(echo $1 | jq '.Records[0].Sns.Message')
  # removing first and last double quote + escape characters
  # because Mesaage is delivered as String by SNS
  EVENT_DATA=$(echo ${EVENT_DATA:1:${#EVENT_DATA}-2} \
    | sed 's@\\n@@g' | sed 's@\\"@"@g' | jq -r .)
  TENANT_ID=$(echo $EVENT_DATA | jq -r '."tenant-id"')
  cd /tmp
  git clone \
    "https://${GITHUB_TOKEN}:x-oauth-basic@github.com/${CLUSTER_CONFIG_REPO}" \
    --branch ${CLUSTER_CONFIG_BRANCH} \
    --single-branch \
    cluster-config
  if [ -d cluster-config/${CLUSTER_CONFIG_PATH}/${TENANT_ID} ]; then
    echo "tenant ${TENANT_ID} already exists"
    exit 1
  fi
  mkdir -p cluster-config/${CLUSTER_CONFIG_PATH}/${TENANT_ID} \
    && cd cluster-config/${CLUSTER_CONFIG_PATH}/${TENANT_ID}
  flux create tenant ${TENANT_ID} \
    --with-namespace=${TENANT_ID} \
    --export > rbac.yaml
  flux create source git ${TENANT_ID} \
    --namespace=${TENANT_ID} \
    --url=${APP_REPO_URL} \
    --branch=${APP_REPO_BRANCH} \
    --export > sync.yaml
  flux create kustomization ${TENANT_ID} \
    --namespace=${TENANT_ID} \
    --service-account=${TENANT_ID} \
    --target-namespace=${TENANT_ID} \
    --source=GitRepository/${TENANT_ID} \
    --path=${APP_REPO_MANIFEST_PATH} \
    --export >> sync.yaml
  kustomize create --autodetect  git add -A
  git -c "user.name=Tenant Onboarding Service" \
    -c "user.email=noreply@example.com" \
    commit -m "created tenant ${TENANT_ID}"
  git pull --rebase && git push
}

Das Skript benötigt darüber hinaus noch einige Konfigurationsparameter zur Laufzeit:

  • $APP_REPO_URL
    $APP_REPO_BRANCH
    $APP_REPO_MANIFEST_PATH
    $CLUSTER_CONFIG_REPO
    $CLUSTER_CONFIG_BRANCH
    $CLUSTER_CONFIG_PATH
    $GITHUB_TOKEN

Für dieses Skript benötigen wir neben jq noch flux, kustomize und git. Deshalb sieht unser Dockerfile in diesem Fall so aus:

# Start from the AWS provided AWS Lambda image
FROM amazon/aws-lambda-provided:al2
# Install git, flux, kustomize, jq & dependencies
RUN yum install -y git unzip jq which tar
RUN curl -s https://fluxcd.io/install.sh | bash
RUN cd /bin && curl -s \
  "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" \
  | bash && ln -sn /usr/kustomize /bin/kustomize
RUN yum erase -y which tar
# Copy our bootstrap and make it executable
COPY bootstrap /var/runtime/bootstrap
RUN chmod 755 /var/runtime/bootstrap
# Copy our function code and make it executable
COPY function.sh /var/task/function.sh
RUN chmod 755 /var/task/function.sh
# Set the handler
# by convention <fileName>.<handlerName>
CMD [ "function.createTenant" ]

Das Anlegen der Lambda-Funktion.
Das Anlegen der Lambda-Funktion.
(Bild: AWS Deutschland)

Auch dieses Docker Image wird gebaut und in das ECR Repository flux-manager hochgeladen:

docker build -t flux-manager . \
  && docker tag flux-manager \
    <ecr-repository>/flux-manager:latest \
  && docker push \
    <ecr-repository>/flux-manager:latest

Das Anlegen der Lambda-Funktion erfolgt wieder mittels der AWS-Konsole.

Erneutes Setzen der Umgebungsvariablen
Erneutes Setzen der Umgebungsvariablen
(Bild: AWS Deutschland)

Mit Ausnahme von CloudWatch-Logs greift die Funktion auf keine AWS Services zu. Deshalb müssen wir in diesem Fall keine IAM-Rechte anpassen. Wir müssen allerdings wieder das Timeout-Intervall auf 90 Sekunden erhöhen und die notwendigen Umgebungsvariablen setzen.

Ein Testlauf

Über die „Publish Message“-Schaltfläche senden wir testhalber eine Nachricht.
Über die „Publish Message“-Schaltfläche senden wir testhalber eine Nachricht.
(Bild: AWS Deutschland)

Wir können nun über die SNS-Konsole einen Test durchführen. Hierzu senden wir eine Nachricht von Hand, dies funktioniert über die im vorangestellten Bild markierte Schaltfläche.

In den Message Body kommt ein von den Funktionen erwarteter JSON-Payload.
In den Message Body kommt ein von den Funktionen erwarteter JSON-Payload.
(Bild: AWS Deutschland)

Für unsere Zwecke muss lediglich ein von den Funktionen erwartete JSON als Message Body eingetragen werden. Alle weiteren Felder können leer bzw. auf den vorgewählten Werten gelassen werden. Nach kurzer Zeit wird der neuen Tenant nun im EKS Cluster angelegt sein, die Status von Tenant und Pods erfahren wir mithilfe der Befehle …

flux get all -n tenant-001

… und …

kubectl get pods -n tenant-001

Über die AWS-Konsole können wir auch verifizieren, dass der Nutzer im Cognito User Pool angelegt wurde.
Über die AWS-Konsole können wir auch verifizieren, dass der Nutzer im Cognito User Pool angelegt wurde.
(Bild: AWS Deutschland)

Über die AWS-Konsole können wir auch verifizieren, dass der Nutzer im Cognito User Pool angelegt wurde. Wir können nun das SNS Topic als Interface zu unserem automatisierten Onboarding-Prozess verwenden. Dazu veröffentlicht das Backend des Webformulars zur Registrierung ein Event mit dem oben beschriebenen JSON als Message. Die offizielle Amazon-SNS-Dokumentation enthält Code-Beispiele für unterschiedliche Programmiersprachen.

In dieser Artikel-Serie haben wir gesehen, wie EKS als Betriebsplattform genutzt werden kann, um eine SaaS-Lösung zu hosten. Wir haben uns damit beschäftigt, essenzielle Aspekte des Onboardings zu automatisieren und das Lifecycle Management über die Instanzen der einzelnen Tenants durch einen GitOps-Workflow abzubilden.

Mit der SaaS-Identität haben wir ein grundlegendes Konstrukt eingeführt, auf dem die Isolierung das Mandanten auf Anwendungs- und Datenhaltungsebene aufsetzen kann. Der AWS SaaS Factory Insights Hub bietet viel weiterführende Dokumentation, mit der SaaS-Anbieter ihre Lösung skalierbar, kosteneffizient und sicher gestalten können.

* Markus Kokott arbeitet als Solutions Architect bei Amazon Web Services. Er hilft Softwareherstellern dabei ihre Prozesse und Produkte zu modernisieren und damit fit für die Cloud zu machen. Technologisch interessiert sich Markus insbesondere für die Bereiche DevOps und Container.

(ID:47849425)