From 6fba3ae8d6ac65b5c06889f1b2969e620c5fed15 Mon Sep 17 00:00:00 2001 From: Arjaan Buijk Date: Wed, 2 Jun 2021 13:58:21 -0400 Subject: [PATCH 01/11] Reorganize ci pipeline Reorganize ci pipeline Reorganize ci pipeline Reorganize ci pipeline Reorganize ci pipeline Cache pip Reorganize ci pipeline cleanup cleanup cleanup cleanup cleanup All in one cicd All in one cicd All in on cicd All in on cicd aws-sit-create-infrastructure Trigger workflow Fix format Trigger workflow infra infra trigger infra trigger trigger trigger trigger infra infra trigger patch trigger infra trigger patch trigger patch trigger executive-summary executive-summary trigger executive summary trigger parameters trigger parameters trigger trigger actions cicd trigger cicd trigger cicd t cicd t c t c ta c t c t1 c t3 c tr3 c T3 c t3 t1 c T1 c tr c tr c c c t c t c c p t c t c t c t c t c t c t c t c t c c c t c t c t c t c t action_server (to AWS ECR) rasa_model (to S3) c train model aws-eks-namespace-test t c t c t c t c t c t c t c t c t c t c c c t c t c t c t c t c t c t c t c t c c t c c t c t c t c t c t demo Sharing session demo, up to deploy_TEST Rasa Enterprise Deploy placeholders Install rasa enterprise With action server Deploy t Deploy t Deploy t Deploy rasa model t Rasa Enterprise Health t Deploy model try deploy model deploy after wait for endpoint Run smoketest T Fix workflow t Fix fix spacy Fix smoketest t Upgrade eksctl tr Increase initialProbeDelay for rasa livenesscheck tr1 Fix smoketest tr2 Add sleep after tagging production tr3 tr4 tr5 Update README Test full pipeline w. cluster in place tr1 Production cluster production deployment prod pr1 pe1 prr1 Attempt 6 Attempt 7 sigh sigh2 sigh 3 Use only steps based skipping Doing it on job basis if very flaky and upredictable fix if Final --- .github/trigger.txt | 2 + .github/workflows/build_and_deploy.yml | 94 --- .github/workflows/cicd.yml | 450 ++++++++++++++ .github/workflows/continuous-integration.yml | 33 -- Dockerfile | 2 +- Makefile | 588 +++++++++++++++++-- README.md | 583 +++++++++++++++++- actions/actions.py | 2 +- data/nlu/nlu.yml | 1 + deploy/gcr-auth.json | 12 + deploy/values.yml | 54 ++ images/cicd.png | Bin 0 -> 17861 bytes images/financial-demo-eks-test-vpc.png | Bin 0 -> 102757 bytes requirements.txt | 6 +- scripts/patch_gcr_auth_json.py | 24 + scripts/smoketest.py | 141 +++++ scripts/wait_for_external_ip.sh | 18 + 17 files changed, 1829 insertions(+), 181 deletions(-) create mode 100644 .github/trigger.txt delete mode 100644 .github/workflows/build_and_deploy.yml create mode 100644 .github/workflows/cicd.yml delete mode 100644 .github/workflows/continuous-integration.yml create mode 100644 deploy/gcr-auth.json create mode 100644 deploy/values.yml create mode 100644 images/cicd.png create mode 100644 images/financial-demo-eks-test-vpc.png create mode 100644 scripts/patch_gcr_auth_json.py create mode 100644 scripts/smoketest.py create mode 100755 scripts/wait_for_external_ip.sh diff --git a/.github/trigger.txt b/.github/trigger.txt new file mode 100644 index 00000000..1ace9944 --- /dev/null +++ b/.github/trigger.txt @@ -0,0 +1,2 @@ +# A dummy file to trigger the workflow without making any actual change. +# Just change something arbitrary in this text, like a space . \ No newline at end of file diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml deleted file mode 100644 index 919493cc..00000000 --- a/.github/workflows/build_and_deploy.yml +++ /dev/null @@ -1,94 +0,0 @@ - -name: Build and Deploy -on: [pull_request] - -jobs: - lint-testing: - name: Code Formatting Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 - with: - python-version: 3.7 - - name: Install dependencies - run: | - python -m pip install -U pip - pip install -r requirements-dev.txt - - name: Code Formatting Tests - working-directory: ${{ github.workspace }} - run: | - make lint - type-testing: - name: Type Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 - with: - python-version: 3.7 - - name: Install dependencies - run: | - python -m pip install -U pip - pip install -r requirements-dev.txt - - name: Type Checking - working-directory: ${{ github.workspace }} - run: | - make types - unit-testing: - name: Unit Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 - with: - python-version: 3.7 - - name: Install dependencies - run: | - python -m pip install -U pip - pip install -r requirements-dev.txt - - name: Unit Tests - working-directory: ${{ github.workspace }} - run: | - pytest . - - training-testing: - name: Training and Testing - runs-on: ubuntu-latest - needs: [lint-testing, type-testing] - services: - # Label used to access the service container - duckling: - image: rasa/duckling - ports: - # Maps port 8000 on service container to port 8000 on host VM - - 8000:8000 - steps: - - uses: actions/checkout@v1 - - id: files - uses: jitterbit/get-changed-files@v1 - - name: set_training - if: | - contains( steps.files.outputs.all, 'data/' ) - || contains( steps.files.outputs.all, 'config.yml' ) - || contains( steps.files.outputs.all, 'domain.yml' ) - run: echo "RUN_TRAINING=true" >> $GITHUB_ENV - - name: Rasa Train and Test GitHub Action - if: env.RUN_TRAINING == 'true' - uses: RasaHQ/rasa-train-test-gha@main - with: - rasa_version: '2.4.0-spacy-en' - test_type: all - data_validate: true - cross_validation: true - publish_summary: true - github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Upload model - if: github.ref == 'refs/heads/main' - uses: actions/upload-artifact@main - with: - name: model - path: models diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 00000000..44b9b469 --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,450 @@ + +name: financial-demo cicd + +on: + push: + paths: + - 'data/**' + - 'config.yml' + - 'domain.yml' + - 'actions/**' + - '.github/trigger.txt' + +env: + # Keep these values in sync with the values in the Makefile + AWS_REGION: us-west-2 + AWS_ECR_URI: 024629701212.dkr.ecr.us-west-2.amazonaws.com + AWS_ECR_REPOSITORY: financial-demo + AWS_S3_BUCKET_NAME: rasa-financial-demo + + # Notes: + # (-) rasa & rasa-sdk versions are extracted from `requirements.txt` + # (-) check https://rasa.com/docs/rasa-x/changelog/compatibility-matrix/ + RASA_ENTERPRISE_VERSION: "0.38.1" + +jobs: + params: + name: params + runs-on: ubuntu-latest + # Map step outputs to job outputs, for use by downstream jobs + outputs: + git_branch: ${{ steps.git.outputs.git_branch }} + do_deploy_to_prod_cluster: ${{ steps.git.outputs.do_deploy_to_prod_cluster }} + + rasa_enterprise_version: ${{ steps.versions.outputs.rasa_enterprise_version }} + rasa_version: ${{ steps.versions.outputs.rasa_version }} + rasa_sdk_version: ${{ steps.versions.outputs.rasa_sdk_version }} + + do_action_server: ${{ steps.action_server.outputs.do_action_server }} + + do_training: ${{ steps.rasa_model.outputs.do_training }} + + aws_region: ${{ steps.aws.outputs.aws_region }} + aws_s3: ${{ steps.aws.outputs.aws_s3 }} + + do_create_test_cluster: ${{ steps.aws.outputs.do_create_test_cluster }} + + steps: + - name: git + id: git + run: | + echo $GITHUB_REF + git_branch=$(echo ${GITHUB_REF##*/}) + + echo "::set-output name=git_branch::$git_branch" + + # TODO: Change after merge !! + if [[ $git_branch == "infrastructure-as-code" ]] + then + echo "::set-output name=do_deploy_to_prod_cluster::true" + else + echo "::set-output name=do_deploy_to_prod_cluster::false" + fi + + - name: checkout + uses: actions/checkout@v2 + + - name: files + id: files + uses: jitterbit/get-changed-files@v1 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: versions + id: versions + run: | + rasa_version=$( cat requirements.txt | grep 'rasa\[spacy\]' | cut -d'=' -f 3 )-spacy-en + rasa_sdk_version=$( cat requirements.txt | grep 'rasa-sdk' | cut -d'=' -f 3 ) + + echo "::set-output name=rasa_enterprise_version::${{ env.RASA_ENTERPRISE_VERSION }}" + echo "::set-output name=rasa_version::$rasa_version" + echo "::set-output name=rasa_sdk_version::$rasa_sdk_version" + + - name: action_server + id: action_server + run: | + if [[ "${{ steps.files.outputs.all }}" == *"actions/"* ]] + then + echo "::set-output name=do_action_server::true" + else + echo "::set-output name=do_action_server::false" + fi + + - name: rasa_model + id: rasa_model + run: | + if [[ "${{ steps.files.outputs.all }}" == *"data/"* ]] || \ + [[ "${{ steps.files.outputs.all }}" == *"config.yml"* ]] || \ + [[ "${{ steps.files.outputs.all }}" == *"domain.yml"* ]] + then + echo "::set-output name=do_training::true" + else + echo "::set-output name=do_training::false" + fi + + - name: aws + id: aws + run: | + echo "::set-output name=aws_region::${{ env.AWS_REGION }}" + echo "::set-output name=aws_s3::${{ env.AWS_S3_BUCKET_NAME }}" + + if [[ $(make aws-eks-cluster-exists) = True ]] + then + echo "::set-output name=do_create_test_cluster::false" + else + echo "::set-output name=do_create_test_cluster::true" + fi + + + + params_summary: + name: params_summary + runs-on: ubuntu-latest + needs: [params] + steps: + - name: params_summary + run: | + echo git_branch: ${{ needs.params.outputs.git_branch }} + echo do_deploy_to_prod_cluster: ${{ needs.params.outputs.do_deploy_to_prod_cluster }} + + echo rasa_enterprise_version : ${{ needs.params.outputs.rasa_enterprise_version }} + echo rasa_version : ${{ needs.params.outputs.rasa_version }} + echo rasa_sdk_version : ${{ needs.params.outputs.rasa_sdk_version }} + + echo do_action_server: ${{ needs.params.outputs.do_action_server }} + + echo do_training: ${{ needs.params.outputs.do_training }} + + echo aws_region: ${{ needs.params.outputs.aws_region }} + echo aws_s3: ${{ needs.params.outputs.aws_s3 }} + + echo do_create_test_cluster: ${{ needs.params.outputs.do_create_test_cluster }} +# +# - name: Dump GitHub context +# env: +# GITHUB_CONTEXT: ${{ toJSON(github) }} +# run: | +# echo "$GITHUB_CONTEXT" +# exit 1 + + + action_server: + name: action_server (to AWS ECR) + runs-on: ubuntu-latest + needs: [params, params_summary] + steps: + - name: checkout + if: needs.params.outputs.do_action_server == 'true' + uses: actions/checkout@v2 + + - name: Set up Python 3.7 + if: needs.params.outputs.do_action_server == 'true' + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Cache pip + if: needs.params.outputs.do_action_server == 'true' + # see: https://docs.github.com/en/actions/guides/building-and-testing-python#caching-dependencies + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + + - name: Configure AWS Credentials + if: needs.params.outputs.do_action_server == 'true' + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Do it all + if: needs.params.outputs.do_action_server == 'true' + run: | + python -m pip install -U pip + pip install -r requirements-dev.txt + make lint + make types + make test + make docker-build + make aws-ecr-docker-login + make docker-push + + rasa_model: + name: rasa_model (to AWS S3) + runs-on: ubuntu-latest + needs: [params, params_summary] + services: + # Label used to access the service container + duckling: + image: rasa/duckling + ports: + # Maps port 8000 on service container to port 8000 on host VM + - 8000:8000 + steps: + - name: checkout + if: needs.params.outputs.do_training == 'true' + uses: actions/checkout@v2 + + - name: Install dependencies + if: needs.params.outputs.do_training == 'true' + run: | + python -m pip install -U pip + pip install -r requirements.txt + python -m spacy download en_core_web_md + python -m spacy link en_core_web_md en + + - name: Configure AWS Credentials + if: needs.params.outputs.do_training == 'true' + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ needs.params.outputs.aws_region }} + + - name: Do it all + if: needs.params.outputs.do_training == 'true' + run: | + make rasa-train + make rasa-test + make aws-s3-upload-rasa-model + + aws_eks_create_test_cluster: + name: aws_eks_create_test_cluster + runs-on: ubuntu-latest + needs: [params, params_summary] + steps: + - name: checkout + if: needs.params.outputs.do_create_test_cluster == 'true' + uses: actions/checkout@v2 + + - name: Configure AWS Credentials + if: needs.params.outputs.do_create_test_cluster == 'true' + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ needs.params.outputs.aws_region }} + + - name: Do it all + if: needs.params.outputs.do_create_test_cluster == 'true' + run: | + make install-eksctl + make install-kubectl + make aws-eks-cluster-create + + deploy_to_test_cluster: + name: deploy_to_test_cluster + runs-on: ubuntu-latest + needs: [params, aws_eks_create_test_cluster, rasa_model, action_server] + # if: always() + steps: + - name: checkout + uses: actions/checkout@v2 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ needs.params.outputs.aws_region }} + + - name: install CLIs & dependencies + run: | + make install-eksctl + make install-kubectl + make install-helm + make install-jp + + - name: configure kubectl + run: | + make aws-eks-cluster-update-kubeconfig + + - name: create namespace if not exists + run: | + make aws-eks-namespace-create + + # Enable cluster to pull the private rasa enterprise image + - name: create/refresh gcr-pull-secret + env: + GCR_AUTH_JSON_PRIVATE_KEY_ID: ${{ secrets.GCR_AUTH_JSON_PRIVATE_KEY_ID }} + GCR_AUTH_JSON_PRIVATE_KEY: ${{ secrets.GCR_AUTH_JSON_PRIVATE_KEY }} + GCR_AUTH_JSON_CLIENT_EMAIL: ${{ secrets.GCR_AUTH_JSON_CLIENT_EMAIL }} + GCR_AUTH_JSON_CLIENT_ID: ${{ secrets.GCR_AUTH_JSON_CLIENT_ID }} + run: | + make pull-secret-gcr-create + + # Enable cluster to pull the private action server image + - name: create/refresh ecr-pull-secret + run: | + make pull-secret-ecr-create + + - name: Install or Upgrade Rasa Enterprise + env: + GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD: ${{ secrets.GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD }} + GLOBAL_REDIS_PASSWORD: ${{ secrets.GLOBAL_REDIS_PASSWORD }} + RABBITMQ_RABBITMQ_PASSWORD: ${{ secrets.RABBITMQ_RABBITMQ_PASSWORD }} + RASAX_INITIALUSER_USERNAME: ${{ secrets.RASAX_INITIALUSER_USERNAME }} + RASAX_INITIALUSER_PASSWORD: ${{ secrets.RASAX_INITIALUSER_PASSWORD }} + RASAX_JWTSECRET: ${{ secrets.RASAX_JWTSECRET }} + RASAX_PASSWORDSALT: ${{ secrets.RASAX_PASSWORDSALT }} + RASAX_TOKEN: ${{ secrets.RASAX_TOKEN }} + RASA_TOKEN: ${{ secrets.RASA_TOKEN }} + run: | + make rasa-enterprise-install + + - name: Check Rasa Enterprise Health + run: | + # Need to give rasa-prod container some time... + sleep 30 + make rasa-enterprise-check-health + + - name: Deploy rasa model + env: + RASAX_INITIALUSER_USERNAME: ${{ secrets.RASAX_INITIALUSER_USERNAME }} + RASAX_INITIALUSER_PASSWORD: ${{ secrets.RASAX_INITIALUSER_PASSWORD }} + + run: | + make aws-s3-download-rasa-model + make rasa-enterprise-model-delete + make rasa-enterprise-model-upload + sleep 2 + make rasa-enterprise-model-tag + # Give rasa-prod some time to unpack & load the model + sleep 30 + + - name: Smoketest + env: + RASAX_INITIALUSER_USERNAME: ${{ secrets.RASAX_INITIALUSER_USERNAME }} + RASAX_INITIALUSER_PASSWORD: ${{ secrets.RASAX_INITIALUSER_PASSWORD }} + run: | + make rasa-enterprise-smoketest + + deploy_to_prod_cluster: + name: deploy_to_prod_cluster + runs-on: ubuntu-latest + needs: [params, deploy_to_test_cluster] + env: + AWS_EKS_CLUSTER_NAME: financial-demo-production + steps: + - name: checkout + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + uses: actions/checkout@v2 + + - name: Configure AWS Credentials + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ needs.params.outputs.aws_region }} + + - name: install CLIs & dependencies + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + run: | + make install-eksctl + make install-kubectl + make install-helm + make install-jp + + - name: configure kubectl + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + run: | + make aws-eks-cluster-update-kubeconfig + + - name: create namespace if not exists + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + run: | + make aws-eks-namespace-create + + # Enable cluster to pull the private rasa enterprise image + - name: create/refresh gcr-pull-secret + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + env: + GCR_AUTH_JSON_PRIVATE_KEY_ID: ${{ secrets.GCR_AUTH_JSON_PRIVATE_KEY_ID }} + GCR_AUTH_JSON_PRIVATE_KEY: ${{ secrets.GCR_AUTH_JSON_PRIVATE_KEY }} + GCR_AUTH_JSON_CLIENT_EMAIL: ${{ secrets.GCR_AUTH_JSON_CLIENT_EMAIL }} + GCR_AUTH_JSON_CLIENT_ID: ${{ secrets.GCR_AUTH_JSON_CLIENT_ID }} + run: | + make pull-secret-gcr-create + + # Enable cluster to pull the private action server image + - name: create/refresh ecr-pull-secret + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + run: | + make pull-secret-ecr-create + + - name: Install or Upgrade Rasa Enterprise + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + env: + GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD: ${{ secrets.GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD }} + GLOBAL_REDIS_PASSWORD: ${{ secrets.GLOBAL_REDIS_PASSWORD }} + RABBITMQ_RABBITMQ_PASSWORD: ${{ secrets.RABBITMQ_RABBITMQ_PASSWORD }} + RASAX_INITIALUSER_USERNAME: ${{ secrets.RASAX_INITIALUSER_USERNAME }} + RASAX_INITIALUSER_PASSWORD: ${{ secrets.RASAX_INITIALUSER_PASSWORD }} + RASAX_JWTSECRET: ${{ secrets.RASAX_JWTSECRET }} + RASAX_PASSWORDSALT: ${{ secrets.RASAX_PASSWORDSALT }} + RASAX_TOKEN: ${{ secrets.RASAX_TOKEN }} + RASA_TOKEN: ${{ secrets.RASA_TOKEN }} + run: | + make rasa-enterprise-install + + - name: Check Rasa Enterprise Health + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + run: | + # Need to give rasa-prod container some time... + sleep 30 + make rasa-enterprise-check-health + + - name: Deploy rasa model + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + env: + RASAX_INITIALUSER_USERNAME: ${{ secrets.RASAX_INITIALUSER_USERNAME }} + RASAX_INITIALUSER_PASSWORD: ${{ secrets.RASAX_INITIALUSER_PASSWORD }} + + run: | + make aws-s3-download-rasa-model + make rasa-enterprise-model-delete + make rasa-enterprise-model-upload + sleep 2 + make rasa-enterprise-model-tag + # Give rasa-prod some time to unpack & load the model + sleep 30 + + - name: Smoketest + if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' + env: + RASAX_INITIALUSER_USERNAME: ${{ secrets.RASAX_INITIALUSER_USERNAME }} + RASAX_INITIALUSER_PASSWORD: ${{ secrets.RASAX_INITIALUSER_PASSWORD }} + run: | + make rasa-enterprise-smoketest diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml deleted file mode 100644 index feaac68e..00000000 --- a/.github/workflows/continuous-integration.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Continuous Integration -on: - push: - branches: - - main - paths: - - 'actions/**' - -jobs: - docker: - name: Build Action Server Docker Image - runs-on: ubuntu-latest - env: - DOCKERHUB_USERNAME: oakela - steps: - - name: set_tag_latest - if: | - github.ref == 'refs/heads/main' - || github.head_ref == 'refs/heads/main' - run: echo "TAG=latest" >> $GITHUB_ENV - - run: echo ${{ env.TAG }} - - name: Checkout git repository 🕝 - uses: actions/checkout@v2 - - name: Build Actions Server Image - uses: RasaHQ/rasa-action-server-gha@main - with: - actions_directory: 'actions' - requirements_file: 'actions/requirements-actions.txt' - docker_registry_login: ${{ env.DOCKERHUB_USERNAME }} - docker_registry_password: ${{ secrets.DOCKERHUB_PASSWORD }} - docker_image_name: 'rasa/financial-demo' - docker_image_tag: ${{ env.TAG }} - rasa_sdk_version: '2.4.0' diff --git a/Dockerfile b/Dockerfile index ff624c36..98f7166d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,4 +6,4 @@ USER root RUN pip install --no-cache-dir -r /app/actions/requirements-actions.txt USER 1001 -CMD ["start", "--actions", "actions", "--debug"] +CMD ["start", "--actions", "actions"] diff --git a/Makefile b/Makefile index 1088f118..826003aa 100644 --- a/Makefile +++ b/Makefile @@ -1,34 +1,40 @@ -ACTION_SERVER_DOCKERPATH ?= financial-demo:test -ACTION_SERVER_DOCKERNAME ?= financial-demo -ACTION_SERVER_PORT ?= 5056 -ACTION_SERVER_ENDPOINT_HEALTH ?= health - -help: - @echo "make" - @echo " clean" - @echo " Remove Python/build artifacts." - @echo " formatter" - @echo " Apply black formatting to code." - @echo " lint" - @echo " Lint code with flake8, and check if black formatter should be applied." - @echo " types" - @echo " Check for type errors using pytype." - @echo " test" - @echo " Run unit tests for the custom actions using pytest." - @echo " docker-build" - @echo " Builds docker image of the action server" - @echo " docker-run" - @echo " Runs the docker image of the action server created by `docker-build`" - @echo " docker-test" - @echo " Tests the health endpoint of the action server" - @echo " docker-clean-containers" - @echo " Stops & removes the containers created by `docker-run`" - @echo " docker-clean-images" - @echo " Removes the images created by `docker-build`" - @echo " docker-clean" - @echo " Runs `docker-clean-containers` and `docker-clean-images`" - @echo " docker-push" - @echo " Pushes docker images to dockerhub" +SHELL := /bin/bash + +# Extract rasa version to install from `requirements.txt` +RASA_TAG := $(shell cat requirements.txt | grep 'rasa\[spacy\]' | cut -d'=' -f 3 )-spacy-en +# Make sure to install a compatible Rasa Enterprise: +RASAX_TAG := 0.38.1 + +ACTION_SERVER_DOCKER_IMAGE_NAME := financial-demo +ACTION_SERVER_DOCKER_IMAGE_TAG := $(shell git branch --show-current) +ACTION_SERVER_DOCKER_CONTAINER_NAME := financial-demo_$(shell git branch --show-current) +ACTION_SERVER_PORT := 5056 +ACTION_SERVER_ENDPOINT_HEALTH := health + +RASA_MODEL_NAME := $(shell git branch --show-current) +RASA_MODEL_PATH := models/$(shell git branch --show-current).tar.gz + +# The CICD pipeline sets these as environment variables +# Set some defaults for when you're running locally +AWS_REGION := us-west-2 + +AWS_ECR_URI := 024629701212.dkr.ecr.us-west-2.amazonaws.com +AWS_ECR_REPOSITORY := financial-demo + +AWS_S3_BUCKET_NAME := rasa-financial-demo + +AWS_IAM_ROLE_NAME := eksClusterRole + +#AWS_EKS_VPC_STACK_NAME := eks-vpc-financial-demo-$(shell git branch --show-current) +AWS_EKS_VPC_TEMPLATE := aws/cloudformation/amazon-eks-vpc-private-subnets.yaml +AWS_EKS_KEYPAIR_NAME := findemo +AWS_EKS_CLUSTER_NAME := financial-demo-$(shell git branch --show-current) +AWS_EKS_KUBERNETES_VERSION := 1.19 + +AWS_EKS_NAMESPACE := my-namespace +AWS_EKS_RELEASE_NAME := my-release + +GIT_BRANCH_NAME := $(shell git branch --show-current) clean: find . -name '*.pyc' -exec rm -f {} + @@ -39,6 +45,36 @@ clean: rm -rf dist/ rm -rf docs/_build +install-eksctl: + curl --silent --location "https://github.com/weaveworks/eksctl/releases/download/0.51.0/eksctl_Linux_amd64.tar.gz" | tar xz -C /tmp + sudo mv /tmp/eksctl /usr/local/bin + @echo $(NEWLINE) + eksctl version + @echo $(NEWLINE) + +install-kubectl: + sudo snap install kubectl --classic + @echo $(NEWLINE) + @kubectl version --client --short + +install-helm: + sudo snap install helm --classic + @echo $(NEWLINE) + @helm version --short + +install-jp: + sudo apt-get update && sudo apt-get install jp + @echo $(NEWLINE) + @jp --version + +rasa-train: + @echo Training $(RASA_MODEL_NAME) + rasa train --fixed-model-name $(RASA_MODEL_NAME) + +rasa-test: + @echo Testing $(RASA_MODEL_PATH) + rasa test --model $(RASA_MODEL_PATH) + formatter: black actions @@ -53,22 +89,494 @@ test: pytest tests docker-build: - docker build . --file Dockerfile --tag $(ACTION_SERVER_DOCKERPATH) + docker build . --file Dockerfile --tag $(AWS_ECR_URI)/$(ACTION_SERVER_DOCKER_IMAGE_NAME):$(ACTION_SERVER_DOCKER_IMAGE_TAG) docker-run: - docker run -d -p $(ACTION_SERVER_PORT):5055 --name $(ACTION_SERVER_DOCKERNAME) $(ACTION_SERVER_DOCKERPATH) + docker run -d -p $(ACTION_SERVER_PORT):5055 --name $(ACTION_SERVER_DOCKER_CONTAINER_NAME) $(AWS_ECR_URI)/$(ACTION_SERVER_DOCKER_IMAGE_NAME):$(ACTION_SERVER_DOCKER_IMAGE_TAG) docker-test: curl http://localhost:$(ACTION_SERVER_PORT)/$(ACTION_SERVER_ENDPOINT_HEALTH) + @echo $(NEWLINE) -docker-clean-containers: - docker stop $(ACTION_SERVER_DOCKERNAME) - docker rm $(ACTION_SERVER_DOCKERNAME) +docker-stop: + docker stop $(ACTION_SERVER_DOCKER_CONTAINER_NAME) -docker-clean-images: - docker rmi $(ACTION_SERVER_DOCKERPATH) +docker-clean-container: + docker stop $(ACTION_SERVER_DOCKER_CONTAINER_NAME) + docker rm $(ACTION_SERVER_DOCKER_CONTAINER_NAME) + +docker-clean-image: + docker rmi $(AWS_ECR_URI)/$(ACTION_SERVER_DOCKER_IMAGE_NAME):$(ACTION_SERVER_DOCKER_IMAGE_TAG) -docker-clean: docker-clean-containers docker-clean-images +docker-clean: docker-clean-container docker-clean-image +docker-login: + @echo docker registry: $(DOCKER_REGISTRY) + @echo docker user: $(DOCKER_USER) + @echo $(DOCKER_PW) | docker login $(DOCKER_REGISTRY) -u $(DOCKER_USER) --password-stdin + docker-push: - docker image push $(ACTION_SERVER_DOCKERPATH) \ No newline at end of file + @echo pushing image: $(AWS_ECR_URI)/$(ACTION_SERVER_DOCKER_IMAGE_NAME):$(ACTION_SERVER_DOCKER_IMAGE_TAG) + docker image push $(AWS_ECR_URI)/$(ACTION_SERVER_DOCKER_IMAGE_NAME):$(ACTION_SERVER_DOCKER_IMAGE_TAG) + +aws-iam-role-get-Arn: + @aws iam get-role \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --role-name $(AWS_IAM_ROLE_NAME) \ + --query "Role.Arn" + +aws-ecr-docker-login: + @$(eval AWS_ECR_URI := $(shell make aws-ecr-get-repositoryUri)) + @echo logging into AWS ECR registry: $(AWS_ECR_URI) + aws ecr get-login-password --region $(AWS_REGION) | docker login --username AWS --password-stdin $(AWS_ECR_URI) + +aws-ecr-create-repository: + @echo creating ecr repository: $(AWS_ECR_REPOSITORY) + @echo $(NEWLINE) + aws ecr create-repository \ + --repository-name $(AWS_ECR_REPOSITORY) \ + --region $(AWS_REGION) + +aws-ecr-get-authorization-token: + @aws ecr get-authorization-token \ + --no-paginate \ + --output text \ + --region=$(AWS_REGION) \ + --query authorizationData[].authorizationToken | base64 -d | cut -d: -f2 + +aws-ecr-get-repositoryUri: + @aws ecr describe-repositories \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --repository-names $(AWS_ECR_REPOSITORY) \ + --query "repositories[].repositoryUri" + +aws-s3-create-bucket: + @echo creating s3 bucket: $(AWS_S3_BUCKET_NAME) + @echo $(NEWLINE) + aws s3api create-bucket \ + --bucket $(AWS_S3_BUCKET_NAME) \ + --region $(AWS_REGION) \ + --create-bucket-configuration LocationConstraint=$(AWS_REGION) + +aws-s3-delete-bucket: + @echo deleting s3 bucket: $(AWS_S3_BUCKET_NAME) + aws s3 rb s3://$(AWS_S3_BUCKET_NAME) --force + +aws-s3-upload-rasa-model: + @echo uploading $(RASA_MODEL_PATH) to s3://$(AWS_S3_BUCKET_NAME)/$(RASA_MODEL_PATH) + aws s3 cp $(RASA_MODEL_PATH) s3://$(AWS_S3_BUCKET_NAME)/$(RASA_MODEL_PATH) + +aws-s3-download-rasa-model: + @echo downloading $(RASA_MODEL_PATH) from s3://$(AWS_S3_BUCKET_NAME)/$(RASA_MODEL_PATH) + aws s3 cp s3://$(AWS_S3_BUCKET_NAME)/$(RASA_MODEL_PATH) $(RASA_MODEL_PATH) + +aws-cloudformation-eks-get-SubnetsPrivate: + @aws cloudformation describe-stacks \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --stack-name eksctl-$(AWS_EKS_CLUSTER_NAME)-cluster \ + --query "Stacks[].Outputs[?OutputKey=='SubnetsPrivate'].OutputValue" + +aws-cloudformation-eks-get-SubnetsPublic: + @aws cloudformation describe-stacks \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --stack-name eksctl-$(AWS_EKS_CLUSTER_NAME)-cluster \ + --query "Stacks[].Outputs[?OutputKey=='SubnetsPublic'].OutputValue" + +aws-cloudformation-eks-get-ServiceRoleARN: + @aws cloudformation describe-stacks \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --stack-name eksctl-$(AWS_EKS_CLUSTER_NAME)-cluster \ + --query "Stacks[].Outputs[?OutputKey=='ServiceRoleARN'].OutputValue" + +aws-cloudformation-eks-get-Endpoint: + @aws cloudformation describe-stacks \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --stack-name eksctl-$(AWS_EKS_CLUSTER_NAME)-cluster \ + --query "Stacks[].Outputs[?OutputKey=='Endpoint'].OutputValue" + +aws-cloudformation-eks-get-SharedNodeSecurityGroup: + @aws cloudformation describe-stacks \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --stack-name eksctl-$(AWS_EKS_CLUSTER_NAME)-cluster \ + --query "Stacks[].Outputs[?OutputKey=='SharedNodeSecurityGroup'].OutputValue" + +aws-cloudformation-eks-get-VPC: + @aws cloudformation describe-stacks \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --stack-name eksctl-$(AWS_EKS_CLUSTER_NAME)-cluster \ + --query "Stacks[].Outputs[?OutputKey=='VPC'].OutputValue" + +aws-cloudformation-eks-get-ClusterSecurityGroupId: + @aws cloudformation describe-stacks \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --stack-name eksctl-$(AWS_EKS_CLUSTER_NAME)-cluster \ + --query "Stacks[].Outputs[?OutputKey=='ClusterSecurityGroupId'].OutputValue" + +aws-cloudformation-eks-get-CertificateAuthorityData: + @aws cloudformation describe-stacks \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --stack-name eksctl-$(AWS_EKS_CLUSTER_NAME)-cluster \ + --query "Stacks[].Outputs[?OutputKey=='CertificateAuthorityData'].OutputValue" + +aws-cloudformation-eks-get-SecurityGroup: + @aws cloudformation describe-stacks \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --stack-name eksctl-$(AWS_EKS_CLUSTER_NAME)-cluster \ + --query "Stacks[].Outputs[?OutputKey=='SecurityGroup'].OutputValue" + +aws-cloudformation-eks-get-ARN: + @aws cloudformation describe-stacks \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --stack-name eksctl-$(AWS_EKS_CLUSTER_NAME)-cluster \ + --query "Stacks[].Outputs[?OutputKey=='ARN'].OutputValue" + +aws-eks-cluster-create: + eksctl create cluster \ + --name $(AWS_EKS_CLUSTER_NAME) \ + --region $(AWS_REGION) \ + --version $(AWS_EKS_KUBERNETES_VERSION) \ + --with-oidc \ + --ssh-access \ + --ssh-public-key $(AWS_EKS_KEYPAIR_NAME) \ + --managed + +aws-eks-cluster-list-all: + eksctl get cluster + +aws-eks-cluster-info: + kubectl cluster-info + +# https://docs.aws.amazon.com/eks/latest/userguide/delete-cluster.html +aws-eks-cluster-delete: + @[ "${GIT_BRANCH_NAME}" != "main" ] || ( echo ">> You are on main branch. Deletion via Makefile not allowed"; exit 1 ) + eksctl delete cluster \ + --name $(AWS_EKS_CLUSTER_NAME) \ + --region $(AWS_REGION) + @echo $(NEWLINE) + @echo See AWS CloudFormation Console. The stack deletion is still in progress... + +aws-eks-cluster-exists: + @aws eks list-clusters \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --query "contains(clusters[*], '$(AWS_EKS_CLUSTER_NAME)')" + +aws-eks-cluster-describe: + @aws eks describe-cluster \ + --no-paginate \ + --region $(AWS_REGION) \ + --name $(AWS_EKS_CLUSTER_NAME) + +aws-eks-cluster-describe-stacks: + @eksctl utils describe-stacks \ + --region $(AWS_REGION) \ + --cluster $(AWS_EKS_CLUSTER_NAME) + +aws-eks-cluster-status: + @aws eks describe-cluster \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --name $(AWS_EKS_CLUSTER_NAME) \ + --query "cluster.status" + +aws-eks-cluster-get-endpoint: + @aws eks describe-cluster \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --name $(AWS_EKS_CLUSTER_NAME) \ + --query "cluster.endpoint" + +aws-eks-cluster-get-certificateAuthority: + @aws eks describe-cluster \ + --no-paginate \ + --output text \ + --region $(AWS_REGION) \ + --name $(AWS_EKS_CLUSTER_NAME) \ + --query "cluster.certificateAuthority" + +aws-eks-cluster-update-kubeconfig: + @echo Updating kubeconfig for AWS EKS cluster with name: $(AWS_EKS_CLUSTER_NAME) + @echo $(NEWLINE) + aws eks update-kubeconfig \ + --region $(AWS_REGION) \ + --name $(AWS_EKS_CLUSTER_NAME) + +aws-eks-namespace-create: + kubectl create namespace $(AWS_EKS_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - + +aws-eks-namespace-delete: + @[ "${GIT_BRANCH_NAME}" != "main" ] || ( echo ">> You are on main branch. Deletion via Makefile not allowed"; exit 1 ) + kubectl delete namespace $(AWS_EKS_NAMESPACE) + + +kubectl-config-current-context: + kubectl config current-context + +pull-secret-gcr-create: + @[ "${GCR_AUTH_JSON_PRIVATE_KEY_ID}" ] || ( echo ">> GCR_AUTH_JSON_PRIVATE_KEY_ID is not set"; exit 1 ) + @[ "${GCR_AUTH_JSON_PRIVATE_KEY}" ] || ( echo ">> GCR_AUTH_JSON_PRIVATE_KEY is not set"; exit 1 ) + @[ "${GCR_AUTH_JSON_CLIENT_EMAIL}" ] || ( echo ">> GCR_AUTH_JSON_CLIENT_EMAIL is not set"; exit 1 ) + @[ "${GCR_AUTH_JSON_CLIENT_ID}" ] || ( echo ">> GCR_AUTH_JSON_CLIENT_ID is not set"; exit 1 ) + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + delete secret gcr-pull-secret \ + --ignore-not-found + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + create secret docker-registry gcr-pull-secret \ + --docker-server=gcr.io \ + --docker-username=_json_key \ + --docker-password='$(shell python ./scripts/patch_gcr_auth_json.py)' +# @kubectl --namespace $(AWS_EKS_NAMESPACE) \ +# create secret docker-registry gcr-pull-secret \ +# --docker-server=gcr.io \ +# --docker-username=_json_key \ +# --docker-password='$(shell cat ./secret/gcr-auth.json)' + +pull-secret-gcr-delete: + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + delete secret gcr-pull-secret \ + --ignore-not-found + +pull-secret-ecr-create: + @$(eval AWS_ECR_TOKEN := $(shell make aws-ecr-get-authorization-token)) + @$(eval AWS_ECR_URI := $(shell make aws-ecr-get-repositoryUri)) + + @echo "Creating pull secret for Action Server (in ECR)" + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + delete secret ecr-pull-secret \ + --ignore-not-found + + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + create secret docker-registry ecr-pull-secret \ + --docker-server=https://$(AWS_ECR_URI) \ + --docker-username=AWS \ + --docker-password="$(AWS_ECR_TOKEN)" + +pull-secret-ecr-delete: + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + delete secret ecr-pull-secret \ + --ignore-not-found + +rasa-enterprise-install: + @[ "${GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD}" ] || ( echo ">> GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD is not set"; exit 1 ) + @[ "${GLOBAL_REDIS_PASSWORD}" ] || ( echo ">> GLOBAL_REDIS_PASSWORD is not set"; exit 1 ) + @[ "${RABBITMQ_RABBITMQ_PASSWORD}" ] || ( echo ">> RABBITMQ_RABBITMQ_PASSWORD is not set"; exit 1 ) + @[ "${RASAX_INITIALUSER_USERNAME}" ] || ( echo ">> RASAX_INITIALUSER_USERNAME is not set"; exit 1 ) + @[ "${RASAX_INITIALUSER_PASSWORD}" ] || ( echo ">> RASAX_INITIALUSER_PASSWORD is not set"; exit 1 ) + @[ "${RASAX_JWTSECRET}" ] || ( echo ">> RASAX_JWTSECRET is not set"; exit 1 ) + @[ "${RASAX_PASSWORDSALT}" ] || ( echo ">> RASAX_PASSWORDSALT is not set"; exit 1 ) + @[ "${RASAX_TOKEN}" ] || ( echo ">> RASAX_TOKEN is not set"; exit 1 ) + @[ "${RASA_TOKEN}" ] || ( echo ">> RASA_TOKEN is not set"; exit 1 ) + + @[ "${RASAX_TAG}" ] || ( echo ">> RASAX_TAG is not set"; exit 1 ) + @[ "${RASA_TAG}" ] || ( echo ">> RASA_TAG is not set"; exit 1 ) + @[ "${AWS_ECR_URI}" ] || ( echo ">> AWS_ECR_URI is not set"; exit 1 ) + @[ "${ACTION_SERVER_DOCKER_IMAGE_NAME}" ] || ( echo ">> ACTION_SERVER_DOCKER_IMAGE_NAME is not set"; exit 1 ) + @[ "${ACTION_SERVER_DOCKER_IMAGE_TAG}" ] || ( echo ">> ACTION_SERVER_DOCKER_IMAGE_TAG is not set"; exit 1 ) + + helm repo add rasa-x https://rasahq.github.io/rasa-x-helm + helm repo update + + @echo $(NEWLINE) + @echo Installing or Upgrading Rasa Enterprise with: + @echo - RASAX_TAG: $(RASAX_TAG) + @echo - RASA_TAG: $(RASA_TAG) + @echo - APP_NAME: $(AWS_ECR_URI)/$(ACTION_SERVER_DOCKER_IMAGE_NAME) + @echo - APP_TAG: $(ACTION_SERVER_DOCKER_IMAGE_TAG) + @echo $(NEWLINE) + @helm --namespace $(AWS_EKS_NAMESPACE) \ + upgrade \ + --install \ + --values ./deploy/values.yml \ + --set rasax.tag=$(RASAX_TAG) \ + --set rasax.initialUser.username=$(RASAX_INITIALUSER_USERNAME) \ + --set rasax.initialUser.password=$(RASAX_INITIALUSER_PASSWORD) \ + --set rasax.passwordSalt=$(RASAX_PASSWORDSALT) \ + --set rasax.token=$(RASAX_TOKEN) \ + --set rasax.jwtSecret=$(RASAX_JWTSECRET) \ + --set rasa.tag=$(RASA_TAG) \ + --set rasa.token=$(RASA_TOKEN) \ + --set rabbitmq.rabbitmq.password=$(RABBITMQ_RABBITMQ_PASSWORD) \ + --set global.postgresql.postgresqlPassword=$(GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD) \ + --set global.redis.password=$(GLOBAL_REDIS_PASSWORD) \ + --set app.name=$(AWS_ECR_URI)/$(ACTION_SERVER_DOCKER_IMAGE_NAME) \ + --set app.tag=$(ACTION_SERVER_DOCKER_IMAGE_TAG) \ + $(AWS_EKS_RELEASE_NAME) \ + rasa-x/rasa-x + + @echo $(NEWLINE) + @echo Waiting until all deployments are AVAILABLE + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + wait \ + --for=condition=available \ + --timeout=20m \ + --all \ + deployment + + @echo $(NEWLINE) + @echo Waiting until all pods are READY + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + wait \ + --for=condition=ready \ + --timeout=20m \ + --all \ + pod + + @echo $(NEWLINE) + @echo Waiting for external IP assignment + @./scripts/wait_for_external_ip.sh $(AWS_EKS_NAMESPACE) $(AWS_EKS_RELEASE_NAME) + +rasa-enterprise-uninstall: + @echo Uninstalling Rasa Enterprise release $(AWS_EKS_RELEASE_NAME). + @echo $(NEWLINE) + @helm --namespace $(AWS_EKS_NAMESPACE) \ + uninstall $(AWS_EKS_RELEASE_NAME) + +rasa-enterprise-check-health: + @$(eval URL := $(shell make rasa-enterprise-get-base-url)/api/health) + @$(eval PRODUCTION_STATUS := $(shell curl --silent --request GET --url $(URL) | jp production.status)) + @$(eval WORKER_STATUS := $(shell curl --silent --request GET --url $(URL) | jp worker.status)) + @$(eval DB_MIGRATION_STATUS := $(shell curl --silent --request GET --url $(URL) | jp database_migration.status)) + + @echo Checking health at: $(URL) + @curl --silent --request GET --url $(URL) | json_pp + + @[ "${PRODUCTION_STATUS}" = "200" ] || ( echo ">> production.status not ok"; exit 1 ) + + @[ "${WORKER_STATUS}" = "200" ] || ( echo ">> worker.status not ok"; exit 1 ) + + @[ "${DB_MIGRATION_STATUS}" = "completed" ] || ( echo ">> database_migration.status not completed (not fatal)" ) + +rasa-enterprise-get-pods: + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + get pods + +rasa-enterprise-get-secrets-postgresql: + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + get secret $(AWS_EKS_RELEASE_NAME)-postgresql -o yaml | \ + awk -F ': ' '/password/{print $2}' | base64 -d + +rasa-enterprise-get-secrets-redis: + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + get secret $(AWS_EKS_RELEASE_NAME)-redis -o yaml | \ + awk -F ': ' '/password/{print $2}' | base64 -d + +rasa-enterprise-get-secrets-rabbit: + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + get secret $(AWS_EKS_RELEASE_NAME)-rabbit -o yaml | \ + awk -F ': ' '/password/{print $2}' | base64 -d + + +rasa-enterprise-get-login: + @./scripts/wait_for_external_ip.sh $(AWS_EKS_NAMESPACE) $(AWS_EKS_RELEASE_NAME) 1 + +rasa-enterprise-get-access-token: + @$(eval URL := $(shell make rasa-enterprise-get-base-url)/api/auth) + @curl --silent --request POST --url $(URL) \ + --header 'Content-Type: application/json' \ + --data '{ "username": "$(RASAX_INITIALUSER_USERNAME)", "password": "$(RASAX_INITIALUSER_PASSWORD)" }' \ + | jp --unquoted access_token + +rasa-enterprise-get-chat-token: + @$(eval URL := $(shell make rasa-enterprise-get-base-url)/api/chatToken) + @$(eval ACCESS_TOKEN := $(shell make rasa-enterprise-get-access-token)) + @curl --silent --request GET --url $(URL) \ + --header 'Authorization: Bearer $(ACCESS_TOKEN)' \ + | jp --unquoted chat_token + +rasa-enterprise-model-upload: + @$(eval URL := $(shell make rasa-enterprise-get-base-url)/api/projects/default/models) + @$(eval ACCESS_TOKEN := $(shell make rasa-enterprise-get-access-token)) + @$(eval CURL_OUTPUT_FILE := /tmp/curl_output_$(shell date +'%y%m%d_%H%M%S').txt) + + @echo "Uploading model:" + @echo "- Model: $(RASA_MODEL_PATH)" + @echo "- URL: $(URL)" + @curl -k \ + --progress-bar \ + --output $(CURL_OUTPUT_FILE) \ + --request POST \ + --url "$(URL)" \ + -F "model=@$(RASA_MODEL_PATH)" \ + -H "Authorization: Bearer $(ACCESS_TOKEN)" + + @cat $(CURL_OUTPUT_FILE) | json_pp + + @echo $(NEWLINE) + @if [[ $$(cat ${CURL_OUTPUT_FILE} | jp message) == "null" ]]; then \ + echo "Upload was successful"; \ + rm -f ${CURL_OUTPUT_FILE}; \ + else \ + echo "Error: $$(cat ${CURL_OUTPUT_FILE} | jp message)"; \ + rm -f ${CURL_OUTPUT_FILE}; \ + exit 1; \ + fi + + +rasa-enterprise-model-tag: + @$(eval URL := $(shell make rasa-enterprise-get-base-url)/api/projects/default/models/$(RASA_MODEL_NAME)/tags/production) + @$(eval ACCESS_TOKEN := $(shell make rasa-enterprise-get-access-token)) + + @echo "Tagging as production the model:" + @echo "- Model: $(RASA_MODEL_NAME)" + @echo "- URL: $(URL)" + + @curl \ + --request PUT \ + --url "$(URL)" \ + -H "Authorization: Bearer $(ACCESS_TOKEN)" + +rasa-enterprise-model-delete: + @$(eval URL := $(shell make rasa-enterprise-get-base-url)/api/projects/default/models/$(RASA_MODEL_NAME)) + @$(eval ACCESS_TOKEN := $(shell make rasa-enterprise-get-access-token)) + + @echo "Deleting the model:" + @echo "- Model: $(RASA_MODEL_NAME)" + @echo "- URL: $(URL)" + + @curl \ + --request DELETE \ + --url "$(URL)" \ + -H "Authorization: Bearer $(ACCESS_TOKEN)" + +rasa-enterprise-smoketest: + @$(eval URL := $(shell make rasa-enterprise-get-base-url)) + export BASE_URL=$(URL); \ + python ./scripts/smoketest.py + +rasa-enterprise-get-base-url: + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + get service $(AWS_EKS_RELEASE_NAME)-rasa-x-nginx \ + --output jsonpath='{.status.loadBalancer.ingress[0].hostname}' | \ + awk '{v="http://"$$1":80"; print v}' + +rasa-enterprise-get-loadbalancer-hostname: + @kubectl --namespace $(AWS_EKS_NAMESPACE) \ + get service $(AWS_EKS_RELEASE_NAME)-rasa-x-nginx \ + --output jsonpath='{.status.loadBalancer.ingress[0].hostname}' \ No newline at end of file diff --git a/README.md b/README.md index 7c5509f7..7247ee6e 100644 --- a/README.md +++ b/README.md @@ -258,21 +258,590 @@ As part of the deployment, you'll need to set up [git integration](https://rasa. You will need to have docker installed in order to build the action server image. If you haven't made any changes to the action code, you can also use the [public image on Dockerhub](https://hub.docker.com/r/rasa/financial-demo) instead of building it yourself. +Build & tag the image: -See the Dockerfile for what is included in the action server image, +```bash +export ACTION_SERVER_DOCKERPATH=/: +make docker-build +``` + +Run the action server container: + +```bash +make docker-run +``` -To build the image: +Perform a smoke test on the health endpoint: ```bash -docker build . -t : +make docker-test ``` -To test the container locally, you can then run the action server container with: +Once you have confirmed that the container works as it should, push the container image to a registry: ```bash -docker run -p 5055:5055 : +# login to a container registry with your credentials +docker login + +# check the registry logged into +docker system info | grep Registry + +# push the action server image +make docker-push ``` -Once you have confirmed that the container works as it should, you can push the container image to a registry with `docker push` +# CI/CD + +## Summary + +A CI/CD pipeline is used to test, build and deploy the financial-demo bot to AWS EKS: + +The pipeline uses GitHub Actions, defined in `.github/workflows/cicd.yml`. It includes these jobs: + +![](images/cicd.png) + +**params** + +- Defines parameters for use by downstream jobs + +**params_summary** + +- Prints the value of the parameters. + +**action_server** + +- Builds & Tests the docker image of the action server with tag: `` +- Uploads the docker image to the AWS ECR repository: `financial-demo` + +**rasa_model** + +- Trains & Tests the rasa model with name: `models/.tar.gz +- Uploads the trained model to the AWS S3 bucket: `rasa-financial-demo` + +**aws_eks_create_test_cluster** + +- If not existing yet, create an AWS EKS cluster with name: `financial-demo-` + +**deploy_to_test_cluster** + +- Installs/Updates Rasa Enterprise, with the docker image created by the **action_server** job. +- Deploys the rasa model, trained by the **rasa_model** job. +- Performs smoke tests to ensure basic operation is all OK. + +**deploy_to_prod_cluster** + +- Runs when pushing to the `main` branch, and all previous steps are successful. +- Installs/Updates Rasa Enterprise, with the docker image created by the **action_server** job. +- Deploys the rasa model, trained by the **rasa_model** job. +- Performs smoke tests to ensure basic operation is all OK. + + + +## GitHub Secrets + +In your GitHub repository, go to `Settings > Secrets`, and add these `New repository secrets` . + +When entering the value, do not use quotes. + +#### AWS IAM User API Keys: + +To allow the GitHub actions to configure the aws cli, create IAM User API Keys as described below, and add them as GitHub Secrets to the repo: + +- AWS_ACCESS_KEY_ID = `Access key ID` +- AWS_SECRET_ACCESS_KEY = `Secret access key` + +#### AWS Elastic IP: + +Create an Elastic IP as described below, and add it as a GitHub Secret to the repo: + +- AWS_ELASTIC_IP = `PublicIp` + +#### Rasa Enterprise License: + +To allow the GitHub actions to define a pull secret for the private GCR repo, get the private values from your Rasa Enterprise license file ([docs](https://rasa.com/docs/rasa-x/installation-and-setup/install/helm-chart#5-configure-rasa-x-image)) and add them as GitHub Secrets to the repo: + +- GCR_AUTH_JSON_PRIVATE_KEY_ID = `private_key_id` +- GCR_AUTH_JSON_PRIVATE_KEY = `private_key` +- GCR_AUTH_JSON_CLIENT_EMAIL = `client_email` +- GCR_AUTH_JSON_CLIENT_ID = `client_id` + +#### Helm chart Credentials + +To allow the GitHub actions to use safe_credentials in the `values.yml` ([docs](https://rasa.com/docs/rasa-x/installation-and-setup/install/helm-chart#3-configure-credentials)), add following GitHub Secrets to the repo, replacing each `` with a different alphanumeric string, and choose a `` for the initial user. + +*(Please use **safe credentials** to avoid data breaches)* + +- GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD = `` +- GLOBAL_REDIS_PASSWORD = `` +- RABBITMQ_RABBITMQ_PASSWORD = `` +- RASAX_INITIALUSER_USERNAME = `` +- RASAX_INITIALUSER_PASSWORD = `` +- RASAX_JWTSECRET = `` +- RASAX_PASSWORDSALT = `` +- GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD = `` +- GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD = `` + + + +## AWS Preparation + +The CI/CD pipeline of financial-demo uses AWS for all the storage & compute resources. + +After cloning or forking the financial-demo GitHub repository you must set up the following items before the pipeline can run. + +### IAM User API Keys + +The CI/CD pipeline uses the [aws cli](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html#cliv2-linux-install). + +The [aws cli](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html#cliv2-linux-install) needs a set of IAM User API keys for authentication & authorization: + +- In your AWS Console, go to IAM dashboard to create a new set of API keys: + + - Click on Users + + - Click on Add user + + - User name = findemo *(The actual name is not important, we will never use this name directly)* + + Choose "**programmatic access**." This allows you to use the aws cli to interact with AWS. + + - Click on Next: Permissions + + - Click on *Attach existing policies directly* + + For IAM access, you can choose “**AdministratorAccess**”, or limit access to only what is needed by the CD pipeline. + + - Click on Next: Tags + + - Click on Next: Review + + - Click on Create user + + - Store in a safe location: `Access key ID` & `Secret access key` + + +### SSH Key Pair + +To be able to SSH into the EC2 worker nodes of the EKS cluster, you need an SSH Key Pair + +- In your AWS Console, go to **EC2 > Key Pairs**, and create a Key Pair with the name `findemo`, and download the file `findemo.pem` which contains the private SSH key. + **Note that the name `findemo` is important, since it is used by the CI/CD pipeline when the cluster is created.** + +### Local AWS CLI + +Before the CI/CD pipeline can run, you will use the AWS CLI locally to create some items, so also install & configure the CLI locally. + +#### Install AWS CLI v2 + +See the [installation instructions](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html). + +#### Configure your AWS CLI + +```bash +# AWS CLI version 2.1.26 or later +aws --version + +# Configure AWS CLI +aws configure +AWS Access Key ID [None]: ----- # See above: IAM User API Keys +AWS Secret Access Key [None]: ------- # See above: IAM User API Keys +Default region name [None]: us-west-2 # The CI/CD pipeline uses us-west-2 +Default output format [None]: + +# Check your configuration +aws configure list [--profile profile-name] + +# verify it works +aws s3 ls +``` + +### ECR repository & S3 bucket + +The CI pipeline creates two artifacts: + +- An action server docker **image**, which is pushed to an **AWS ECR** repository. +- A trained rasa **model**, which is copied to an **AWS S3** bucket + +Run these commands to create the storage for these artifacts. These commands run the AWS CLI and create an ECR repository and an S3 bucket: + +```bash +# create ECR repository with name `financial-demo` +make aws-ecr-create-repository + +# create S3 bucket with name `rasa-financial-demo` +make aws-s3-create-bucket +``` + + + +## EKS production cluster + +If the production cluster is already set up, you can skip this section. + +This section describes the initial deployment of the financial-demo bot on an EKS production cluster. + +This initial deployment is done manually. After that, the deployment is maintained & upgraded automatically by the CI/CD pipeline. + +### Preparation + +#### Install eksctl + +See the [installation instructions](https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html) + +If you use Ubuntu, you can just issue the command: + +```bash +make install-eksctl +``` + +#### Install kubectl + +See the [installation instructions](https://kubernetes.io/docs/tasks/tools/#kubectl) + +If you use Ubuntu, you can just issue the command: + +```bash +make install-kubectl +``` + +#### Install helm + +See the [installation instructions](https://helm.sh/docs/intro/install/) + +If you use Ubuntu, you can just issue the command: + +```bash +make install-helm +``` + +#### Install jp + +See the [installation instructions](https://github.com/jmespath/jp#installing) + +If you use Ubuntu, you can just issue the command: + +```bash +make install-jp +``` + +#### Set environment variables + +There are many ways to set the required environment variables in your local environment. + +One way is to create a file `./secret/envs_export`, with this content: + +```bash +# source this file to set the environment variables the same as the GitHub secrets. +export GCR_AUTH_JSON_PRIVATE_KEY_ID=... +export GCR_AUTH_JSON_PRIVATE_KEY='-----BEGIN PRIVATE KEY-...-END PRIVATE KEY-----\n' +export GCR_AUTH_JSON_CLIENT_EMAIL='...' +export GCR_AUTH_JSON_CLIENT_ID=... +export GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD=... +export GLOBAL_REDIS_PASSWORD=... +export RABBITMQ_RABBITMQ_PASSWORD=... +export RASAX_INITIALUSER_USERNAME=admin +export RASAX_INITIALUSER_PASSWORD=... +export RASAX_JWTSECRET=... +export RASAX_PASSWORDSALT=... +export RASAX_TOKEN=... +export RASA_TOKEN=... +export AWS_ELASTIC_IP=... +``` + +Then, create the environment variables with the command: + +```bash +source ./secret/envs_export +``` + +### Create the EKS cluster + +Create the EKS production cluster, with name `financial-demo-`: + +```bash +make aws-eks-cluster-create AWS_EKS_CLUSTER_NAME=financial-demo-production +``` + +Some other useful 'Makefile functions' to interact with the EKS cluster: + +```bash +make aws-eks-cluster-list-all +make aws-eks-cluster-describe AWS_EKS_CLUSTER_NAME=financial-demo-production +make aws-eks-cluster-describe-stacks AWS_EKS_CLUSTER_NAME=financial-demo-production +make aws-eks-cluster-delete AWS_EKS_CLUSTER_NAME=financial-demo-production +``` + +### Configure `kubeconfig` + +Add the EKS cluster information to `~/.kube/config`, and set the `current-context` to that cluster: + +```bash +make aws-eks-cluster-update-kubeconfig AWS_EKS_CLUSTER_NAME=financial-demo-production + +# make sure kubectl is now looking at the correct EKS cluster +make kubectl-config-current-context +``` + +### Install/Upgrade Rasa Enterprise + +#### Build & push action server docker image + +Build, run, test & push the action docker server image, with name: + +- `/financial-demo:` + +```bash +# Verify that the correct rasa-sdk is selected in `Dockerfile` +# ==> FROM rasa/rasa-sdk:.... +# +# Then, build the image with: +make docker-build + +make docker-run +make docker-test +make docker-stop + +# Login & push the docker image to the AWS ECR repository +make aws-ecr-docker-login +make docker-push +``` + +#### Install/Upgrade Rasa Enterprise + +Install/Upgrade Rasa Enterprise, using the action server docker image that was uploaded to the ECR repository: + +```bash +# make sure kubectl is looking at the correct EKS cluster +# -> if not, configure kubeconfig as described above +make kubectl-config-current-context AWS_EKS_CLUSTER_NAME=financial-demo-production + +# create namespace `my-namespace` +make aws-eks-namespace-create + +# create/refresh gcr-pull-secret, for Rasa Enterprise image +make pull-secret-gcr-create + +# create/refresh ecr-pull-secret, for action server image +make pull-secret-ecr-create + +# Verify that: +# +# the correct Rasa Enterprise version is selected in `Makefile` +# ==> RASAX_TAG := ...... +# +# the correct rasa version is selected in `requirements.txt` +# ==> rasa[spacy]==.... +# +# Install/Upgrade Rasa Enterprise with the action server +make rasa-enterprise-install + +# Check Rasa Enterprise Health +make rasa-enterprise-check-health + +# To troubleshoot, highly recommended to use OCTANT, see Appendix C +``` + +### Train, test & upload model to S3 + +```bash +# Train the model: `models/.tar.gz` +make rasa-train + +# In another window, start duckling server +docker run -p 8000:8000 rasa/duckling + +# Run the end-2-end tests +make rasa-test + +# Upload `models/.tar.gz` to S3 +# Note: This does not mean the model is deployed to Rasa Enterprise, +# which is done in the next step. +make aws-s3-upload-rasa-model +``` + +### Deploy, Tag & Smoketest the trained model + +```bash +# Configure ~/.kube/config and set current_context +make aws-eks-cluster-update-kubeconfig AWS_EKS_CLUSTER_NAME=financial-demo-production + +# Deploy rasa model +make aws-s3-download-rasa-model +make rasa-enterprise-model-delete +make rasa-enterprise-model-upload +make rasa-enterprise-model-tag + +# Wait about 1 minute, so rasa-production can download, upack & load the model +# Smoketest +make rasa-enterprise-smoketest +``` + +### DNS + +Define a DNS record of type CNAME with your domain service provider: + +- **name of sub-domain**: `aws-financial-demo` + + *==> This example name will resolve to `aws-financial-demo.my-domain.com`* + +- **Type**: `CNAME` + +- **Content**: `--------.us-west-2.elb.amazonaws.com` + + *==> This is the hostname of the External Application Load Balancer that AWS EKS created during the deployment. You can get this hostname with the command:* + + ```bash + make rasa-enterprise-get-loadbalancer-hostname + ``` + +- **TTL (Time to Live)**: 1 Hour + +- **Priority**: + +It might take some time for things to propagate, but you can verify it with commands like nslookup & dig: + +```bash +######################################################################## +# use `nslookup -type` +nslookup -type=CNAME aws-financial-demo.my-domain.com +aws-financial-demo.my-domain.com canonical name = ---.us-west-2.elb.amazonaws.com. + +######################################################################## +# use `dig` +dig CNAME aws-financial-demo.my-domain.com +... +;; ANSWER SECTION: +aws-financial-demo.my-domain.com. ---- IN CNAME ---.us-west-2.elb.amazonaws.com. + +``` + +Once propagated, you can access Rasa Enterprise at **http://aws-financial-demo.my-domain.com** + + + +## Appendix A: The AWS EKS cluster + +We use *eksctl* to create the clusters. It creates many AWS resources with CloudFormation templates. + +Some of these AWS resources are: + +- A VPC with + - Public & private subnets + - Internet & NAT Gateways + - Route Tables +- An IAM EKS service role +- An EKS Cluster Control Plane +- An EKS Managed nodegroup of EC2 instances + +The cluster context is also added to the `~/.kube/config` file. + +The VPC created looks something like this: + +![](images/financial-demo-eks-test-vpc.png) + +The EKS Control Plane interacts with the the EKS Data Plane (the nodes), like this: + +![img](https://d2908q01vomqb2.cloudfront.net/fe2ef495a1152561572949784c16bf23abb28057/2020/04/10/subnet_pubpri.png) + + + +## Appendix B: Manual Cleanup of AWS resources + +Sometimes things do not clean up properly, and you need to do a manual cleanup in the **AWS console**: + +- **CloudFormation**: Try to delete all the stacks in reverse order as they were created by the eksctl command. + +- When a stack fails to delete due to dependencies, you have two options: + + - Select to retain the resources that have dependency errors. (**NOT RECOMMENDED**) + + The stack delete operation will simply skip deleting them. This is NOT recommended, because you will clutter up your AWS account with many unused resources. + + - Manually delete the resources that the stack is not able to delete. (**RECOMMENDED**) + + You do this by drilling down into the **CloudFormation stack delete events** messages, and delete items bottom-up the dependency tree. + + One example of a bottom-up delete sequence is when deletion of the VPC fails: + + - **EC2 > Load Balancers**: first, delete the ELB load balancers + - **VPC > Subnets**: then, delete the subnets + - This will also delete the EC2 > Network interfaces, named `eni-xxxx` + - You cannot delete Subnets until the ELB load balancers are deleted + - **VPC > Your VPCs**: finally, delete the VPC + - This will also delete all associated: + - security groups (`sg-xxx`) + - internet gateways (`igw-xxx`) + - subnets (`subnet-xxx`) + + - After cleaning up, try again to delete the **AWS CloudFormation** stack. + + - If it still does not delete, iterate the manual cleanups until it does. + + Again, this can be a painful process, but once the CloudFormation stacks delete properly without retaining/skipping anything, you are guaranteed that you have cleaned up all the resources created by the CI/CD pipeline. + +## Appendix C: OCTANT + +[Octant](https://octant.dev/) is a great tool to look inside the cluster and trouble shoot issues when they arise. + +### Install Octant + +[Installation instructions](https://github.com/vmware-tanzu/octant#installation) + +#### Install on Ubuntu + +```bash +cd ~ +mkdir octant +cd octant +wget https://github.com/vmware-tanzu/octant/releases/download/v0.20.0/octant_0.20.0_Linux-64bit.deb +sudo dpkg -i octant_0.20.0_Linux-64bit.deb +``` + +### Run Octant + +```bash +# #################################################### # +# Configure kubectl to look at the correct EKS cluster # +# #################################################### # + +# -> for a feature branch test cluster +git checkout my-feature-branch + +# -> for the production cluster +git branch production # create temporary, local `production` branch +git checkout production + +# Configure kubeconfig for the correct EKS cluster +make aws-eks-cluster-update-kubeconfig + +# make sure kubectl is now looking at the correct EKS cluster +make kubectl-config-current-context + +# #################################################### # +# Run Octant on default port 7777 and open the browser # +# #################################################### # +OCTANT_LISTENER_ADDR=0.0.0.0:7777 octant & + +# Within octant, select the namespace `my-namespace` +``` + +## Appendix D: AWS EKS references + +There are two commands to create AWS EKS clusters; `eksctl` & `aws eks`. + +- The `eksctl` cli is the most complete, and it is what we ended up using. + +- The `aws eks` cli does not support to launch worker nodes to the cluster control plane. This has to be done manually from the AWS Console, which makes it unsuited for a CI/CD pipeline where everything needs to be done via scripting (=> infrastructure as code). + +The following references are great to learn about AWS EKS, and it is highly recommended to manually build some test-clusters with both the `eksctl` & `aws eks` commands to demystify all those AWS resources that are being generated: + +- [eksctl – the EKS CLI](https://aws.amazon.com/blogs/opensource/eksctl-eks-cli/) +- [Getting started with Amazon EKS - eksctl](https://docs.aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html) +- [Blog: EKS Cluster with One Command](https://aws.amazon.com/blogs/opensource/eksctl-eks-cluster-one-command/) +- [Blog: Demystifying cluster networking for EKS worker nodes](https://aws.amazon.com/blogs/containers/de-mystifying-cluster-networking-for-amazon-eks-worker-nodes/) +- https://docs.aws.amazon.com/eks/latest/userguide/create-public-private-vpc.html +- https://docs.aws.amazon.com/eks/latest/userguide/eks-compute.html +- https://logz.io/blog/amazon-eks-cluster/amp/ +- https://www.eksworkshop.com/ +- [aws eks - Command Reference](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/eks/index.html) (Not used) -It is recommended to use an [automated CI/CD process](https://rasa.com/docs/rasa/user-guide/setting-up-ci-cd) to keep your action server up to date in a production environment. diff --git a/actions/actions.py b/actions/actions.py index 9bee776b..eb623896 100644 --- a/actions/actions.py +++ b/actions/actions.py @@ -1,4 +1,4 @@ -"""Custom actions""" +"""Custom actions """ import os from typing import Dict, Text, Any, List import logging diff --git a/data/nlu/nlu.yml b/data/nlu/nlu.yml index 93a3534e..96265d30 100644 --- a/data/nlu/nlu.yml +++ b/data/nlu/nlu.yml @@ -21,6 +21,7 @@ nlu: - ok - ye - okay + - yes. - intent: ask_transfer_charge examples: | - Will I be charged for transferring money diff --git a/deploy/gcr-auth.json b/deploy/gcr-auth.json new file mode 100644 index 00000000..a4321ce8 --- /dev/null +++ b/deploy/gcr-auth.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "rasa-platform", + "private_key_id": "set_as_environment_variable-GCR_AUTH_JSON_PRIVATE_KEY_ID", + "private_key": "set_as_environment_variable-GCR_AUTH_JSON_PRIVATE_KEY", + "client_email": "set_as_environment_variable-GCR_AUTH_JSON_CLIENT_EMAIL", + "client_id": "set_as_environment_variable-GCR_AUTH_JSON_CLIENT_ID", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/rasa-testing%40rasa-platform.iam.gserviceaccount.com" +} \ No newline at end of file diff --git a/deploy/values.yml b/deploy/values.yml new file mode 100644 index 00000000..bb184c54 --- /dev/null +++ b/deploy/values.yml @@ -0,0 +1,54 @@ +# nginx ingress controller +nginx: + service: + port: 80 +# docker pull secret for private images +images: + imagePullSecrets: + - name: "gcr-pull-secret" + - name: "ecr-pull-secret" +# action server image +app: + name: "set_as_environment_variable-APP_NAME" + tag: "set_as_environment_variable-APP_TAG" +# rasax specific settings +rasax: + tag: "set_as_environment_variable-RASAX_TAG" + # name of the Rasa Enterprise image to use + name: "gcr.io/rasa-platform/rasa-x-ee" + # initialUser is the user which is created upon the initial start of Rasa Enterprise + initialUser: + # username specifies a name of this user + username: "set_as_environment_variable-RASAX_INITIALUSER_USERNAME" + # password for the Rasa Enterprise user + password: "set_as_environment_variable-RASAX_INITIALUSER_PASSWORD" + # passwordSalt Rasa X uses to salt the user passwords + passwordSalt: "set_as_environment_variable-RASAX_PASSWORDSALT" + # token Rasa X accepts as authentication token from other Rasa services + token: "set_as_environment_variable-RASAX_TOKEN" + # jwtSecret which is used to sign the jwtTokens of the users + jwtSecret: "set_as_environment_variable-RASAX_JWTSECRET" +# rasa: Settings common for all Rasa containers +rasa: + tag: "set_as_environment_variable-RASA_TAG" + # token Rasa accepts as authentication token from other Rasa services + token: "set_as_environment_variable-RASA_TOKEN" + livenessProbe: + # initialProbeDelay for the `livenessProbe` + initialProbeDelay: 60 +# RabbitMQ specific settings +rabbitmq: + # rabbitmq settings of the subchart + rabbitmq: + # password which is used for the authentication + password: "set_as_environment_variable-RABBITMQ_RABBITMQ_PASSWORD" +# global settings of the used subcharts +global: + # postgresql: global settings of the postgresql subchart + postgresql: + # postgresqlPassword is the password which is used when the postgresqlUsername equals "postgres" + postgresqlPassword: "set_as_environment_variable-GLOBAL_POSTGRESQL_POSTGRESQLPASSWORD" + # redis: global settings of the redis subchart + redis: + # password to use in case there no external secret was provided + password: "set_as_environment_variable-GLOBAL_REDIS_PASSWORD" \ No newline at end of file diff --git a/images/cicd.png b/images/cicd.png new file mode 100644 index 0000000000000000000000000000000000000000..7d2f4ab35488306b14ec8c5e1e5148661834bcad GIT binary patch literal 17861 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 c=d1fiwo3#;kK;-g4z>{8y``gpzWFrl-|D!z>% literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index 5d5f9189..9f74a5dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,6 @@ -r actions/requirements-actions.txt -# Sync with files: -# (-) .github/workflows/build_and_deploy.yml (`rasa_version`) rasa[spacy]==2.4.0 -# Sync with files: -# (-) .github/workflows/continuous-integration.yml (`rasa_sdk_version`) -# (-) Dockerfile +# Sync with `Dockerfile` rasa-sdk==2.4.0 \ No newline at end of file diff --git a/scripts/patch_gcr_auth_json.py b/scripts/patch_gcr_auth_json.py new file mode 100644 index 00000000..9d93f13a --- /dev/null +++ b/scripts/patch_gcr_auth_json.py @@ -0,0 +1,24 @@ +import os + +with open('deploy/gcr-auth.json', 'r') as f: + lines = f.readlines() + for line in lines: + print( + line.replace( + "set_as_environment_variable-GCR_AUTH_JSON_PRIVATE_KEY_ID", + os.environ["GCR_AUTH_JSON_PRIVATE_KEY_ID"], + ) + .replace( + "set_as_environment_variable-GCR_AUTH_JSON_PRIVATE_KEY", + os.environ["GCR_AUTH_JSON_PRIVATE_KEY"], + ) + .replace( + "set_as_environment_variable-GCR_AUTH_JSON_CLIENT_EMAIL", + os.environ["GCR_AUTH_JSON_CLIENT_EMAIL"], + ) + .replace( + "set_as_environment_variable-GCR_AUTH_JSON_CLIENT_ID", + os.environ["GCR_AUTH_JSON_CLIENT_ID"], + ), + end='', + ) diff --git a/scripts/smoketest.py b/scripts/smoketest.py new file mode 100644 index 00000000..b07fd147 --- /dev/null +++ b/scripts/smoketest.py @@ -0,0 +1,141 @@ +"""Perform Rasa Enterprise smoke tests + +Rasa X HTTP API: https://rasa.com/docs/rasa-x/pages/http-api + +Rasa HTTP API: https://rasa.com/docs/rasa/pages/http-api + (Use BASE_URL/core/... to reach rasa-production server directly) +""" +from typing import Any +import os +import pprint +import time +import json +from multiprocessing import Pool + +from requests import request + +BASE_URL = os.environ.get("BASE_URL") +USERNAME = os.environ.get("RASAX_INITIALUSER_USERNAME") +PASSWORD = os.environ.get("RASAX_INITIALUSER_PASSWORD") +VERBOSE = int(os.environ.get("VERBOSE", 1)) + + +def my_print(msg: Any, status_code: int = None) -> None: + """Pretty print msg""" + if VERBOSE > 0: + if status_code: + print(f"status_code: {status_code}") + + if isinstance(msg, (list, dict)): + pprint.pprint(msg) + else: + print(msg) + + +quit = False +if not BASE_URL: + my_print("Please set environment variable: BASE_URL") + quit = True +if not USERNAME: + my_print("Please set environment variable: RASAX_INITIALUSER_USERNAME") + quit = True +if not PASSWORD: + my_print("Please set environment variable: RASAX_INITIALUSER_PASSWORD") + quit = True +if quit: + exit(1) + +################################### +my_print("--\nBASE_URL") +my_print(BASE_URL) + +################################### +my_print("--\nRasa X Health check") + +url = f"{BASE_URL}/api/health" +r = request("GET", url) +if r.status_code != 200: + my_print(r.json(), r.status_code) + exit(1) + +status_rasa_production = r.json().get("production", {}).get("status") +status_rasa_worker = r.json().get("worker", {}).get("status") + +my_print(r.json(), r.status_code) + +if status_rasa_production != 200: + my_print(f'rasa-production not OK!\nstatus = {status_rasa_production}') + exit(1) + +if status_rasa_worker != 200: + my_print(f'rasa-worker not OK!\nstatus = {status_rasa_worker}') + exit(1) + +################################### +my_print("--\nRasa Production Health check (direct to rasa-production)") + +url = f"{BASE_URL}/core/" +r = request("GET", url) +if r.status_code != 200: + my_print(r.json(), r.status_code) + exit(1) + +my_print(r.text) + + +################################### +my_print("--\nGet access_token") + +url = f"{BASE_URL}/api/auth" +payload = {"username": USERNAME, "password": PASSWORD} +headers = {"Content-Type": "application/json"} +r = request("POST", url, json=payload, headers=headers) +if r.status_code != 200: + my_print(r.json(), r.status_code) + exit(1) + +access_token = r.json().get("access_token") + +if not access_token: + my_print("access_token is empty???") + exit(1) + +################################### +my_print("--\nCheck tagged production model") + +url = f"{BASE_URL}/api/projects/default/models" +payload = {} +headers = {"Authorization": f"Bearer {access_token}"} +params = {'tag': 'production'} +r = request("GET", url, json=payload, headers=headers, params=params) +if r.status_code != 200: + my_print(r.json(), r.status_code) + exit(1) + +production_model = r.json()[0].get("model") +my_print(f"Found production model: {production_model}") + +################################### +my_print("--\nSay hi") + +url = f"{BASE_URL}/api/chat" +payload = {"message": "hi"} +headers = {"Authorization": f"Bearer {access_token}"} +params = {} +r = request("POST", url, json=payload, headers=headers, params=params) +if r.status_code != 200 or r.json() == []: + my_print(r.json(), r.status_code) + if r.status_code == 200 and r.json() == []: + print( + "Bot responded, but the response is empty.\n" + "Probably the model is not loaded yet by rasa-prod container.\n" + "Confirm by running `make rasa-enterprise-smoketest locally`.\n" + "If so, increase sleep time in pipeline after tagging of production model." + ) + exit(1) + +my_print(r.json(), r.status_code) + +################################### +my_print("--\nSmoke tests passed") +exit(0) diff --git a/scripts/wait_for_external_ip.sh b/scripts/wait_for_external_ip.sh new file mode 100755 index 00000000..6e3d400f --- /dev/null +++ b/scripts/wait_for_external_ip.sh @@ -0,0 +1,18 @@ +AWS_EKS_NAMEPACE=${1:-"my-namespace"} +AWS_EKS_RELEASE_NAME=${2:-"my-release"} +ATTEMPTS=${3:-100} +COUNT=1; +IP="" +while [ -z $IP ]; do + IP=$(kubectl --namespace ${AWS_EKS_NAMEPACE} get service ${AWS_EKS_RELEASE_NAME}-rasa-x-nginx --output jsonpath='{.status.loadBalancer.ingress[0].hostname}') + if [[ $COUNT -eq $ATTEMPTS ]]; then + echo "# Limit of $ATTEMPTS attempts has exceeded." + exit 1 + fi + if [[ -z "$IP" ]]; then + echo -e "$(( COUNT++ ))... \c" + sleep 2 + fi +done +echo 'Found External IP' +echo 'Login at: http://'$IP':8000/login' From 6a006dc5f3838a65f69decc4022f080c02dd1751 Mon Sep 17 00:00:00 2001 From: Arjaan Buijk Date: Wed, 2 Jun 2021 14:03:16 -0400 Subject: [PATCH 02/11] Final test --- actions/actions.py | 2 +- data/nlu/nlu.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/actions.py b/actions/actions.py index eb623896..9bee776b 100644 --- a/actions/actions.py +++ b/actions/actions.py @@ -1,4 +1,4 @@ -"""Custom actions """ +"""Custom actions""" import os from typing import Dict, Text, Any, List import logging diff --git a/data/nlu/nlu.yml b/data/nlu/nlu.yml index 96265d30..9343638a 100644 --- a/data/nlu/nlu.yml +++ b/data/nlu/nlu.yml @@ -21,7 +21,7 @@ nlu: - ok - ye - okay - - yes. + - yes.. - intent: ask_transfer_charge examples: | - Will I be charged for transferring money From edcc03410733ec26dce6959c82b310feed4649f7 Mon Sep 17 00:00:00 2001 From: Arjaan Buijk Date: Wed, 2 Jun 2021 14:53:27 -0400 Subject: [PATCH 03/11] Fix kubeconfig setting for prod --- .github/trigger.txt | 2 +- .github/workflows/cicd.yml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/trigger.txt b/.github/trigger.txt index 1ace9944..c9abeaa9 100644 --- a/.github/trigger.txt +++ b/.github/trigger.txt @@ -1,2 +1,2 @@ # A dummy file to trigger the workflow without making any actual change. -# Just change something arbitrary in this text, like a space . \ No newline at end of file +# Just change something arbitrary in this text, like a space . \ No newline at end of file diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 44b9b469..ed585c80 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -354,8 +354,6 @@ jobs: name: deploy_to_prod_cluster runs-on: ubuntu-latest needs: [params, deploy_to_test_cluster] - env: - AWS_EKS_CLUSTER_NAME: financial-demo-production steps: - name: checkout if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' @@ -380,7 +378,7 @@ jobs: - name: configure kubectl if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' run: | - make aws-eks-cluster-update-kubeconfig + make aws-eks-cluster-update-kubeconfig AWS_EKS_CLUSTER_NAME=financial-demo-production - name: create namespace if not exists if: needs.params.outputs.do_deploy_to_prod_cluster == 'true' From 9631faf645602cb63ebfbf977a1a0015f5a89a47 Mon Sep 17 00:00:00 2001 From: Arjaan Buijk Date: Wed, 9 Jun 2021 15:30:36 -0400 Subject: [PATCH 04/11] Update TOC --- README.md | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7247ee6e..4a8e20b9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ This is an example chatbot demonstrating how to build AI assistants for financia **Table of Contents** -- [Financial Services Example Bot](#financial-services-example-bot) - [Install dependencies](#install-dependencies) - [Run the bot](#run-the-bot) - [Overview of the files](#overview-of-the-files) @@ -25,6 +24,42 @@ This is an example chatbot demonstrating how to build AI assistants for financia - [Testing the bot](#testing-the-bot) - [Rasa X Deployment](#rasa-x-deployment) - [Action Server Image](#action-server-image) +- [CI/CD](#cicd) + - [Summary](#summary) + - [GitHub Secrets](#github-secrets) + - [AWS IAM User API Keys:](#aws-iam-user-api-keys) + - [AWS Elastic IP:](#aws-elastic-ip) + - [Rasa Enterprise License:](#rasa-enterprise-license) + - [Helm chart Credentials](#helm-chart-credentials) + - [AWS Preparation](#aws-preparation) + - [IAM User API Keys](#iam-user-api-keys) + - [SSH Key Pair](#ssh-key-pair) + - [Local AWS CLI](#local-aws-cli) + - [Install AWS CLI v2](#install-aws-cli-v2) + - [Configure your AWS CLI](#configure-your-aws-cli) + - [ECR repository & S3 bucket](#ecr-repository--s3-bucket) + - [EKS production cluster](#eks-production-cluster) + - [Preparation](#preparation) + - [Install eksctl](#install-eksctl) + - [Install kubectl](#install-kubectl) + - [Install helm](#install-helm) + - [Install jp](#install-jp) + - [Set environment variables](#set-environment-variables) + - [Create the EKS cluster](#create-the-eks-cluster) + - [Configure `kubeconfig`](#configure-kubeconfig) + - [Install/Upgrade Rasa Enterprise](#installupgrade-rasa-enterprise) + - [Build & push action server docker image](#build--push-action-server-docker-image) + - [Install/Upgrade Rasa Enterprise](#installupgrade-rasa-enterprise-1) + - [Train, test & upload model to S3](#train-test--upload-model-to-s3) + - [Deploy, Tag & Smoketest the trained model](#deploy-tag--smoketest-the-trained-model) + - [DNS](#dns) + - [Appendix A: The AWS EKS cluster](#appendix-a-the-aws-eks-cluster) + - [Appendix B: Manual Cleanup of AWS resources](#appendix-b-manual-cleanup-of-aws-resources) + - [Appendix C: OCTANT](#appendix-c-octant) + - [Install Octant](#install-octant) + - [Install on Ubuntu](#install-on-ubuntu) + - [Run Octant](#run-octant) + - [Appendix D: AWS EKS references](#appendix-d-aws-eks-references) From 029ac802c9a9e3a7ed98b23e1393d031e8ebef84 Mon Sep 17 00:00:00 2001 From: Arjaan Buijk Date: Wed, 9 Jun 2021 15:34:31 -0400 Subject: [PATCH 05/11] Explain CI/CD and reference to rasa docs --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a8e20b9..cb60076e 100644 --- a/README.md +++ b/README.md @@ -329,7 +329,9 @@ make docker-push ## Summary -A CI/CD pipeline is used to test, build and deploy the financial-demo bot to AWS EKS: +As explained in the [Setting up CI/CD](https://rasa.com/docs/rasa/setting-up-ci-cd) section of the Rasa documentation, Continous Integration (**CI**) is the practice of merging in code changes frequently and automatically testing changes as they are committed. Continuous Deployment (**CD**) means automatically deploying integrated changes to a staging or production environment. Together, they allow you to make more frequent improvements to your assistant and efficiently test and deploy those changes. + +A CI/CD pipeline is used to test, build and deploy the financial-demo bot to AWS EKS. The pipeline uses GitHub Actions, defined in `.github/workflows/cicd.yml`. It includes these jobs: From 02fcfc8f614fa5748fa4c1648ff04899d2d44b32 Mon Sep 17 00:00:00 2001 From: Arjaan Buijk <16159200+ArjaanBuijk@users.noreply.github.com> Date: Wed, 9 Jun 2021 15:38:32 -0400 Subject: [PATCH 06/11] Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index cb60076e..75015799 100644 --- a/README.md +++ b/README.md @@ -796,7 +796,7 @@ Sometimes things do not clean up properly, and you need to do a manual cleanup i - Manually delete the resources that the stack is not able to delete. (**RECOMMENDED**) - You do this by drilling down into the **CloudFormation stack delete events** messages, and delete items bottom-up the dependency tree. + You can do this by drilling down into the **CloudFormation stack delete events** messages and deleting items bottom-up the dependency tree. One example of a bottom-up delete sequence is when deletion of the VPC fails: @@ -881,4 +881,3 @@ The following references are great to learn about AWS EKS, and it is highly reco - https://logz.io/blog/amazon-eks-cluster/amp/ - https://www.eksworkshop.com/ - [aws eks - Command Reference](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/eks/index.html) (Not used) - From 62618515c0994eb591f92a7f1d632cab5bb6db4f Mon Sep 17 00:00:00 2001 From: Arjaan Buijk <16159200+ArjaanBuijk@users.noreply.github.com> Date: Wed, 9 Jun 2021 15:38:52 -0400 Subject: [PATCH 07/11] Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 75015799..c4d8a9c4 100644 --- a/README.md +++ b/README.md @@ -818,7 +818,7 @@ Sometimes things do not clean up properly, and you need to do a manual cleanup i ## Appendix C: OCTANT -[Octant](https://octant.dev/) is a great tool to look inside the cluster and trouble shoot issues when they arise. +[Octant](https://octant.dev/) is a useful open sourced tool for visualizing workloads inside the cluster and troubleshooting issues when they arise. ### Install Octant From ef0f9aa9a7454a928fc582f15dcb48e5a96bc68a Mon Sep 17 00:00:00 2001 From: Arjaan Buijk <16159200+ArjaanBuijk@users.noreply.github.com> Date: Wed, 9 Jun 2021 15:39:12 -0400 Subject: [PATCH 08/11] Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4d8a9c4..0f444ac7 100644 --- a/README.md +++ b/README.md @@ -870,7 +870,7 @@ There are two commands to create AWS EKS clusters; `eksctl` & `aws eks`. - The `aws eks` cli does not support to launch worker nodes to the cluster control plane. This has to be done manually from the AWS Console, which makes it unsuited for a CI/CD pipeline where everything needs to be done via scripting (=> infrastructure as code). -The following references are great to learn about AWS EKS, and it is highly recommended to manually build some test-clusters with both the `eksctl` & `aws eks` commands to demystify all those AWS resources that are being generated: +The following references are useful for learning about AWS EKS, and it is highly recommended to manually build test-clusters with both the `eksctl` & `aws eks` commands to help demystify the AWS resources that are being generated: - [eksctl – the EKS CLI](https://aws.amazon.com/blogs/opensource/eksctl-eks-cli/) - [Getting started with Amazon EKS - eksctl](https://docs.aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html) From 4f377f585d1082d9680c12313f0051177b8bc639 Mon Sep 17 00:00:00 2001 From: Arjaan Buijk <16159200+ArjaanBuijk@users.noreply.github.com> Date: Wed, 9 Jun 2021 15:39:19 -0400 Subject: [PATCH 09/11] Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f444ac7..83e18cd0 100644 --- a/README.md +++ b/README.md @@ -687,7 +687,7 @@ make rasa-train # In another window, start duckling server docker run -p 8000:8000 rasa/duckling -# Run the end-2-end tests +# Run the end-to-end tests make rasa-test # Upload `models/.tar.gz` to S3 From 2bede4f7404be3c85f056a2c26c4a2f5146bbb0f Mon Sep 17 00:00:00 2001 From: Arjaan Buijk Date: Fri, 11 Jun 2021 14:02:14 -0400 Subject: [PATCH 10/11] Reference gcloud credential helper in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cb60076e..44785fdc 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,8 @@ To allow the GitHub actions to define a pull secret for the private GCR repo, ge - GCR_AUTH_JSON_CLIENT_EMAIL = `client_email` - GCR_AUTH_JSON_CLIENT_ID = `client_id` +An alternative approach to GCR repo authentication would be with the [gcloud credential helper](https://cloud.google.com/container-registry/docs/advanced-authentication#gcloud-helper). + #### Helm chart Credentials To allow the GitHub actions to use safe_credentials in the `values.yml` ([docs](https://rasa.com/docs/rasa-x/installation-and-setup/install/helm-chart#3-configure-credentials)), add following GitHub Secrets to the repo, replacing each `` with a different alphanumeric string, and choose a `` for the initial user. From 3e3679e26eb8f78ea8f55212f574aecc82520598 Mon Sep 17 00:00:00 2001 From: Arjaan Buijk Date: Mon, 21 Jun 2021 14:16:01 -0400 Subject: [PATCH 11/11] Link README job description to cicd.yml in Github Help section in Makefile Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Update README.md Co-authored-by: Ben Quachtran <65514514+b-quachtran@users.noreply.github.com> Resolve 2 more items --- Makefile | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- README.md | 58 ++++++++++---------- 2 files changed, 180 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index 826003aa..9d1102a6 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,156 @@ AWS_EKS_RELEASE_NAME := my-release GIT_BRANCH_NAME := $(shell git branch --show-current) +help: + @echo "make" + @echo " clean" + @echo " Remove Python/build artifacts." + @echo " formatter" + @echo " Apply black formatting to code." + @echo " lint" + @echo " Lint code with flake8, and check if black formatter should be applied." + @echo " types" + @echo " Check for type errors using pytype." + @echo " test" + @echo " Run unit tests for the custom actions using pytest." + @echo " aws-cloudformation-eks-get-ARN" + @echo " Gets Amazon Resource Name (ARN) of an EKS cluster." + @echo " aws-cloudformation-eks-get-CertificateAuthorityData" + @echo " Gets CertificateAuthorityData of an EKS cluster." + @echo " aws-cloudformation-eks-get-ClusterSecurityGroupId" + @echo " Gets Security Group ID of an EKS cluster." + @echo " aws-cloudformation-eks-get-Endpoint" + @echo " Gets Endpoint (https://...) of an EKS cluster." + @echo " aws-cloudformation-eks-get-SecurityGroup" + @echo " Gets Security Group ID of nodes in an EKS cluster." + @echo " aws-cloudformation-eks-get-ServiceRoleARN" + @echo " Gets Amazon Resource Name (ARN) of an EKS cluster Service Role." + @echo " aws-cloudformation-eks-get-SharedNodeSecurityGroup" + @echo " Gets SharedNodeSecurityGroup of an EKS cluster." + @echo " aws-cloudformation-eks-get-SubnetsPrivate" + @echo " Gets the private subnets of an EKS cluster." + @echo " aws-cloudformation-eks-get-SubnetsPublic" + @echo " Gets the public subnets of an EKS cluster." + @echo " aws-cloudformation-eks-get-VPC" + @echo " Gets the VPC of an EKS cluster." + @echo " aws-ecr-create-repository" + @echo " Creates an ECR repository to store docker images." + @echo " aws-ecr-docker-login" + @echo " Logs docker cli into the ECR." + @echo " aws-ecr-get-authorization-token" + @echo " Gets a short lived authentication token for the ECR." + @echo " aws-ecr-get-repositoryUri" + @echo " Gets the URI of an ECR repository." + @echo " aws-eks-cluster-create" + @echo " Creates an EKS cluster with ssh-access & managed nodes, using eksctl." + @echo " aws-eks-cluster-delete" + @echo " Deletes an EKS cluster." + @echo " aws-eks-cluster-describe" + @echo " Describes an EKS cluster." + @echo " aws-eks-cluster-describe-stacks" + @echo " Describes the cloudformation stacks created by the eksctl command." + @echo " aws-eks-cluster-exists" + @echo " Checks if an EKS cluster exits." + @echo " aws-eks-cluster-get-certificateAuthority" + @echo " Gets Certificate Authority of an EKS cluster." + @echo " aws-eks-cluster-get-endpoint" + @echo " Gets Endpoint (https://...) of an EKS cluster.." + @echo " aws-eks-cluster-info" + @echo " Describes an EKS cluster." + @echo " aws-eks-cluster-list-all" + @echo " Lists all EKS clusters." + @echo " aws-eks-cluster-status" + @echo " Gets status of an EKS cluster." + @echo " aws-eks-cluster-update-kubeconfig" + @echo " Add the EKS cluster information to ~/.kube/config, and set the current-context to that cluster." + @echo " aws-eks-namespace-create" + @echo " Creates a namespace in an EKS cluster." + @echo " aws-eks-namespace-delete" + @echo " Deletes a namespace in an EKS cluster." + @echo " aws-iam-role-get-Arn" + @echo " Gets the eksClusterRole of an EKS cluster." + @echo " aws-s3-create-bucket" + @echo " Creates an S3 bucket to store trained rasa models." + @echo " aws-s3-delete-bucket" + @echo " Deletes an S3 bucket." + @echo " aws-s3-download-rasa-model" + @echo " Downloads a trained rasa model from an S3 bucket." + @echo " aws-s3-upload-rasa-model" + @echo " Uploads a trained rasa model to an S3 bucket." + @echo " docker-build" + @echo " Builds the custom action server image." + @echo " docker-clean" + @echo " Runs docker-clean-container & docker-clean-image." + @echo " docker-clean-container" + @echo " Stops & removes container created by docker-run." + @echo " docker-clean-image" + @echo " Removes image created by docker-buil." + @echo " docker-login" + @echo " Logs into a docker registry." + @echo " docker-push" + @echo " Pushes the action server docker image to the ECR repository." + @echo " docker-run" + @echo " Runs the action server docker image." + @echo " docker-stop" + @echo " Stops the action server docker container." + @echo " docker-test" + @echo " Performs a basic test for a running action server docker container." + @echo " install-eksctl" + @echo " Installs eksctl." + @echo " install-helm" + @echo " Installs helm." + @echo " install-jp" + @echo " Installs jp." + @echo " install-kubectl" + @echo " Installs kubectl." + @echo " kubectl-config-current-context" + @echo " Gets the current kubectl context (The EKS cluster)." + @echo " pull-secret-ecr-create" + @echo " Creates an ECR pull secret for action server image." + @echo " pull-secret-ecr-delete" + @echo " Deletes the ECR pull secret." + @echo " pull-secret-gcr-create" + @echo " Creates a GCR pull secret for Rasa Enterprise image." + @echo " pull-secret-gcr-delete" + @echo " Deletes the GCR pull secret." + @echo " rasa-enterprise-check-health" + @echo " Checks <->/api/health of Rasa Enterprise." + @echo " rasa-enterprise-get-access-token" + @echo " Gets access token of Rasa Enterprise." + @echo " rasa-enterprise-get-base-url" + @echo " Gets base URL of Rasa Enterprise." + @echo " rasa-enterprise-get-chat-token" + @echo " Gets chat token of Rasa Enterprise." + @echo " rasa-enterprise-get-loadbalancer-hostname" + @echo " Gets the load balancer hostname for Rasa Enterprise." + @echo " rasa-enterprise-get-login" + @echo " Gets login URL of Rasa Enterprise." + @echo " rasa-enterprise-get-pods" + @echo " Gets pods of Rasa Enterprise deployed in the EKS cluster." + @echo " rasa-enterprise-get-secrets-postgresql" + @echo " Gets secrets of PostgreSQL deployed with Rasa Enterprise." + @echo " rasa-enterprise-get-secrets-rabbit" + @echo " Gets secrets of RabbitMQ deployed with Rasa Enterprise." + @echo " rasa-enterprise-get-secrets-redis" + @echo " Gets secrets of REDIS deployed with Rasa Enterprise." + @echo " rasa-enterprise-install" + @echo " Installs or Upgrades Rasa Enterprise using helm." + @echo " rasa-enterprise-model-delete" + @echo " Deletes a trained rasa model in Rasa Enterprise." + @echo " rasa-enterprise-model-tag" + @echo " Tags a trained rasa model as the production model in Rasa Enterprise." + @echo " rasa-enterprise-model-upload" + @echo " Uploads a trained rasa model to Rasa Enterprise." + @echo " rasa-enterprise-smoketest" + @echo " Performs smoketest to verify Rasa Enterprise is functioning." + @echo " rasa-enterprise-uninstall" + @echo " Uninstalls Rasa Enterprise." + @echo " rasa-test" + @echo " Tests a trained rasa model." + @echo " rasa-train" + @echo " Trains a rasa model." + + clean: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + @@ -352,11 +502,6 @@ pull-secret-gcr-create: --docker-server=gcr.io \ --docker-username=_json_key \ --docker-password='$(shell python ./scripts/patch_gcr_auth_json.py)' -# @kubectl --namespace $(AWS_EKS_NAMESPACE) \ -# create secret docker-registry gcr-pull-secret \ -# --docker-server=gcr.io \ -# --docker-username=_json_key \ -# --docker-password='$(shell cat ./secret/gcr-auth.json)' pull-secret-gcr-delete: @kubectl --namespace $(AWS_EKS_NAMESPACE) \ diff --git a/README.md b/README.md index 3cee293d..d2959d07 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ As part of the deployment, you'll need to set up [git integration](https://rasa. You will need to have docker installed in order to build the action server image. If you haven't made any changes to the action code, you can also use the [public image on Dockerhub](https://hub.docker.com/r/rasa/financial-demo) instead of building it yourself. -Build & tag the image: +To build & tag the image, run: ```bash export ACTION_SERVER_DOCKERPATH=/: @@ -312,7 +312,7 @@ Perform a smoke test on the health endpoint: make docker-test ``` -Once you have confirmed that the container works as it should, push the container image to a registry: +Once you have confirmed that the container is working, push the container image to a registry: ```bash # login to a container registry with your credentials @@ -337,52 +337,52 @@ The pipeline uses GitHub Actions, defined in `.github/workflows/cicd.yml`. It i ![](images/cicd.png) -**params** +**[params](https://github.com/RasaHQ/financial-demo/blob/d40467b4fb2a7d4fb072b86a2828a8cec662eb63/.github/workflows/cicd.yml#L26)** - Defines parameters for use by downstream jobs -**params_summary** +**[params_summary](https://github.com/RasaHQ/financial-demo/blob/d40467b4fb2a7d4fb072b86a2828a8cec662eb63/.github/workflows/cicd.yml#L125)** - Prints the value of the parameters. -**action_server** +**[action_server](https://github.com/RasaHQ/financial-demo/blob/d40467b4fb2a7d4fb072b86a2828a8cec662eb63/.github/workflows/cicd.yml#L156)** - Builds & Tests the docker image of the action server with tag: `` - Uploads the docker image to the AWS ECR repository: `financial-demo` -**rasa_model** +**[rasa_model](https://github.com/RasaHQ/financial-demo/blob/d40467b4fb2a7d4fb072b86a2828a8cec662eb63/.github/workflows/cicd.yml#L203)** - Trains & Tests the rasa model with name: `models/.tar.gz - Uploads the trained model to the AWS S3 bucket: `rasa-financial-demo` -**aws_eks_create_test_cluster** +**[aws_eks_create_test_cluster](https://github.com/RasaHQ/financial-demo/blob/d40467b4fb2a7d4fb072b86a2828a8cec662eb63/.github/workflows/cicd.yml#L242)** -- If not existing yet, create an AWS EKS cluster with name: `financial-demo-` +- If not existing yet, creates an AWS EKS cluster with name: `financial-demo-` -**deploy_to_test_cluster** +**[deploy_to_test_cluster](https://github.com/RasaHQ/financial-demo/blob/d40467b4fb2a7d4fb072b86a2828a8cec662eb63/.github/workflows/cicd.yml#L266)** - Installs/Updates Rasa Enterprise, with the docker image created by the **action_server** job. - Deploys the rasa model, trained by the **rasa_model** job. -- Performs smoke tests to ensure basic operation is all OK. +- Performs smoke tests to ensure basic operations are all OK. -**deploy_to_prod_cluster** +**[deploy_to_prod_cluster](https://github.com/RasaHQ/financial-demo/blob/d40467b4fb2a7d4fb072b86a2828a8cec662eb63/.github/workflows/cicd.yml#L353)** - Runs when pushing to the `main` branch, and all previous steps are successful. - Installs/Updates Rasa Enterprise, with the docker image created by the **action_server** job. - Deploys the rasa model, trained by the **rasa_model** job. -- Performs smoke tests to ensure basic operation is all OK. +- Performs smoke tests to ensure basic operations are all OK. ## GitHub Secrets -In your GitHub repository, go to `Settings > Secrets`, and add these `New repository secrets` . +Secrets can be added to your GitHub repository by going to `Settings > Secrets` and selecting `New repository secret`. -When entering the value, do not use quotes. +When entering values, be sure to omit quotes. #### AWS IAM User API Keys: -To allow the GitHub actions to configure the aws cli, create IAM User API Keys as described below, and add them as GitHub Secrets to the repo: +To configure the aws cli in Github Actions, create IAM User API Keys as described below, and add them as GitHub secrets to the repository: - AWS_ACCESS_KEY_ID = `Access key ID` - AWS_SECRET_ACCESS_KEY = `Secret access key` @@ -395,7 +395,7 @@ Create an Elastic IP as described below, and add it as a GitHub Secret to the re #### Rasa Enterprise License: -To allow the GitHub actions to define a pull secret for the private GCR repo, get the private values from your Rasa Enterprise license file ([docs](https://rasa.com/docs/rasa-x/installation-and-setup/install/helm-chart#5-configure-rasa-x-image)) and add them as GitHub Secrets to the repo: +To define a pull secret in Github Actions for the private GCR repo, you'll need to retrieve the private values from your Rasa Enterprise license file ([docs](https://rasa.com/docs/rasa-x/installation-and-setup/install/helm-chart#5-configure-rasa-x-image)) and add them as GitHub secrets to the repository: - GCR_AUTH_JSON_PRIVATE_KEY_ID = `private_key_id` - GCR_AUTH_JSON_PRIVATE_KEY = `private_key` @@ -406,7 +406,7 @@ An alternative approach to GCR repo authentication would be with the [gcloud cre #### Helm chart Credentials -To allow the GitHub actions to use safe_credentials in the `values.yml` ([docs](https://rasa.com/docs/rasa-x/installation-and-setup/install/helm-chart#3-configure-credentials)), add following GitHub Secrets to the repo, replacing each `` with a different alphanumeric string, and choose a `` for the initial user. +To use safe_credentials in Github Actions through `values.yml` ([docs](https://rasa.com/docs/rasa-x/installation-and-setup/install/helm-chart#3-configure-credentials)), add following GitHub Secrets to the repo, replacing each `` with a different alphanumeric string and choosing a `` for the initial user. *(Please use **safe credentials** to avoid data breaches)* @@ -434,7 +434,7 @@ The CI/CD pipeline uses the [aws cli](https://docs.aws.amazon.com/cli/latest/use The [aws cli](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html#cliv2-linux-install) needs a set of IAM User API keys for authentication & authorization: -- In your AWS Console, go to IAM dashboard to create a new set of API keys: +In your AWS Console, go to the IAM dashboard to create a new set of API keys: - Click on Users @@ -461,14 +461,14 @@ The [aws cli](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-lin ### SSH Key Pair -To be able to SSH into the EC2 worker nodes of the EKS cluster, you need an SSH Key Pair +To SSH into the EC2 worker nodes of the EKS cluster, you'll need an SSH Key Pair - In your AWS Console, go to **EC2 > Key Pairs**, and create a Key Pair with the name `findemo`, and download the file `findemo.pem` which contains the private SSH key. **Note that the name `findemo` is important, since it is used by the CI/CD pipeline when the cluster is created.** ### Local AWS CLI -Before the CI/CD pipeline can run, you will use the AWS CLI locally to create some items, so also install & configure the CLI locally. +Before the CI/CD pipeline can run, you will use the AWS CLI locally to create some resources and need to install & configure the CLI locally. #### Install AWS CLI v2 @@ -523,11 +523,13 @@ This initial deployment is done manually. After that, the deployment is maintain ### Preparation +Before you can create the EKS cluster, you must install `eksctl`, `kubectl`, `helm`, `jp`, and define some environment variables. + #### Install eksctl See the [installation instructions](https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html) -If you use Ubuntu, you can just issue the command: +If you use Ubuntu, you can issue the command: ```bash make install-eksctl @@ -537,7 +539,7 @@ make install-eksctl See the [installation instructions](https://kubernetes.io/docs/tasks/tools/#kubectl) -If you use Ubuntu, you can just issue the command: +If you use Ubuntu, you can issue the command: ```bash make install-kubectl @@ -547,7 +549,7 @@ make install-kubectl See the [installation instructions](https://helm.sh/docs/intro/install/) -If you use Ubuntu, you can just issue the command: +If you use Ubuntu, you can issue the command: ```bash make install-helm @@ -557,7 +559,7 @@ make install-helm See the [installation instructions](https://github.com/jmespath/jp#installing) -If you use Ubuntu, you can just issue the command: +If you use Ubuntu, you can issue the command: ```bash make install-jp @@ -717,7 +719,7 @@ make rasa-enterprise-smoketest ### DNS -Define a DNS record of type CNAME with your domain service provider: +Optionally, if you want to access Rasa Enterprise at your own (sub)-domain name, define a DNS record of type CNAME with your domain service provider: - **name of sub-domain**: `aws-financial-demo` @@ -786,11 +788,11 @@ The EKS Control Plane interacts with the the EKS Data Plane (the nodes), like th ## Appendix B: Manual Cleanup of AWS resources -Sometimes things do not clean up properly, and you need to do a manual cleanup in the **AWS console**: +Sometimes things do not clean up properly and you will need to do a manual cleanup in the **AWS console**: -- **CloudFormation**: Try to delete all the stacks in reverse order as they were created by the eksctl command. +**CloudFormation**: Try to delete all the stacks in reverse order as they were created by the eksctl command. -- When a stack fails to delete due to dependencies, you have two options: +When a stack fails to delete due to dependencies, you have two options: - Select to retain the resources that have dependency errors. (**NOT RECOMMENDED**)