Skip to content

Commit

Permalink
git: Deploy using AWS Lightsail (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
yoomlam authored Jun 4, 2024
1 parent f1464cd commit f651a88
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 45 deletions.
244 changes: 206 additions & 38 deletions .github/workflows/push-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,30 @@ on:
options:
- '05-assistive-chatbot'
- '02-household-queries'
subdomain:
description: 'Subdomain of navalabs.co'
type: choice
required: true
default: 'chat'
options:
- 'chat'
- 'chatbot'
- 'chatbdt'
- 'chat-bdt'
- 'bdtbot'
- 'bdt-bot'
- 'bdt-chat'
- 'bdt-chatbot'
- 'chatbot-prototype'
- 'chat.zone'
service_name:
description: 'Name of target AWS service. Leave blank if unsure.'
description: 'Name of target service. Leave blank if unsure'
type: choice
default: ''
options:
- ''
- 'container-service-3'
- 'container-service-2'
- 'chatbot-chainlit-svc'
- 'secure-chatbot-svc'
build_image:
Expand All @@ -27,51 +46,148 @@ on:
description: "Deploy image"
required: true
type: boolean
default: 'true'
# image_tag:
# description: 'Tag/Version of the image to push'
# required: true
# type: string
# default: '0.06'
create_new_svc:
description: "Create new Lightsail service"
required: true
type: boolean
default: 'false'
delete_images:
description: 'Delete previous images associated with service'
required: true
type: boolean
default: 'false'

env:
AWS_REGION: us-east-1
IMAGE_NAME: localimage

jobs:
publish-image:
runs-on: ubuntu-latest
steps:
- name: Check inputs
id: check_inputs
run: |
service_name="${{ inputs.service_name }}"
if [ "${service_name}" = "" ]; then
case "${{ inputs.dockerfile_folder }}" in
'02-household-queries') service_name='secure-chatbot-svc';;
'05-assistive-chatbot') service_name='chatbot-chainlit-svc';;
*) echo "Unknown dockerfile_folder: '${dockerfile_folder}'"; exit 2;;
esac
fi
echo "service_name=$service_name" >> $GITHUB_OUTPUT
case "${service_name}" in
# The image_tag is specific to the `*-svc` service
'secure-chatbot-svc') image_tag='0.01';;
'chatbot-chainlit-svc') image_tag='chatbot-chainlit';;
*) echo "Unknown service_name: '${service_name}'"; exit 3;;
esac
echo "image_tag=$image_tag" >> $GITHUB_OUTPUT
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-east-1
aws-region: ${{ env.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
mask-aws-account-id: true
# TODO: secure credentials: https://github.com/aws-actions/amazon-ecr-login?tab=readme-ov-file#ecr-private
# https://github.com/docker/login-action?tab=readme-ov-file#aws-elastic-container-registry-ecr
# https://medium.com/@lukhee/automating-aws-lightsail-deployments-with-github-actions-53c73c9a1c1f

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
with:
mask-password: true

- name: "Upgrade AWS CLI version and setup lightsailctl"
run: |
# aws --version
# curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
# unzip awscliv2.zip
# sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update
# which aws
aws --version
sudo curl "https://s3.us-west-2.amazonaws.com/lightsailctl/latest/linux-amd64/lightsailctl" -o "/usr/local/bin/lightsailctl"
sudo chmod +x /usr/local/bin/lightsailctl
aws lightsail push-container-image help
- name: Check inputs
id: check_inputs
run: |
service_name="${{ inputs.subdomain }}-svc"
# if [ "${service_name}" = "" ]; then
# case "${{ inputs.dockerfile_folder }}" in
# '02-household-queries') service_name='secure-chatbot-svc';;
# '05-assistive-chatbot') service_name='chatbot-chainlit-svc';;
# *) exit 1;;
# esac
# fi
echo "service_name=$service_name" >> $GITHUB_OUTPUT
if [ ${{ inputs.create_new_svc }} == 'false' ]; then
echo "Since not creating new service, checking if service '$service_name' exists"
aws lightsail get-container-services --service-name "$service_name"
fi
# image_tag="${{ inputs.image_tag }}"
# if [ "${image_tag}" = "" ]; then
# case "${service_name}" in
# # The image_tag is specific to the `*-svc` service
# 'secure-chatbot-svc') image_tag='0.01';;
# 'chatbot-chainlit-svc') image_tag='chatbot-chainlit';;
# container-service-*) image_tag='not-used';;
# *) echo "Unknown service_name: '${service_name}'"; exit 3;;
# esac
# fi
# echo "image_tag=$image_tag" >> $GITHUB_OUTPUT
- name: "Create new Lightsail container service"
if: inputs.create_new_svc
env:
DOMAIN_NAME: navalabs.co
FULL_DOMAIN: ${{ inputs.subdomain }}.navalabs.co
SERVICE_NAME: ${{ steps.check_inputs.outputs.service_name }}
run: |
# check if service already exists
if aws lightsail get-container-services --service-name "$SERVICE_NAME" > /dev/null; then
echo "Already exists: $SERVICE_NAME!"
else
# `micro` power is needed for it's memory capacity; 60%+ memory is needed for the vector DB
aws lightsail create-container-service --service-name $SERVICE_NAME --power micro --scale 1 --public-domain-names navalabs-cert=$FULL_DOMAIN
echo "Waiting for service to be ready before updating container service"
while true; do
sleep 15
SVC_STATE=$(aws lightsail get-container-services --service-name "$SERVICE_NAME" | jq -r '.containerServices[0].state')
echo "service state: $SVC_STATE"
if [ "$SVC_STATE" == "READY" ]; then
break
fi
done
SVC_URL=$(aws lightsail get-container-services --service-name "$SERVICE_NAME" | jq -r '.containerServices[0].url')
# Remove 'https://' prefix
URL_DOMAIN=${SVC_URL#https://}
# Remove '/' suffix
TARGET_DOMAIN=${URL_DOMAIN%/}
# If domain entry exists, delete it
OLD_TARGET=$(aws lightsail get-domain --domain-name $DOMAIN_NAME | jq -r ".domain.domainEntries[] | select( .name == \"$FULL_DOMAIN\" ) | .target")
if [ "$OLD_TARGET" ] ; then
echo "Deleting existing '$FULL_DOMAIN' entry with target '$OLD_TARGET'"
aws lightsail delete-domain-entry --domain-name $DOMAIN_NAME --domain-entry "type=A,isAlias=true,name=$FULL_DOMAIN,target=$OLD_TARGET"
fi
echo "Creating DNS assignment by adding a domain entry $FULL_DOMAIN to target $TARGET_DOMAIN"
aws lightsail create-domain-entry --domain-name $DOMAIN_NAME --domain-entry "type=A,isAlias=true,name=$FULL_DOMAIN,target=$TARGET_DOMAIN"
fi
- name: "Delete previous container images"
if: inputs.delete_images
env:
SERVICE_NAME: ${{ steps.check_inputs.outputs.service_name }}
run: |
AWS_IMAGES=$(aws lightsail get-container-images --region "$AWS_REGION" --service-name "$SERVICE_NAME" --output text)
IMAGE_NAMES=$(echo $AWS_IMAGES | grep -Eo ':"$SERVICE_NAME"\.${{ inputs.image-name }}\.[0-9]+')
echo $IMAGE_NAMES
FIRST=0
while read LINE; do
if [ "$FIRST" -ne 0 ]; then
aws lightsail delete-container-image --region "$AWS_REGION" --service-name "$SERVICE_NAME" --image $LINE;
fi
FIRST=1;
done <<< $IMAGE_NAMES
# - name: "Login to Amazon ECR"
# id: login-ecr
# uses: aws-actions/amazon-ecr-login@v2
# with:
# mask-password: true

- name: "Checkout source code"
if: inputs.build_image
Expand All @@ -84,23 +200,75 @@ jobs:
# TODO: make this more easily editable and secure
# The DOT_ENV_FILE_CONTENTS contains LITERAL_API_KEY, OPENAI_API_KEY, RETRIEVE_K, LLM_MODEL_NAME, SUMMARIZER_LLM_MODEL_NAME
echo "${{secrets.DOT_ENV_FILE_CONTENTS}}" > .env
echo "BUILD_DATE=$(date +%Y-%m-%d-%T)" >> .env
echo "GIT_SHA=${{ github.sha }}" >> .env
docker build -t "$IMAGE_NAME" --build-arg GURU_CARDS_URL="https://docs.google.com/uc?export=download&id=${{ secrets.GURU_CARDS_URL_ID }}" .
- name: "Publish image to AWS ECR'"
id: publish_image
- name: "Publish image to Lightsail"
if: inputs.build_image
id: pub_image_to_ls
env:
ECR_PATH: ${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPO }}
ECR_PATH: ${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPO }}
SERVICE_NAME: ${{ steps.check_inputs.outputs.service_name }}
# IMAGE_TAG: ${{ steps.check_inputs.outputs.image_tag }}
# LABEL must match regex ^(?:[a-z0-9]{1,2}|[a-z0-9][a-z0-9-]+[a-z0-9])$
LABEL: git-push
IMAGE_SHA_TAG: ${{ github.sha }}
run: |
image_tag="${{ steps.check_inputs.outputs.image_tag }}"
echo "# Publishing image ${image_tag} to $ECR_PATH"
echo "# Publishing image for $SERVICE_NAME"
aws lightsail push-container-image --region $AWS_REGION --service-name "$SERVICE_NAME" --label "$LABEL" --image "$IMAGE_NAME"
docker tag "$IMAGE_NAME" "$ECR_PATH:${image_tag}"
docker push "$ECR_PATH:${image_tag}"
LS_DOCKER_IMAGE=$(aws lightsail get-container-images --service-name "$SERVICE_NAME" | jq -r .containerImages[0].image)
echo "Image name: '$LS_DOCKER_IMAGE'"
echo "LS_DOCKER_IMAGE=$LS_DOCKER_IMAGE" >> $GITHUB_ENV
- name: "Update AWS Service"
- name: Deploy container on AWS Lightsail
if: inputs.deploy_image
env:
CLUSTER_NAME: genai-experiments
SERVICE_NAME: ${{ steps.check_inputs.outputs.service_name }}
run: |
aws ecs update-service --force-new-deployment --cluster "$CLUSTER_NAME" --service "${{ steps.check_inputs.outputs.service_name }}"
TEMPLATE='{
"serviceName": "$SERVICE_NAME",
"containers": {
"chatbot": {
"image": "$LS_DOCKER_IMAGE",
"command": [],
"environment": {
"ENV": "PROD",
"BUILD_DATE": "$BUILD_DATE"
},
"ports": {
"8000": "HTTP"
}
}
},
"publicEndpoint": {
"containerName": "chatbot",
"containerPort": 8000,
"healthCheck": {
"healthyThreshold": 2,
"unhealthyThreshold": 4,
"timeoutSeconds": 20,
"intervalSeconds": 60,
"path": "/healthcheck",
"successCodes": "200-499"
}
}
}'
echo "$TEMPLATE" | BUILD_DATE=$(date +%Y-%m-%d-%T%z) envsubst > config.json
cat config.json
aws lightsail create-container-service-deployment --cli-input-json file://config.json
# aws lightsail create-container-service-deployment --region ${{ inputs.aws-region }} --cli-input-json '${{ inputs.aws-lightsail-service-config }}' > /dev/null
# aws lightsail update-container-service --service-name "$SERVICE_NAME" --no-is-disabled
# TODO: Wait for deployment to complete
# TODO: warm up vector DB on startup
# - name: "Update AWS Service"
# if: inputs.deploy_image
# env:
# CLUSTER_NAME: genai-experiments
# run: |
# aws ecs update-service --force-new-deployment --cluster "$CLUSTER_NAME" --service "${{ steps.check_inputs.outputs.service_name }}"
7 changes: 7 additions & 0 deletions 05-assistive-chatbot/.env-DEV
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

ENABLE_CHATBOT_API=False

CHAT_ENGINE='Summaries'
LLM_MODEL_NAME='openai :: gpt-3.5-turbo-instruct'
RETRIEVE_K=4
SUMMARIZER_LLM_MODEL_NAME='openai :: gpt-3.5-turbo-instruct'
8 changes: 8 additions & 0 deletions 05-assistive-chatbot/.env-PROD
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ENABLE_CHATBOT_API=True

CHATBOT_LOG_LEVEL='DEBUG'

CHAT_ENGINE='Summaries'
LLM_MODEL_NAME='openai :: gpt-3.5-turbo-instruct'
RETRIEVE_K=4
SUMMARIZER_LLM_MODEL_NAME='openai :: gpt-3.5-turbo-instruct'
12 changes: 8 additions & 4 deletions 05-assistive-chatbot/chatbot-chainlit.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import logging
import os
import pprint

import chainlit as cl
Expand All @@ -23,13 +24,13 @@

logger.info("Chatbot API loaded: %s", chatbot_api.__name__)

## TODO: Enable users to log in so that they can be distinguished in GetLiteralAI feedback logs


@cl.on_chat_start
async def init_chat():
elements = [
cl.Text(name="side-text", display="side", content="Side Text"),
]
await cl.Message("Example of side-text", elements=elements).send()
build_date = os.environ.get("BUILD_DATE", "unknown")
await cl.Message(f"Welcome to the Assistive Chat prototype (built {build_date})").send()

# https://docs.chainlit.io/api-reference/chat-settings
chat_settings = cl.ChatSettings(
Expand Down Expand Up @@ -124,6 +125,9 @@ async def message_submitted(message: cl.Message):
if not cl.user_session.get("settings_applied", False):
return

# TODO: Provide visual feedback that chatbot is working, e.g., add Chainlit spinner
# TODO: Send results as they are generated

chat_engine = cl.user_session.get("chat_engine")
response = chat_engine.gen_response(message.content)

Expand Down
9 changes: 7 additions & 2 deletions 05-assistive-chatbot/chatbot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
from datetime import date

import dotenv

Expand All @@ -22,9 +23,11 @@ def configure_logging():
logging.info("Configured logging level: %s", log_level)


env = os.environ.get("ENV", "DEV")
print(f"Loading .env-{env}")
dotenv.load_dotenv(f".env-{env}")
dotenv.load_dotenv()
configure_logging()

logger = logging.getLogger(__name__)


Expand All @@ -40,13 +43,15 @@ def configure_logging():
# Set to true to enable caching for faster responses and optimizing prompts using DSPy
os.environ.setdefault("DSP_CACHEBOOL", "false")

os.environ.setdefault("BUILD_DATE", str(date.today()))


@utils.verbose_timer(logger)
def _init_settings():
# Remember to update ChatSettings in chatbot-chainlit.py when adding new settings
# and update chatbot/engines/__init.py:CHATBOT_SETTING_KEYS
return {
"env": os.environ.get("ENV", "DEV"),
"env": env,
"enable_api": is_true(os.environ.get("ENABLE_CHATBOT_API", "False")),
"chat_engine": os.environ.get("CHAT_ENGINE", "Direct"),
"model": os.environ.get("LLM_MODEL_NAME", "mock :: llm"),
Expand Down
Loading

0 comments on commit f651a88

Please sign in to comment.