mirror of
https://github.com/Hestia-Homes/ML.git
synced 2026-06-30 13:10:43 +00:00
formatted
This commit is contained in:
parent
f67b138406
commit
e2ce04aa0d
5 changed files with 307 additions and 3 deletions
115
.github/workflows/Deploy.yml
vendored
Normal file
115
.github/workflows/Deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
name: Sap Change Model Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ sap_change-dev, sap_change-prod ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.10.12
|
||||||
|
|
||||||
|
- name: Install Serverless and plugins
|
||||||
|
run: |
|
||||||
|
npm install -g serverless
|
||||||
|
npm install -g serverless-domain-manager
|
||||||
|
|
||||||
|
- name: AWS credentials for dev
|
||||||
|
if: github.ref == 'refs/heads/dev'
|
||||||
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
|
with:
|
||||||
|
aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
|
||||||
|
aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
aws-region: eu-west-2
|
||||||
|
|
||||||
|
- name: AWS credentials for prod
|
||||||
|
if: github.ref == 'refs/heads/prod'
|
||||||
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
|
with:
|
||||||
|
aws-access-key-id: ${{ secrets.PROD_AWS_ACCESS_KEY_ID }}
|
||||||
|
aws-secret-access-key: ${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
aws-region: eu-west-2
|
||||||
|
|
||||||
|
# Set up all of the secrets required for the deployment
|
||||||
|
- name: set secret prefix which is used across multiple steps
|
||||||
|
id: secret_prefix
|
||||||
|
run: |
|
||||||
|
# Convert branch name to uppercase and replace hyphens with underscores
|
||||||
|
echo "::set-output name=secret_prefix::$(echo "${{ github.ref_name }}" | tr 'a-z-' 'A-Z_')"
|
||||||
|
|
||||||
|
- name: Set domain name
|
||||||
|
id: set_domain
|
||||||
|
run: echo "::set-output name=domain::${{ secrets[format('{0}_DOMAIN_NAME', steps.secret_prefix.outputs.secret_prefix)] }}"
|
||||||
|
|
||||||
|
- name: Set ECR credentials
|
||||||
|
id: set_ecr_credentials
|
||||||
|
run: |
|
||||||
|
# Fetch the secret using the secret prefix
|
||||||
|
echo "::set-output name=ecr_uri::${{ secrets[format('{0}_ECR_URI', steps.secret_prefix.outputs.secret_prefix)] }}"
|
||||||
|
|
||||||
|
- name: Set S3 buckets
|
||||||
|
id: set_s3_buckets
|
||||||
|
run: |
|
||||||
|
# Fetch the secret using the secret prefix
|
||||||
|
echo "::set-output name=data_bucket::${{ secrets[format('{0}_DATA_BUCKET', steps.secret_prefix.outputs.secret_prefix)] }}"
|
||||||
|
echo "::set-output name=predictions_bucket::${{ secrets[format('{0}_PREDICTIONS_BUCKET', steps.secret_prefix.outputs.secret_prefix)] }}"
|
||||||
|
echo "::set-output name=model_directory_bucket::${{ secrets[format('{0}_MODEL_DIRECTORY_BUCKET', steps.secret_prefix.outputs.secret_prefix)] }}"
|
||||||
|
|
||||||
|
- name: Set stack_name
|
||||||
|
id: set_stack_name
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.ref_name }}" == "sap_change-dev" || "${{ github.ref_name }}" == "sap_change-prod" ]]; then
|
||||||
|
echo "::set-output name=stack_name::sapmodel"
|
||||||
|
else
|
||||||
|
echo "::set-output name=stack_name::"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Set runtime_environment
|
||||||
|
id: set_runtime_environment
|
||||||
|
run: |
|
||||||
|
# Extract the suffix after the hyphen from the branch name
|
||||||
|
runtime_environment=$(echo "${{ github.ref_name }}" | awk -F'-' '{print $NF}')
|
||||||
|
echo "::set-output name=runtime_environment::$runtime_environment"
|
||||||
|
|
||||||
|
|
||||||
|
- name: Setup Docker
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to ECR
|
||||||
|
run: |
|
||||||
|
aws ecr get-login-password --region eu-west-2 | docker login --username AWS --password-stdin ${{ steps.set_ecr_credentials.outputs.ecr_uri }}
|
||||||
|
|
||||||
|
# Building and pushing Docker image with caching
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: ./model_data/simulation_system
|
||||||
|
file: ./model_data/simulation_system/Dockerfiles/Dockerfile.prediction.lambda
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.set_ecr_credentials.outputs.ecr_uri }}:${{ github.sha }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
platform: linux/amd64
|
||||||
|
provenance: false
|
||||||
|
|
||||||
|
- name: Deploy to AWS Lambda via Serverless
|
||||||
|
env:
|
||||||
|
RUNTIME_ENVIRONMENT: ${{ steps.set_runtime_environment.outputs.runtime_environment }}
|
||||||
|
MODEL_DIRECTORY_BUCKET: ${{ steps.s3_buckets.outputs.model_directory_bucket }}
|
||||||
|
PREDICTIONS_BUCKET: ${{ steps.s3_buckets.outputs.predictions_bucket }}
|
||||||
|
DATA_BUCKET: ${{ steps.s3_buckets.outputs.data_bucket }}
|
||||||
|
DOMAIN_NAME: ${{ steps.set_domain.outputs.domain }}
|
||||||
|
ECR_URI: ${{ steps.set_ecr_credentials.outputs.ecr_uri }}
|
||||||
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
|
STACK_NAME: ${{ steps.set_stack_name.outputs.stack_name }}
|
||||||
|
run: |
|
||||||
|
# Deploy to AWS Lambda via Serverless
|
||||||
|
sls deploy --config serverless.yml --stage ${{ steps.set_runtime_environment.outputs.runtime_environment }} --verbose
|
||||||
21
README.md
21
README.md
|
|
@ -19,5 +19,24 @@ These branches will differ by the configuration files that define the data used
|
||||||
|
|
||||||
# Deployment
|
# Deployment
|
||||||
|
|
||||||
TBD
|
Scripts associated to deployment can be found in the deployment/ folder.
|
||||||
|
|
||||||
|
Deployment is automated via Github Actions, where a deployment is triggered by a push to one of the
|
||||||
|
protected branch, with one of dev or prod as the suffix, describing the target environment.
|
||||||
|
|
||||||
|
The github actions file will build and push a docker image to ECR and then deploy a lambda
|
||||||
|
which produces predictions for the relevant model.
|
||||||
|
|
||||||
|
In order for this to be set up, some key environment variables needs to be inserted into Github
|
||||||
|
secrets. Each different model and protected branch has its own set of secrets which allows for flexibility
|
||||||
|
between different pipelines.
|
||||||
|
|
||||||
|
For example, for the branch sap_change-dev, the prefix=SAP_CHANGE_DEV, and the following secrets are:
|
||||||
|
|
||||||
|
- {prefix}_ECR_URI, which is the URI of the ECR repository to push to. For example, for the
|
||||||
|
sap change model this is the lambda-sap-prediction-dev repository.
|
||||||
|
- {prefix}_DOMAIN_NAME, is the custom domain name. This is likely going to be the same across the different
|
||||||
|
models, but is still included in the secrets for flexibility.
|
||||||
|
- {prefix}_DATA_BUCKET, is the name of the s3 data bucket where data to be scored by the model is stored
|
||||||
|
- {prefix}_MODEL_BUCKET, is the name of the s3 bucket where the model is stored
|
||||||
|
- {prefix}_PREDICTIONS_BUCKET, is the name of the s3 bucket where the predictions are stored
|
||||||
|
|
|
||||||
21
deployment/Dockerfile.prediction.lambda
Normal file
21
deployment/Dockerfile.prediction.lambda
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
FROM public.ecr.aws/lambda/python:3.10
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR ${LAMBDA_TASK_ROOT}
|
||||||
|
ENV PYTHONPATH "${PYTHONPATH}:${LAMBDA_TASK_ROOT}"
|
||||||
|
|
||||||
|
# Install necessary build tools - required to test locally
|
||||||
|
RUN yum install -y gcc python3-devel
|
||||||
|
|
||||||
|
# Install python packages
|
||||||
|
COPY modules/ml-pipeline/src/requirements/predictions/requirements.txt ./requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r ./requirements.txt
|
||||||
|
|
||||||
|
# Copy the project code
|
||||||
|
COPY modules/ml-pipeline/src/pipeline ./pipeline
|
||||||
|
# Copy the handler
|
||||||
|
COPY deployment/handlers/prediction_app.py prediction_app.py
|
||||||
|
# Get the model
|
||||||
|
# RUN dev pull -r ${RUNTIME_ENVIRONMENT}
|
||||||
|
|
||||||
|
CMD [ "prediction_handler.handler" ]
|
||||||
93
deployment/handlers/prediction_app.py
Normal file
93
deployment/handlers/prediction_app.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""
|
||||||
|
This script is the handler for the lambda prediction function, responsible
|
||||||
|
for producting predictions for a model
|
||||||
|
"""
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
from botocore.exceptions import NoCredentialsError
|
||||||
|
import json
|
||||||
|
from io import StringIO
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
RUNTIME_ENVIRONMENT = os.environ.get("RUNTIME_ENVIRONMENT", "dev")
|
||||||
|
|
||||||
|
|
||||||
|
def upload_dataframe_to_s3(df, bucket, s3_file_name):
|
||||||
|
"""
|
||||||
|
Upload a pandas DataFrame to an S3 bucket as CSV
|
||||||
|
|
||||||
|
:param df: DataFrame to upload
|
||||||
|
:param bucket: Bucket to upload to
|
||||||
|
:param s3_file_name: S3 object name
|
||||||
|
:return: True if file was uploaded, else False
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Initialize the S3 client
|
||||||
|
s3 = boto3.client("s3")
|
||||||
|
csv_buffer = StringIO()
|
||||||
|
|
||||||
|
# Write the DataFrame to the buffer as CSV
|
||||||
|
df.to_csv(csv_buffer, index=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Upload the CSV from the buffer to S3
|
||||||
|
s3.put_object(Bucket=bucket, Key=s3_file_name, Body=csv_buffer.getvalue())
|
||||||
|
print(f"Successfully uploaded DataFrame to {bucket}/{s3_file_name}")
|
||||||
|
return True
|
||||||
|
except NoCredentialsError:
|
||||||
|
print("Credentials not available")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def handler(event, context):
|
||||||
|
"""
|
||||||
|
Take in event and trigger the prediction pipeline
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info("received event: " + str(event))
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = (
|
||||||
|
json.loads(event["body"])
|
||||||
|
if not isinstance(event["body"], dict)
|
||||||
|
else event["body"]
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Inside handler with body: " + str(body))
|
||||||
|
|
||||||
|
data_path = body["file_location"]
|
||||||
|
property_id = body["property_id"]
|
||||||
|
portfolio_id = body["portfolio_id"]
|
||||||
|
created_at = body["created_at"]
|
||||||
|
|
||||||
|
# TODO: Implement the loading of the model and prediction
|
||||||
|
|
||||||
|
storage_filepath = f"{portfolio_id}/{property_id}/{created_at}.csv"
|
||||||
|
|
||||||
|
# upload_dataframe_to_s3(
|
||||||
|
# df=outputs,
|
||||||
|
# bucket=f"retrofit-sap-predictions-{RUNTIME_ENVIRONMENT}",
|
||||||
|
# s3_file_name=storage_filepath
|
||||||
|
# )
|
||||||
|
|
||||||
|
return {
|
||||||
|
"statusCode": 200,
|
||||||
|
"body": json.dumps(
|
||||||
|
{
|
||||||
|
"message": "Successfully processed input",
|
||||||
|
"storage_filepath": storage_filepath,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
except (Exception, KeyError, ValueError) as e:
|
||||||
|
logger.info("Prediction failed")
|
||||||
|
logger.info(e)
|
||||||
|
return {
|
||||||
|
"statusCode": 500,
|
||||||
|
"body": json.dumps({"message": "Prediction failed", "error": str(e)}),
|
||||||
|
}
|
||||||
56
deployment/serverless.yml
Normal file
56
deployment/serverless.yml
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
service: ${env:STACK_NAME}
|
||||||
|
|
||||||
|
provider:
|
||||||
|
name: aws
|
||||||
|
region: eu-west-2
|
||||||
|
architecture: x86_64
|
||||||
|
environment:
|
||||||
|
RUNTIME_ENVIRONMENT: ${env:RUNTIME_ENVIRONMENT}
|
||||||
|
MODEL_DIRECTORY_BUCKET: ${env:MODEL_DIRECTORY_BUCKET}
|
||||||
|
PREDICTIONS_BUCKET: ${env:PREDICTIONS_BUCKET}
|
||||||
|
DATA_BUCKET: ${env:DATA_BUCKET}
|
||||||
|
DOMAIN_NAME: ${env:DOMAIN_NAME}
|
||||||
|
ECR_URI: ${env:ECR_URI}
|
||||||
|
GITHUB_SHA: ${env:GITHUB_SHA}
|
||||||
|
iam:
|
||||||
|
role:
|
||||||
|
name: ${env:STACK_NAME}_s3_access
|
||||||
|
statements:
|
||||||
|
# Allow reading from MODEL_DIRECTORY_BUCKET and DATA_BUCKET
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- s3:*
|
||||||
|
Resource:
|
||||||
|
- arn:aws:s3:::${env:MODEL_DIRECTORY_BUCKET}
|
||||||
|
- arn:aws:s3:::${env:MODEL_DIRECTORY_BUCKET}/*
|
||||||
|
- arn:aws:s3:::${env:DATA_BUCKET}
|
||||||
|
- arn:aws:s3:::${env:DATA_BUCKET}/*
|
||||||
|
# Allow reading and writing to PREDICTIONS_BUCKET
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- s3:*
|
||||||
|
Resource:
|
||||||
|
- arn:aws:s3:::${env:PREDICTIONS_BUCKET}
|
||||||
|
- arn:aws:s3:::${env:PREDICTIONS_BUCKET}/*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- serverless-domain-manager
|
||||||
|
|
||||||
|
custom:
|
||||||
|
customDomain:
|
||||||
|
domainName: api.${self:provider.environment.DOMAIN_NAME}
|
||||||
|
basePath: ${env:STACK_NAME}
|
||||||
|
createRoute53Record: true
|
||||||
|
certificateArn: ${ssm:/ssl_certificate_arn}
|
||||||
|
|
||||||
|
functions:
|
||||||
|
sap_prediction_lambda:
|
||||||
|
image:
|
||||||
|
uri: ${env:ECR_URI}:${env:GITHUB_SHA}
|
||||||
|
events:
|
||||||
|
- http:
|
||||||
|
path: /predict
|
||||||
|
method: POST
|
||||||
|
timeout: 120 # Set max run time to 2 minutes - we shouldn't need this much time so this can be reviewed
|
||||||
Loading…
Add table
Reference in a new issue