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

Setup client deployment #48

Merged
merged 13 commits into from
Feb 7, 2022
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ install-server: #- Install server dependencies
install-client: #- Install client dependencies
cd client && npm ci

build: #- Build production assets
cd client && npm run build

serve: #- Serve both the server and the client in parallel
make -j 2 serve-server serve-client

Expand All @@ -26,6 +29,12 @@ serve-server: #- Run API server
serve-client: #- Run the client
./tools/colorize_prefix.sh [client] 33 "cd client && npm run dev"

serve-prod: #- Serve both the server and the production client in parallel
make -j 2 serve-server serve-prod-client

serve-prod-client: #- Run the productionclient
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved
./tools/colorize_prefix.sh [client] 33 "cd client && npm start"

migrate: #- Apply pending migrations
${bin}alembic upgrade head

Expand Down
5 changes: 1 addition & 4 deletions client/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ module.exports = {
moduleFileExtensions: ["js", "ts", "svelte"],
moduleNameMapper: {
"^\\$lib(.*)$": "<rootDir>/src/lib$1",
"^\\$app(.*)$": [
"<rootDir>/.svelte-kit/dev/runtime/app$1",
"<rootDir>/.svelte-kit/build/runtime/app$1",
],
"^\\$app(.*)$": "<rootDir>/src/__tests__/appMock.cjs",
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved
},
setupFilesAfterEnv: ["<rootDir>/jest-setup.ts"],
collectCoverageFrom: ["src/**/*.{ts,tsx,svelte,js,jsx}"],
Expand Down
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"build": "svelte-kit build",
"package": "svelte-kit package",
"preview": "svelte-kit preview",
"start": "node ./build",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
Expand Down
4 changes: 4 additions & 0 deletions client/src/__tests__/appMock.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
dev: false,
browser: false,
}
17 changes: 17 additions & 0 deletions client/src/lib/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { browser } from "$app/env";

export const getApiUrl = () => {
if (!browser) {
// During SSR, request the local API server directly.
// This assumes the same port is used in production
// and during development.
return "http://localhost:3579";
}
// In the browser, request /api on the current domain.
// This works in production because this will use
// https://<domain>/api/..., which is the public URL to the
// API server.
// This works in development because Vite is configured
// to proxy localhost:3000/api/... to the local API server.
return "/api";
};
4 changes: 3 additions & 1 deletion client/src/routes/index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
<script lang="ts">
import { createForm } from "svelte-forms-lib";
import * as yup from "yup";
import { getApiUrl } from "$lib/fetch";

const postData = async (values) => {
const data = JSON.stringify(values);
const response = await fetch("http://127.0.0.1:3579/datasets/", {
const url = `${getApiUrl()}/datasets/`;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: data,
Expand Down
18 changes: 18 additions & 0 deletions client/svelte.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import adapter from "@sveltejs/adapter-node";
import preprocess from "svelte-preprocess";

const isDev = process.env.NODE_ENV === "development";

/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
Expand All @@ -17,6 +19,22 @@ const config = {
methodOverride: {
allowed: ["PATCH", "DELETE"],
},

vite: {
...(isDev
? {
server: {
proxy: {
// Make the location of the API server in production (<domain>/api) available during development.
"/api": {
target: "http://localhost:3579",
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved
rewrite: (path) => path.replace(/^\/api/, ""), // "/api/..." -> "/..."
},
},
},
}
: {}),
},
},
};

Expand Down
40 changes: 27 additions & 13 deletions docs/fr/ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@ Le déploiement et la gestion des serveurs distants est gérée à l'aide de [An
L'architecture du service déployé est la suivante :

```
┌----------------------------------------┐
WWW <--(tcp/80)--> nginx <--(tcp/3579)--> gunicorn |
└---------------------------------^------┘
|
┌ - - v - - -┐
PostgreSQL
└ - - - - - -┘
┌---------------------------------┐
WWW ------- nginx (:80) ---- node (:3000) |
| | | |
| └-------- gunicorn (:3579) |
└----------------------|----------┘
|
┌ - - ┴ - - -┐
PostgreSQL
└ - - - - - -┘
```

Autrement dit, un Nginx sert de frontale web et transmet les requêtes à un serveur applicatif Gunicorn, qui communique avec la base de données (BDD) PosgreSQL;
Autrement dit, un Nginx sert de frontale web et transmet les requêtes à un serveur applicatif Gunicorn qui communique avec la base de données (BDD) PosgreSQL (pour les requêtes d'API), ou à un serveur Node (pour les requêtes client).

Par ailleurs :

* Gunicorn est géré par le _process manager_ `supervisor`, ce qui permet notamment d'assurer son redémarrage en cas d'arrêt inopiné.
* Gunicorn et Node sont gérés par le _process manager_ `supervisor`, ce qui permet notamment d'assurer leur redémarrage en cas d'arrêt inopiné.
* Le lien entre Gunicorn et la base de données PostgreSQL est paramétrable (_database URL_). Cette dernier ne vit donc pas nécessairement sur la même machine que le serveur applicatif (voir [Environnements](#environnements)).

## Démarrage rapide
Expand Down Expand Up @@ -149,6 +151,10 @@ $ pyenv --version
pyenv 2.2.3
$ python -V
Python 3.8.9
$ nvm --version
0.39.1
$ node -v
v16.13.2
$ systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy serv>
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor >
Expand All @@ -168,10 +174,12 @@ cd ops
make deploy-test
```

Vérifiez le bon déploiement en accédant à l'API depuis la VM Vagrant :
Vérifiez le bon déploiement en accédant au site ou à l'API depuis la VM Vagrant :

```console
$ curl localhost/datasets/
$ curl localhost
<!-- Du HTML ... -->
$ curl localhost/api/datasets/
[]
```

Expand All @@ -187,7 +195,7 @@ Une bonne pratique pour limiter les risques : déployer la migration d'abord, pu

### Nginx renvoie une "502 Bad Gateway"

Il y a probablement soit un problème de configuration de la connexion entre Nginx et Gunicorn (ex : mauvais port), soit le serveur Gunicorn n'est pas _up_ (ex : il crashe ou ne démarre pas pour une raison à déterminer).
Il y a probablement soit un problème de configuration de la connexion entre Nginx et Node / Gunicorn (ex : mauvais port), soit le serveur Node / Gunicorn n'est pas _up_ (ex : il crashe ou ne démarre pas pour une raison à déterminer).

* Vérifier l'état de Nginx :

Expand All @@ -201,7 +209,7 @@ Il y a probablement soit un problème de configuration de la connexion entre Ngi
~/catalogage $ systemctl status supervisor
```

* Vérifier l'état du processus serveur au sein de Supervisor :
* Vérifier l'état du processus serveur (`server` pour Gunicorn, `client` pour le frontend Node) au sein de Supervisor :

```
~/catalogage $ sudo supervisorctl status server
Expand Down Expand Up @@ -237,3 +245,9 @@ N.B. : cette commande demandera le mot de passe Vault associé à l'environnemen
On utilise [pyenv](https://github.com/pyenv/pyenv) pour installer Python sur les serveurs distants.

La configuration est aussi gérée par Ansible (rôle `pyenv`), notamment au moyen de la variable `pyenv_python_version`. En la modifiant, on peut ainsi mettre Python à jour. Il sera bien sûr préférable de s'assurer de retirer toute ancienne version de Python après une telle opération.

### Version de Node

De la même façon, on utilise [nvm](https://github.com/nvm-sh/nvm) pour installer Node.

La version est paramétrée par la variable `nvm_node_version`.
6 changes: 5 additions & 1 deletion ops/ansible/playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
vars:
workdir: "{{ ansible_env.HOME }}/catalogage"
pyenv_python_version: "3.8.9"
git_repo: "https://github.com/multi-coop/catalogage-donnees.git"
nvm_node_version: "v16.13.2"
git_repo: "https://github.com/etalab/catalogage-donnees.git"
git_version: "{{ env_branch }}"
web_port: 80
api_port: 3579
client_port: 3000

roles:
- common
Expand Down
5 changes: 5 additions & 0 deletions ops/ansible/roles/common/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
import_role:
name: pyenv

- name: Setup Node
# Use nvm to be able to install a specific Node version too.
import_role:
name: nvm

- name: Ensure nginx is installed and up to date
apt: name=nginx state=latest
become: true
Expand Down
10 changes: 10 additions & 0 deletions ops/ansible/roles/nvm/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
# Installation paths
nvm_home: "{{ ansible_env.HOME }}"
nvm_root: "{{ ansible_env.HOME }}/.nvm"

# nvm version to install
nvm_version: "v0.39.1"

# Node.js version to install
nvm_node_version: "v16.13.2"
6 changes: 6 additions & 0 deletions ops/ansible/roles/nvm/tasks/install.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- name: Install nvm
shell: >
curl https://raw.githubusercontent.com/nvm-sh/nvm/{{ nvm_version }}/install.sh | bash
args:
chdir: "{{ nvm_home }}"
creates: "{{ nvm_root }}/nvm.sh"
5 changes: 5 additions & 0 deletions ops/ansible/roles/nvm/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- name: Install nvm
import_tasks: install.yml

- name: Install Node version
import_tasks: node.yml
37 changes: 37 additions & 0 deletions ops/ansible/roles/nvm/tasks/node.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
- name: Install Node binary
shell: >-
source {{ nvm_root }}/nvm.sh && nvm install {{ nvm_node_version }}
args:
executable: /bin/bash
creates: "{{ nvm_root }}/versions/{{ nvm_node_version }}"

# Get Node and npm bin paths, taking into account that Ansible does not run
# commands as a user by default.
# See: https://stackoverflow.com/a/58744872
- name: Get node binary path
shell: bash -ilc 'which node'
register: which_node
- name: Set node_bin_path fact
set_fact:
node_bin_path: "{{ which_node.stdout | trim() }}"
- name: Get npm binary path
shell: bash -ilc 'which npm'
register: which_npm
- name: Set npm_bin_path fact
set_fact:
npm_bin_path: "{{ which_npm.stdout | trim() }}"
# Create symlinks to node/npm. This step is required so that Ansible
# commands can access them without having to refer to the full binary paths.
# (Nvm shims are only available through a shell, which, again, Ansible does not do by default.)
- name: Create node bin symlink
file:
src: "{{ node_bin_path }}"
dest: /usr/local/bin/node
state: link
become: true
- name: Create npm bin symlink
file:
src: "{{ npm_bin_path }}"
dest: /usr/local/bin/npm
state: link
become: true
4 changes: 4 additions & 0 deletions ops/ansible/roles/web/tasks/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- name: Ensure build is up to date
shell:
cmd: make build
chdir: "{{ workdir }}"
6 changes: 6 additions & 0 deletions ops/ansible/roles/web/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
- name: Sync Python dependencies
import_tasks: python.yml

- name: Sync Node dependencies
import_tasks: node.yml

- name: Sync build
import_tasks: build.yml

- name: Sync DB
import_tasks: db.yml

Expand Down
6 changes: 6 additions & 0 deletions ops/ansible/roles/web/tasks/node.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- name: Ensure Node dependencies are installed
npm:
path: "{{ workdir }}/client"
ci: true
state: present
become: true
5 changes: 5 additions & 0 deletions ops/ansible/roles/web/tasks/supervisor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
# Config or dependencies may have changed, so restart the server.
notify: reload supervisor

- name: Ensure client is running
supervisorctl: name=client state=present
become: true
notify: reload nginx

- name: Ensure server is running
supervisorctl: name=server state=present
become: true
Expand Down
17 changes: 14 additions & 3 deletions ops/ansible/roles/web/templates/nginx.conf.j2
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
upstream client {
server 127.0.0.1:{{ client_port }};
}

upstream api {
server 127.0.0.1:3579;
server 127.0.0.1:{{ api_port }};
}

server {
listen 80;
listen {{ web_port }};
server_name {{ inventory_hostname }};

location /favicon.ico {
Expand All @@ -13,6 +17,13 @@ server {

location / {
include proxy_params;
proxy_pass http://api;
proxy_pass http://client;
}

location /api/ {
include proxy_params;
{# NOTE: trailing / is important. Ensures /api/foo becomes /foo. #}
{# See: http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass #}
proxy_pass http://api/;
}
}
11 changes: 10 additions & 1 deletion ops/ansible/roles/web/templates/supervisor.conf.j2
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
[program:client]
directory={{ workdir }}/client
command=npm start
environment=HOST=127.0.0.1,PORT="{{ client_port }}"
autostart=true
autorestart=true
stderr_logfile=/var/log/client.err.log
stdout_logfile=/var/log/client.out.log

[program:server]
directory={{ workdir }}
command={{ workdir }}/venv/bin/gunicorn -w 2 --bind 127.0.0.1:3579 -k uvicorn.workers.UvicornWorker server.main:app
command={{ workdir }}/venv/bin/gunicorn -w 2 --bind 127.0.0.1:{{ api_port }} -k uvicorn.workers.UvicornWorker server.main:app
autostart=true
autorestart=true
stderr_logfile=/var/log/server.err.log
Expand Down