Backend GKE Deploy, access key for backend

- Configs and automation for deploying backend to GKE
- First steps to adding an optional token check for requests to backend
- First steps frontend sending optional token to backend when configured
This commit is contained in:
Hristo 2024-05-10 16:07:58 -04:00
parent 0fedaef537
commit e6c2042df6
17 changed files with 296 additions and 39 deletions

18
Makefile Normal file
View file

@ -0,0 +1,18 @@
.PHONY: run
run:
docker compose -f docker-compose.yaml up
.PHONY: rebuild-run
rebuild-run:
docker compose -f docker-compose.yaml up --build
.PHONY: run-app-only
run-app-only:
docker compose -f app-docker-compose.yaml up
.PHONY: rebuild-run-app-only
rebuild-run-app-only:
docker compose -f app-docker-compose.yaml up --build

View file

@ -106,6 +106,26 @@ You need to edit the ports accordingly.
[![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267) [![Deploy to RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploylobe.svg)](https://repocloud.io/details/?app_id=267)
## Deploy Perplexica backend to Google GKE
0: Install `docker` and `terraform` (Process specific to your system)
1a: Copy the `sample.env` file to `.env`
1b: Copy the `deploy/gcp/sample.env` file to `deploy/gcp/.env`
2a: Fillout desired LLM provider access keys etc. in `.env`
- Note: you will have to comeback and edit this file again once you have the address of the K8s backend deploy
2b: Fillout the GCP info in `deploy/gcp/.env`
3: Edit `GCP_REPO` to the correct docker image repo path if you are using something other than Container registry
4: Edit the `PREFIX` if you would like images and GKE entities to be prefixed with something else
5: In `deploy/gcp` run `make init` to initialize terraform
6: Follow the normal Preplexica configuration steps outlined in the project readme
7: Auth docker with the appropriate credential for repo Ex. for `gcr.io` -> `gcloud auth configure-docker`
8: In `deploy/gcp` run `make build-deplpy` to build and push the project images to the repo, create a GKE cluster and deploy the app
9: Once deployed successfully edit the `.env` file in the root project folder and update the `REMOTE_BACKEND_ADDRESS` with the remote k8s deployment address and port
10: In root project folder run `make rebuild-run-app-only`
If you configured everything correctly frontend app will run locally and provide you with a local url to open it.
Now you can run queries against the remotely deployed backend from your local machine. :celebrate:
## Upcoming Features ## Upcoming Features
- [ ] Finalizing Copilot Mode - [ ] Finalizing Copilot Mode

13
app-docker-compose.yaml Normal file
View file

@ -0,0 +1,13 @@
services:
perplexica-frontend:
build:
context: .
dockerfile: app.dockerfile
args:
- SUPER_SECRET_KEY=${SUPER_SECRET_KEY}
- NEXT_PUBLIC_API_URL=http://${REMOTE_BACKEND_ADDRESS}/api
- NEXT_PUBLIC_WS_URL=ws://${REMOTE_BACKEND_ADDRESS}
expose:
- 3000
ports:
- 3000:3000

View file

@ -2,8 +2,11 @@ FROM node:alpine
ARG NEXT_PUBLIC_WS_URL ARG NEXT_PUBLIC_WS_URL
ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_API_URL
ARG SUPER_SECRET_KEY
ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL} ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL}
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV SUPER_SECRET_KEY=${SUPER_SECRET_KEY}
WORKDIR /home/perplexica WORKDIR /home/perplexica

View file

@ -1,21 +1,14 @@
# USAGE: # Adds all the deployment relevant sensitive information about project
# 0: Install `docker` and `terraform` (Process specific to your system)
# 1: Copy the sample.env file to .env
# 2: Fillout the GCP info in .env
# 3: Edit GCP_REPO to the correct docker image repo path if you are using something other than Container registry
# 4: Edit the PREFIX if you would like images and GKE entities to be prefixed with something else
# 5: Run `make init` to initialize terraform
# 6: Follow the normal Preplexica configuration steps outlined in the project readme
# 7: Run `make build-deplpy` to build and push the project images to the repo, create a GKE cluster and deploy the app
#
# NOTES/ WARNINGS:
# - The app endpoint is unsecured and exposed to the internet at large, you will need to implement your desired auth
# - No auto scaling is enabled for this cluster and deployments, edit the terraform files accordingly for that
include .env include .env
# Adds secrets/ keys we have define for the project locally and deployment
include ../../.env
# Use `location-id-docker.pkg` for artifact registry Ex. west-1-docker.pkg # Use `location-id-docker.pkg` for artifact registry Ex. west-1-docker.pkg
GCP_REPO=gcr.io GCP_REPO=gcr.io
PREFIX=perplexica PREFIX=perplexica
SEARCH_PORT=8080
BACKEND_PORT=3001
SEARCH_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-searxng:latest SEARCH_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-searxng:latest
BACKEND_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-backend:latest BACKEND_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-backend:latest
APP_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-app:latest APP_IMAGE_TAG=$(GCP_REPO)/$(GCP_PROJECT_ID)/$(PREFIX)-app:latest
@ -38,8 +31,10 @@ show_config:
&& echo $(GCP_SERVICE_ACCOUNT_KEY_FILE) \ && echo $(GCP_SERVICE_ACCOUNT_KEY_FILE) \
&& echo $(SEARCH_IMAGE_TAG) \ && echo $(SEARCH_IMAGE_TAG) \
&& echo $(BACKEND_IMAGE_TAG) \ && echo $(BACKEND_IMAGE_TAG) \
&& echo $(APP_IMAGE_TAG) && echo $(APP_IMAGE_TAG) \
&& echo $(SEARCH_PORT) \
&& echo $(BACKEND_PORT) \
&& echo $(OPENAI)
.PHONY: docker-build-push-searxng .PHONY: docker-build-push-searxng
docker-build-push-searxng: docker-build-push-searxng:
@ -49,14 +44,15 @@ docker-build-push-searxng:
.PHONY: docker-build-push-backend .PHONY: docker-build-push-backend
docker-build-push-backend: docker-build-push-backend:
cd ../../ && docker build -f ./backed.dockerfile -t $(BACKEND_IMAGE_TAG) . --platform="linux/amd64" cd ../../ && docker build -f ./backend.dockerfile -t $(BACKEND_IMAGE_TAG) . --platform="linux/amd64"
docker push $(BACKEND_IMAGE_TAG) docker push $(BACKEND_IMAGE_TAG)
.PHONY: docker-build-push-app .PHONY: docker-build-push-app
docker-build-push-app: docker-build-push-app:
cd ../../ && docker build -f ./app.dockerfile -t $(APP_IMAGE_TAG) . --platform="linux/amd64" #
docker push $(APP_IMAGE_TAG) # cd ../../ && docker build -f ./app.dockerfile -t $(APP_IMAGE_TAG) . --platform="linux/amd64"
# docker push $(APP_IMAGE_TAG)
.PHONY: init .PHONY: init
@ -73,6 +69,9 @@ deploy:
&& export TF_VAR_search_image=$(SEARCH_IMAGE_TAG) \ && export TF_VAR_search_image=$(SEARCH_IMAGE_TAG) \
&& export TF_VAR_backend_image=$(BACKEND_IMAGE_TAG) \ && export TF_VAR_backend_image=$(BACKEND_IMAGE_TAG) \
&& export TF_VAR_app_image=$(APP_IMAGE_TAG) \ && export TF_VAR_app_image=$(APP_IMAGE_TAG) \
&& export TF_VAR_search_port=$(SEARCH_PORT) \
&& export TF_VAR_backend_port=$(BACKEND_PORT) \
&& export TF_VAR_open_ai=$(OPENAI) \
&& terraform apply && terraform apply

View file

@ -35,6 +35,9 @@ provider "kubernetes" {
) )
} }
#####################################################################################################
# SearXNG - Search engine deployment and service
#####################################################################################################
resource "kubernetes_deployment" "searxng" { resource "kubernetes_deployment" "searxng" {
metadata { metadata {
name = "searxng" name = "searxng"
@ -60,7 +63,7 @@ resource "kubernetes_deployment" "searxng" {
image = var.search_image image = var.search_image
name = "searxng-container" name = "searxng-container"
port { port {
container_port = 8080 container_port = var.search_port
} }
} }
} }
@ -80,14 +83,88 @@ resource "kubernetes_service" "searxng_service" {
} }
port { port {
port = 8080 port = var.search_port
target_port = 8080 target_port = var.search_port
} }
type = "LoadBalancer" type = "LoadBalancer"
} }
} }
#####################################################################################################
# Perplexica - backend deployment and service
#####################################################################################################
resource "kubernetes_deployment" "backend" {
metadata {
name = "backend"
labels = {
app = "backend"
}
}
spec {
replicas = 1
selector {
match_labels = {
component = "backend"
}
}
template {
metadata {
labels = {
component = "backend"
}
}
spec {
container {
image = var.backend_image
name = "backend-container"
port {
container_port = var.backend_port
}
env {
# searxng service ip
name = "SEARXNG_API_URL"
value = "http://${kubernetes_service.searxng_service.status[0].load_balancer[0].ingress[0].ip}:${var.search_port}"
}
env {
# openai key
name = "OPENAI"
value = var.open_ai
}
env {
# port
name = "PORT"
value = var.backend_port
}
}
}
}
}
}
resource "kubernetes_service" "backend_service" {
metadata {
name = "backend-service"
namespace = "default"
}
spec {
selector = {
component = "backend"
}
port {
port = var.backend_port
target_port = var.backend_port
}
type = "LoadBalancer"
}
}
#####################################################################################################
# Variable and module definitions
#####################################################################################################
variable "project_id" { variable "project_id" {
description = "The ID of the project in which the resources will be deployed." description = "The ID of the project in which the resources will be deployed."
type = string type = string
@ -113,7 +190,7 @@ variable "search_image" {
type = string type = string
} }
variable "backed_image" { variable "backend_image" {
description = "Tag for the Perplexica backend image" description = "Tag for the Perplexica backend image"
type = string type = string
} }
@ -123,6 +200,21 @@ variable "app_image" {
type = string type = string
} }
variable "open_ai" {
description = "OPENAI access key"
type = string
}
variable "search_port" {
description = "Port for searxng service"
type = number
}
variable "backend_port" {
description = "Port for backend service"
type = number
}
module "gke-cluster" { module "gke-cluster" {
source = "./gke-cluster" source = "./gke-cluster"

View file

@ -15,7 +15,12 @@ services:
context: . context: .
dockerfile: backend.dockerfile dockerfile: backend.dockerfile
args: args:
- SEARXNG_API_URL=http://searxng:8080 - SEARXNG_API_URL=null
environment:
SEARXNG_API_URL: "http://searxng:8080"
OPENAI: ${OPENAI}
GROQ: ${GROQ}
OLLAMA_API_URL: ${OLLAMA_API_URL}
depends_on: depends_on:
- searxng - searxng
expose: expose:
@ -30,8 +35,9 @@ services:
context: . context: .
dockerfile: app.dockerfile dockerfile: app.dockerfile
args: args:
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api - SUPER_SECRET_KEY=${SUPER_SECRET_KEY}
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001 - NEXT_PUBLIC_API_URL=http://${REMOTE_BACKEND_ADDRESS}/api
- NEXT_PUBLIC_WS_URL=ws://${REMOTE_BACKEND_ADDRESS}
depends_on: depends_on:
- perplexica-backend - perplexica-backend
expose: expose:

20
sample.env Normal file
View file

@ -0,0 +1,20 @@
# Copy this file over to .env and fill in the desired config.
# .env will become available to docker compose and these values will be
# used when running docker compose up
# Edit to set OpenAI access key
OPENAI=ADD OPENAI KEY HERE
# Uncomment and edit to set GROQ access key
# GROQ: ${GROQ}
# Uncomment and edit to set OLLAMA Url
# OLLAMA_API_URL: ${OLLAMA_API_URL}
# Address and port of the remotely deployed Perplexica backend
REMOTE_BACKEND_ADDRESS=111.111.111.111:0000
# Uncomment and edit to configure backend to reject requests without token
# leave commented to have open access to all endpoints
# Secret key to "secure" backend
# SUPER_SECRET_KEY=THISISASUPERSECRETKEYSERIOUSLY

View file

@ -3,7 +3,8 @@ import express from 'express';
import cors from 'cors'; import cors from 'cors';
import http from 'http'; import http from 'http';
import routes from './routes'; import routes from './routes';
import { getPort } from './config'; import { requireAccessKey } from './auth';
import { getAccessKey, getPort } from './config';
import logger from './utils/logger'; import logger from './utils/logger';
const port = getPort(); const port = getPort();
@ -23,6 +24,10 @@ app.get('/api', (_, res) => {
res.status(200).json({ status: 'ok' }); res.status(200).json({ status: 'ok' });
}); });
if (getAccessKey()) {
app.all('*', requireAccessKey);
};
server.listen(port, () => { server.listen(port, () => {
logger.info(`Server is running on port ${port}`); logger.info(`Server is running on port ${port}`);
}); });

18
src/auth.ts Normal file
View file

@ -0,0 +1,18 @@
import {
getAccessKey,
} from '../config';
const requireAccessKey = (req, res, next) => {
const authHeader = req.headers.authorization;
if (authHeader) {
const token = authHeader.split(' ')[1];
if (token !== getAccessKey()) {
return res.sendStatus(403);
}
next();
} else {
res.sendStatus(401);
}
};

View file

@ -8,6 +8,7 @@ interface Config {
GENERAL: { GENERAL: {
PORT: number; PORT: number;
SIMILARITY_MEASURE: string; SIMILARITY_MEASURE: string;
SUPER_SECRET_KEY: string;
}; };
API_KEYS: { API_KEYS: {
OPENAI: string; OPENAI: string;
@ -28,18 +29,38 @@ const loadConfig = () =>
fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'), fs.readFileSync(path.join(__dirname, `../${configFileName}`), 'utf-8'),
) as any as Config; ) as any as Config;
const loadEnv = () => {
return {
GENERAL: {
PORT: Number(process.env.PORT),
SIMILARITY_MEASURE: process.env.SIMILARITY_MEASURE,
SUPER_SECRET_KEY: process.env.SUPER_SECRET_KEY
},
API_KEYS: {
OPENAI: process.env.OPENAI,
GROQ: process.env.GROQ
},
API_ENDPOINTS: {
SEARXNG: process.env.SEARXNG_API_URL,
OLLAMA: process.env.OLLAMA_API_URL
}
} as Config;
};
export const getPort = () => loadConfig().GENERAL.PORT; export const getPort = () => loadConfig().GENERAL.PORT;
export const getAccessKey = () => loadEnv().GENERAL.SUPER_SECRET_KEY || loadConfig().GENERAL.SUPER_SECRET_KEY;
export const getSimilarityMeasure = () => export const getSimilarityMeasure = () =>
loadConfig().GENERAL.SIMILARITY_MEASURE; loadConfig().GENERAL.SIMILARITY_MEASURE;
export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI; export const getOpenaiApiKey = () => loadEnv().API_KEYS.OPENAI || loadConfig().API_KEYS.OPENAI;
export const getGroqApiKey = () => loadConfig().API_KEYS.GROQ; export const getGroqApiKey = () => loadEnv().API_KEYS.GROQ || loadConfig().API_KEYS.GROQ;
export const getSearxngApiEndpoint = () => loadConfig().API_ENDPOINTS.SEARXNG; export const getSearxngApiEndpoint = () => loadEnv().API_ENDPOINTS.SEARXNG || loadConfig().API_ENDPOINTS.SEARXNG;
export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA; export const getOllamaApiEndpoint = () => loadEnv().API_ENDPOINTS.OLLAMA || loadConfig().API_ENDPOINTS.OLLAMA;
export const updateConfig = (config: RecursivePartial<Config>) => { export const updateConfig = (config: RecursivePartial<Config>) => {
const currentConfig = loadConfig(); const currentConfig = loadConfig();

View file

@ -6,6 +6,7 @@ import Navbar from './Navbar';
import Chat from './Chat'; import Chat from './Chat';
import EmptyChat from './EmptyChat'; import EmptyChat from './EmptyChat';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { clientFetch } from '@/lib/utils';
export type Message = { export type Message = {
id: string; id: string;
@ -34,8 +35,8 @@ const useSocket = (url: string) => {
!embeddingModel || !embeddingModel ||
!embeddingModelProvider !embeddingModelProvider
) { ) {
const providers = await fetch( const providers = await clientFetch(
`${process.env.NEXT_PUBLIC_API_URL}/models`, '/models',
{ {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View file

@ -4,6 +4,7 @@ import { useState } from 'react';
import Lightbox from 'yet-another-react-lightbox'; import Lightbox from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css'; import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow'; import { Message } from './ChatWindow';
import { clientFetch } from '@/lib/utils';
type Image = { type Image = {
url: string; url: string;
@ -33,8 +34,8 @@ const SearchImages = ({
const chatModelProvider = localStorage.getItem('chatModelProvider'); const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel'); const chatModel = localStorage.getItem('chatModel');
const res = await fetch( const res = await clientFetch(
`${process.env.NEXT_PUBLIC_API_URL}/images`, '/images',
{ {
method: 'POST', method: 'POST',
headers: { headers: {

View file

@ -4,6 +4,7 @@ import { useState } from 'react';
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox'; import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css'; import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow'; import { Message } from './ChatWindow';
import { clientFetch } from '@/lib/utils';
type Video = { type Video = {
url: string; url: string;
@ -46,8 +47,8 @@ const Searchvideos = ({
const chatModelProvider = localStorage.getItem('chatModelProvider'); const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel'); const chatModel = localStorage.getItem('chatModel');
const res = await fetch( const res = await clientFetch(
`${process.env.NEXT_PUBLIC_API_URL}/videos`, '/videos',
{ {
method: 'POST', method: 'POST',
headers: { headers: {

View file

@ -1,6 +1,7 @@
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from '@headlessui/react';
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react'; import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react';
import React, { Fragment, useEffect, useState } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import { clientFetch } from '@/lib/utils';
interface SettingsType { interface SettingsType {
chatModelProviders: { chatModelProviders: {
@ -42,7 +43,7 @@ const SettingsDialog = ({
if (isOpen) { if (isOpen) {
const fetchConfig = async () => { const fetchConfig = async () => {
setIsLoading(true); setIsLoading(true);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, { const res = await clientFetch('/config', {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -102,7 +103,7 @@ const SettingsDialog = ({
setIsUpdating(true); setIsUpdating(true);
try { try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, { await clientFetch('/config', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

20
ui/lib/config.ts Normal file
View file

@ -0,0 +1,20 @@
interface Config {
GENERAL: {
SUPER_SECRET_KEY: string;
NEXT_PUBLIC_API_URL: string;
};
}
const loadEnv = () => {
return {
GENERAL: {
SUPER_SECRET_KEY: process.env.SUPER_SECRET_KEY!,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL!,
NEXT_PUBLIC_WS_URL: process.env.NEXT_PUBLIC_WS_URL!
},
} as Config;
};
export const getAccessKey = () => loadEnv().GENERAL.SUPER_SECRET_KEY;
export const getBackendURL = () => loadEnv().GENERAL.NEXT_PUBLIC_API_URL;

View file

@ -1,5 +1,6 @@
import clsx, { ClassValue } from 'clsx'; import clsx, { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { getAccessKey, getBackendURL } from './config'
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes)); export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes));
@ -19,3 +20,20 @@ export const formatTimeDifference = (date1: Date, date2: Date): string => {
else else
return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? 's' : ''}`; return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? 's' : ''}`;
}; };
export const clientFetch = async (path: string, payload: any): Promise<any> => {
let headers = payload.headers;
const url = `${getBackendURL()}${path}`;
const secret_token = getAccessKey();
if (secret_token) {
if (headers == null) {
headers = {};
};
headers['Authorization'] = `Bearer ${secret_token}`;
payload.headers = headers;
};
return await fetch(url, payload);
};