From 4f6dc3686900b5441ca3a3b33f814ced6fe184bb Mon Sep 17 00:00:00 2001 From: Ivan Lopez Date: Fri, 3 Dec 2021 15:58:47 +0100 Subject: [PATCH] Deploy demo (#1837) * Add GH Actions workflow for demo deployment * update demo ec2 instance type * remove redundant docker-compose build * add custom demo command and env vars * deploy demo on updates to workflow resources --- .github/workflows/demo.yaml | 40 +++++ .../workflows/demo/docker-compose.demo.yml | 16 ++ .../workflows/demo/ec2-autoscaling-group.yaml | 158 ++++++++++++++++++ docker-compose-gpu.yml | 2 +- docker-compose.yml | 2 +- ui/webapp.py | 34 ++-- 6 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/demo.yaml create mode 100644 .github/workflows/demo/docker-compose.demo.yml create mode 100644 .github/workflows/demo/ec2-autoscaling-group.yaml diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml new file mode 100644 index 000000000..4a6d89f79 --- /dev/null +++ b/.github/workflows/demo.yaml @@ -0,0 +1,40 @@ +name: Demo + +on: + workflow_dispatch: + push: + branches: + - master + paths: + - ".github/workflows/demo**" + +env: + AWS_REGION: eu-west-1 + +permissions: + id-token: write + contents: read + +jobs: + deploy: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: AWS Authentication + uses: aws-actions/configure-aws-credentials@master + with: + aws-region: ${{ env.AWS_REGION }} + role-to-assume: ${{ secrets.DEMO_AWS_DEPLOY_ROLE }} + + - name: Deploy demo + env: + CF_KEY_NAME: ${{ secrets.DEMO_CF_KEY_NAME }} + CF_IMAGE_ID: ${{ secrets.DEMO_CF_IMAGE_ID }} + CF_IAM_INSTANCE_PROFILE: ${{ secrets.DEMO_CF_IAM_INSTANCE_PROFILE }} + run: | + aws cloudformation deploy \ + --template-file .github/workflows/demo/ec2-autoscaling-group.yaml \ + --stack-name haystack-demo-production-instance \ + --parameter-overrides CommitShortSHA=${{ github.sha }} KeyName=${CF_KEY_NAME} ImageId=${CF_IMAGE_ID} IamInstanceProfile=${CF_IAM_INSTANCE_PROFILE} \ + --capabilities CAPABILITY_IAM diff --git a/.github/workflows/demo/docker-compose.demo.yml b/.github/workflows/demo/docker-compose.demo.yml new file mode 100644 index 000000000..60a0a29ba --- /dev/null +++ b/.github/workflows/demo/docker-compose.demo.yml @@ -0,0 +1,16 @@ +version: "3" +services: + haystack-api: + restart: always + environment: + CONCURRENT_REQUEST_PER_WORKER: 16 + command: "/bin/bash -c 'sleep 10 && gunicorn rest_api.application:app -b 0.0.0.0 -k uvicorn.workers.UvicornWorker --workers 3 --timeout 180'" + + elasticsearch: + restart: always + ui: + restart: always + environment: + DEFAULT_DOCS_FROM_RETRIEVER: 7 + DEFAULT_NUMBER_OF_ANSWERS: 5 + DISABLE_FILE_UPLOAD: 1 diff --git a/.github/workflows/demo/ec2-autoscaling-group.yaml b/.github/workflows/demo/ec2-autoscaling-group.yaml new file mode 100644 index 000000000..95b7add56 --- /dev/null +++ b/.github/workflows/demo/ec2-autoscaling-group.yaml @@ -0,0 +1,158 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/awslabs/goformation/v5.2.11/schema/cloudformation.schema.json + + +Parameters: + Project: + Description: A project name that is used for resource names + Type: String + Default: haystack-demo + + Environment: + Description: An environment name that is suffixed to resource names + Type: String + Default: production + + VPCStack: + Description: VPC stack name + Type: String + Default: haystack-demo-production-vpc + + CommitShortSHA: + Description: Commit short reference that triggered this deployment + Type: String + + InstanceType: + Description: EC2 instance type + Type: String + Default: p3.2xlarge + + ImageId: + Description: AMI to use for the EC2 instance + Type: String + + IamInstanceProfile: + Description: IAM instance profile to attach to the EC2 instance + Type: String + + KeyName: + Description: EC2 key pair to add to the EC2 instance + Type: String + +Resources: + AutoScalingGroup: + Type: AWS::AutoScaling::AutoScalingGroup + CreationPolicy: + ResourceSignal: + Count: "1" + Timeout: PT45M + UpdatePolicy: + AutoScalingRollingUpdate: + MinInstancesInService: "1" + MaxBatchSize: "1" + PauseTime: PT45M + WaitOnResourceSignals: true + SuspendProcesses: + - HealthCheck + - ReplaceUnhealthy + - AZRebalance + - AlarmNotification + - ScheduledActions + Properties: + LaunchConfigurationName: !Ref InstanceConfiguration + VPCZoneIdentifier: + - !ImportValue + "Fn::Sub": "${VPCStack}-PublicSubnet1" + - !ImportValue + "Fn::Sub": "${VPCStack}-PublicSubnet2" + MaxSize: "2" + DesiredCapacity: "1" + MinSize: "1" + TargetGroupARNs: + - !ImportValue + "Fn::Sub": "${VPCStack}-DefaultTargetGroup" + Tags: + - Key: Name + Value: !Sub ${Project}-${Environment} + PropagateAtLaunch: true + - Key: Environment + Value: !Ref Project + PropagateAtLaunch: true + - Key: Project + Value: !Ref Environment + PropagateAtLaunch: true + - Key: CommitShortSHA + Value: !Ref CommitShortSHA + PropagateAtLaunch: true + + InstanceConfiguration: + Type: AWS::AutoScaling::LaunchConfiguration + Properties: + LaunchConfigurationName: !Sub ${Project}-${Environment}-${CommitShortSHA} + InstanceType: !Ref InstanceType + ImageId: !Ref ImageId + IamInstanceProfile: !Ref IamInstanceProfile + KeyName: !Ref KeyName + AssociatePublicIpAddress: true + SecurityGroups: + - !ImportValue + "Fn::Sub": "${VPCStack}-InstanceSecurityGroup" + EbsOptimized: true + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeSize: 200 + + UserData: + Fn::Base64: !Sub | + #!/bin/bash -ex + mkdir -p /opt/aws/bin + wget https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz + python3 -m easy_install --script-dir /opt/aws/bin aws-cfn-bootstrap-py3-latest.tar.gz + + trap '/opt/aws/bin/cfn-signal --exit-code 1 --stack ${AWS::StackId} --resource AutoScalingGroup --region ${AWS::Region}' ERR + + echo "Deploying Haystack demo, commit ${CommitShortSHA}" + + echo 'APT::Periodic::Update-Package-Lists "0"; + APT::Periodic::Unattended-Upgrade "0";' > /etc/apt/apt.conf.d/20auto-upgrades + + apt update + apt install -y curl git ca-certificates curl gnupg lsb-release + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + apt update + apt install -y docker-ce docker-ce-cli containerd.io + + # Install Docker compose + curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/bin/docker-compose + chmod +x /usr/bin/docker-compose + + # Install Nvidia container runtime + curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | \ + apt-key add - + distribution=$(. /etc/os-release;echo $ID$VERSION_ID) + curl -s -L https://nvidia.github.io/nvidia-container-runtime/$distribution/nvidia-container-runtime.list | \ + tee /etc/apt/sources.list.d/nvidia-container-runtime.list + apt-get update + apt-get install -y nvidia-container-runtime + + # Setup and start Docker + groupadd docker || true + usermod -aG docker $USER || true + newgrp docker || true + systemctl unmask docker + systemctl restart docker + + # Exposes the GPUs to Docker + docker run --rm --gpus all ubuntu nvidia-smi + + # Clone and start Haystack + git clone --branch deploy-demo https://github.com/askainet/haystack.git /opt/haystack + cd /opt/haystack + export COMPOSE_FILE=docker-compose-gpu.yml:.github/workflows/demo/docker-compose.demo.yml + docker-compose pull + docker-compose up -d + + /opt/aws/bin/cfn-signal --exit-code $? --stack ${AWS::StackId} --resource AutoScalingGroup --region ${AWS::Region} diff --git a/docker-compose-gpu.yml b/docker-compose-gpu.yml index 3ffdc91ea..b5850cbde 100644 --- a/docker-compose-gpu.yml +++ b/docker-compose-gpu.yml @@ -32,7 +32,7 @@ services: elasticsearch: # This will start an empty elasticsearch instance (so you have to add your documents yourself) #image: "elasticsearch:7.9.2" - # If you want a demo image instead that is "ready-to-query" with some indexed articles + # If you want a demo image instead that is "ready-to-query" with some indexed articles # about countries and capital cities from Wikipedia: image: "deepset/elasticsearch-countries-and-capitals" ports: diff --git a/docker-compose.yml b/docker-compose.yml index b27fd1e06..5b91d43c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: elasticsearch: # This will start an empty elasticsearch instance (so you have to add your documents yourself) #image: "elasticsearch:7.9.2" - # If you want a demo image instead that is "ready-to-query" with some indexed articles + # If you want a demo image instead that is "ready-to-query" with some indexed articles # about countries and capital cities from Wikipedia: image: "deepset/elasticsearch-countries-and-capitals" ports: diff --git a/ui/webapp.py b/ui/webapp.py index 7d3b0baf0..f28935598 100644 --- a/ui/webapp.py +++ b/ui/webapp.py @@ -36,7 +36,7 @@ def main(): # Persistent state state = SessionState.get( - random_question=DEFAULT_QUESTION_AT_STARTUP, + random_question=DEFAULT_QUESTION_AT_STARTUP, random_answer="", last_question=DEFAULT_QUESTION_AT_STARTUP, results=None, @@ -51,7 +51,7 @@ def main(): # Title st.write("# Haystack Demo - Explore the world") st.markdown(""" -This demo takes its data from a selection of Wikipedia pages crawled in November 2021 on the topic of +This demo takes its data from a selection of Wikipedia pages crawled in November 2021 on the topic of

Countries and capital cities

@@ -63,18 +63,18 @@ Ask any question on this topic and see if Haystack can find the correct answer t # Sidebar st.sidebar.header("Options") top_k_reader = st.sidebar.slider( - "Max. number of answers", - min_value=1, - max_value=10, - value=DEFAULT_NUMBER_OF_ANSWERS, - step=1, + "Max. number of answers", + min_value=1, + max_value=10, + value=DEFAULT_NUMBER_OF_ANSWERS, + step=1, on_change=reset_results) top_k_retriever = st.sidebar.slider( - "Max. number of documents from retriever", - min_value=1, - max_value=10, - value=DEFAULT_DOCS_FROM_RETRIEVER, - step=1, + "Max. number of documents from retriever", + min_value=1, + max_value=10, + value=DEFAULT_DOCS_FROM_RETRIEVER, + step=1, on_change=reset_results) eval_mode = st.sidebar.checkbox("Evaluation mode") debug = st.sidebar.checkbox("Show debug info") @@ -107,7 +107,7 @@ Ask any question on this topic and see if Haystack can find the correct answer t text-align: center; }} .haystack-footer h4 {{ - margin: 0.1rem; + margin: 0.1rem; padding:0; }} footer {{ @@ -132,7 +132,7 @@ Ask any question on this topic and see if Haystack can find the correct answer t # Search bar question = st.text_input("", value=state.random_question, - max_chars=100, + max_chars=100, on_change=reset_results ) col1, col2 = st.columns(2) @@ -147,7 +147,7 @@ Ask any question on this topic and see if Haystack can find the correct answer t #state.get_next_question = col2.button("Random question") if col2.button("Random question"): reset_results() - new_row = df.sample(1) + new_row = df.sample(1) while new_row["Question Text"].values[0] == state.random_question: # Avoid picking the same question twice (the change is not visible on the UI) new_row = df.sample(1) state.random_question = new_row["Question Text"].values[0] @@ -200,7 +200,7 @@ Ask any question on this topic and see if Haystack can find the correct answer t answer, context = result["answer"], result["context"] start_idx = context.find(answer) end_idx = start_idx + len(answer) - # Hack due to this bug: https://github.com/streamlit/streamlit/issues/3190 + # Hack due to this bug: https://github.com/streamlit/streamlit/issues/3190 st.write(markdown(context[:start_idx] + str(annotation(answer, "ANSWER", "#8ef")) + context[end_idx:]), unsafe_allow_html=True) source = "" url, title = get_backlink(result) @@ -213,7 +213,7 @@ Ask any question on this topic and see if Haystack can find the correct answer t else: st.info("🤔    Haystack is unsure whether any of the documents contain an answer to your question. Try to reformulate it!") st.write("**Relevance:** ", result["relevance"]) - + if eval_mode and result["answer"]: # Define columns for buttons is_correct_answer = None