Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Deploying #63

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: Build, Publish, and Deploy

on:
push:
branches:
- master

env:
IMAGE_TAG: ${{ github.sha }}
GKE_CLUSTER: monthly-deeplearning-cluster
GKE_ZONE: us-west1-a

jobs:
build-publish-deploy:
name: Build, Publish, and Deploy
runs-on: ubuntu-latest

steps:
- name: Extract branch name
id: extract_branch
shell: bash
run: |
branch_name=${GITHUB_REF#refs/heads/}
if [ $branch_name = "master" ]
then
env_name=latest;
else
env_name=${branch_name#env/};
fi
echo "##[set-output name=env_name;]$env_name"

- name: Checkout
uses: actions/checkout@v2

- name: Extract deploying config
id: extract_config
env:
CONFIG_PATH: configs/deploying/${{ steps.extract_branch.outputs.env_name }}.yaml
run: |
# Convert yaml to json
config=`python -c 'import sys, yaml, json; json.dump(yaml.load(sys.stdin), sys.stdout, indent=4)' < $CONFIG_PATH`

username=`echo $(jq -r '.username' <<< "$config")`
registry=`echo $(jq -r '.registry' <<< "$config")`
owner=`echo $(jq -r '.owner' <<< "$config")`
repository=`echo $(jq -r '.repository' <<< "$config")`
image_name=`echo $(jq -r '.image_name' <<< "$config")`
helm_release_name=`echo $(jq -r '.helm_release_name' <<< "$config")`

echo "##[set-output name=username;]$username"
echo "##[set-output name=registry;]$registry"
echo "##[set-output name=owner;]$owner"
echo "##[set-output name=repository;]$repository"
echo "##[set-output name=image_name;]$image_name"
echo "##[set-output name=helm_release_name;]$helm_release_name"

- name: Login to Github Package
env:
REGISTRY: ${{ steps.extract_config.outputs.registry }}
USERNAME: ${{ steps.extract_config.outputs.username }}
PASSWORD: ${{ secrets.GITHUB_TOKEN }}
run: |
docker login $REGISTRY -u $USERNAME --password $PASSWORD

- name: Build Image
id: build-image
env:
ENVIRONMENT: ${{ steps.extract_branch.outputs.env_name }}
REGISTRY: ${{ steps.extract_config.outputs.registry }}
OWNER: ${{ steps.extract_config.outputs.owner }}
REPOSITORY: ${{ steps.extract_config.outputs.repository }}
IMAGE_NAME: ${{ steps.extract_config.outputs.image_name }}
run: |
docker pull $REGISTRY/$OWNER/$REPOSITORY/$IMAGE_NAME:$ENVIRONMENT || true
docker build . --file charlm-server.Dockerfile --tag $REGISTRY/$OWNER/$REPOSITORY/$IMAGE_NAME:$IMAGE_TAG \
--cache-from $REGISTRY/$OWNER/$REPOSITORY/$IMAGE_NAME:$ENVIRONMENT
echo "::set-output name=image_name::$REGISTRY/$OWNER/$REPOSITORY/$IMAGE_NAME:$IMAGE_TAG"

- name: Test Image
id: test-image
env:
ENVIRONMENT: ${{ steps.extract_branch.outputs.env_name }}
REGISTRY: ${{ steps.extract_config.outputs.registry }}
OWNER: ${{ steps.extract_config.outputs.owner }}
REPOSITORY: ${{ steps.extract_config.outputs.repository }}
IMAGE_NAME: ${{ steps.extract_config.outputs.image_name }}
run: |
CONTAINER_ID=$(docker run -d --rm --env ENVIRONMENT $REGISTRY/$OWNER/$REPOSITORY/$IMAGE_NAME:$IMAGE_TAG)
docker exec $CONTAINER_ID pytest

- name: Publish
env:
ENVIRONMENT: ${{ steps.extract_branch.outputs.env_name }}
REGISTRY: ${{ steps.extract_config.outputs.registry }}
OWNER: ${{ steps.extract_config.outputs.owner }}
REPOSITORY: ${{ steps.extract_config.outputs.repository }}
IMAGE_NAME: ${{ steps.extract_config.outputs.image_name }}
run: |
docker push $REGISTRY/$OWNER/$REPOSITORY/$IMAGE_NAME:$IMAGE_TAG
# For caching
docker tag $REGISTRY/$OWNER/$REPOSITORY/$IMAGE_NAME:$IMAGE_TAG $REGISTRY/$OWNER/$REPOSITORY/$IMAGE_NAME:$ENVIRONMENT
docker push $REGISTRY/$OWNER/$REPOSITORY/$IMAGE_NAME:$ENVIRONMENT

- name: Setup gcloud
uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
with:
version: '290.0.1'
service_account_key: ${{ secrets.GKE_SA_KEY }}
project_id: monthly-deeplearning

- name: Configure docker to use the gcloud command-line tool as a credential helper for authentication
run: |
gcloud --quiet auth configure-docker

- name: Get the GKE credentials so we can deploy to the cluster
run: |
gcloud container clusters get-credentials "$GKE_CLUSTER" --zone "$GKE_ZONE"

- name: Install Helm
run: |
curl https://helm.baltorepo.com/organization/signing.asc | sudo apt-key add -
sudo apt-get install apt-transport-https --yes
echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm

- name: Deploy Helm
env:
ENVIRONMENT: ${{ steps.extract_branch.outputs.env_name }}
HELM_RELEASE_NAME: ${{ steps.extract_config.outputs.helm_release_name }}
run: |
helm upgrade $HELM_RELEASE_NAME ./deploying/helm --install --wait --atomic --namespace=$ENVIRONMENT \
--set=environment=$ENVIRONMENT --set=image_tag=$IMAGE_TAG --values=configs/deploying/$ENVIRONMENT.yaml
7 changes: 7 additions & 0 deletions configs/deploying/latest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
username: dreamgonfly
registry: docker.pkg.github.com
owner: hephaestusproject
repository: template
image_name: mnist-server
app_label: mnist
helm_release_name: mnist
21 changes: 21 additions & 0 deletions deploying/helm/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: v2
name: helm-chart
description: A Helm chart for Kubernetes

# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application

# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.1.0

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application.
appVersion: 0.1.0
34 changes: 34 additions & 0 deletions deploying/helm/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.app_label }}-deployment
labels:
app: {{ .Values.app_label }}
environment: {{ .Values.environment }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ .Values.app_label }}
template:
metadata:
labels:
app: {{ .Values.app_label }}
spec:
containers:
- name: pg
image: "{{ .Values.registry }}/{{ .Values.owner }}/{{ .Values.repository }}/{{ .Values.image_name }}:{{ .Values.image_tag }}"
env:
- name: ENVIRONMENT
value: {{ .Values.environment }}
ports:
- containerPort: 8000
protocol: TCP
readinessProbe:
httpGet:
path: /hello
port: 8000
initialDelaySeconds: 3
periodSeconds: 15
imagePullSecrets:
- name: regcred
14 changes: 14 additions & 0 deletions deploying/helm/templates/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.app_label }}-service
labels:
app: {{ .Values.app_label }}
spec:
type: NodePort
ports:
- port: 8000
targetPort: 8000
protocol: TCP
selector:
app: {{ .Values.app_label }}
66 changes: 66 additions & 0 deletions deploying/helm/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Default values.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
repository: nginx
pullPolicy: IfNotPresent

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
# Specifies whether a service account should be created
create: true
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name:

podSecurityContext: {}
# fsGroup: 2000

securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000

service:
type: ClusterIP
port: 80

ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths: []
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local

resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi

nodeSelector: {}

tolerations: []

affinity: {}
3 changes: 3 additions & 0 deletions predictor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Predictor:
def predict(self, x):
return x
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
torch==1.5.0
torchvision==0.6.0
pytorch-lightning==0.8.5
pytorch-lightning==0.8.5
fastapi==0.60.1
uvicorn==0.11.8
18 changes: 18 additions & 0 deletions server.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM python:3.7-stretch@sha256:ba2b519dbdacc440dd66a797d3dfcfda6b107124fa946119d45b93fc8f8a8d77

WORKDIR /app

RUN apt-get clean \
&& apt-get -y update

RUN pip install --no-cache-dir --upgrade pip
RUN pip install --no-cache-dir pytest

COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

ENV LANG C.UTF-8

CMD [ "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
25 changes: 25 additions & 0 deletions server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pathlib import Path

from pydantic import BaseModel

from predictor import Predictor
from serving.app_factory import create_app


predictor = Predictor()


class Request(BaseModel):
inputs: str


class Response(BaseModel):
outputs: str


def handler(request: Request) -> Response:
prediction = predictor.predict(request.inputs)
return Response(outputs=prediction)


app = create_app(handler, Request, Response)
19 changes: 19 additions & 0 deletions serving/app_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Any, Callable

from fastapi import FastAPI


def create_app(handler: Callable, request_type: Any, response_type: Any):

app = FastAPI()

@app.get("/hello")
async def hello() -> str:
return "hi"

@app.post("/model")
async def inference(request: request_type) -> response_type:
response = handler(request)
return response

return app
12 changes: 12 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from time import sleep

from fastapi.testclient import TestClient

from server import app

client = TestClient(app)


def test_predict():
request_response = client.post("/model", json={"inputs": "hey",})
assert request_response.status_code == 200