4 votes

Déploiement dynamique Angular. Une image docker, plusieurs déploiements

J'ai besoin de trouver un moyen de déployer une application Angular dynamiquement, de sorte que la même image docker me permette de faire plusieurs déploiements (développement, staging, production) dans lesquels l'URL d'accès est modifiée.

Je réalise une application en Angular 7 en utilisant Angular-cli 6.4.1. Pour le déploiement, je crée un conteneur docker multistage où je construis l'image et la configure avec nginx.

Le problème est que pour le déploiement, nous utilisons un registre privé dans lequel nous utilisons un proxy qui gère les redirections, de sorte que mon application serait déployée selon le schéma : {SERVER_HOST} : {PORT} / {SERVER_LOCATION}

Lorsque j'essaie d'accéder à cette URL, seul le fichier index.html est chargé, puisque le reste des ressources est associé au chemin de base "/".

Angular fournit un argument dans le build (--base-href) qui permet de modifier le chemin de l'application entière, cependant cela ne m'aide pas puisque j'ai besoin de la même image Docker pour me permettre d'effectuer différents déploiements afin que le paramètre {SERVER_LOCATION} ne soit pas toujours le même.

J'ai également essayé de lire les variables d'environnement au moment de l'exécution pour modifier l'attribut href de la balise de base, mais il est difficile d'exécuter un code qui se trouve dans un fichier qui n'est pas chargé à côté de l'index.

Comme solution de contournement, j'ai décidé de créer une fonction dans l'index qui exécute une requête ajax qui collecte les paramètres de configuration afin de charger le reste des ressources, mais je n'aime pas cela car cela casse le fonctionnement d'Angular.

<-- This is working fine, but is not dynamic: -->
npm run build -- --prod --base-href https://myHost.net:8080/path/app/ --configuration=$configuration

<-- And this is working but is not Angular friendly -->

/** assets/data/appConfig.json */
{
    "SERVER_HOST": "https://myHost.net:8080",
    "SERVER_PATH": "/path/app/"
}

/** index.html */
<script>
  (function() {
    if (window.onerror) {
      loadConfig();
    }

    function loadConfig() {
      var xhttp = new XMLHttpRequest();
      var url = 'assets/data/appConfig.json';
      xhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          var data = JSON.parse(this.response);
          window.appBaseHref = data.SERVER_PATH;
          document.getElementById("base").href = data.SERVER_PATH;
        }
      };
      xhttp.open('GET', url);
      xhttp.send();
    }
  })()
</script>

On m'a dit qu'il y avait une option pour sauter le proxy, mais je ne trouve pas le moyen de le faire, je ne sais pas comment le configurer. Je pense qu'il y a peut-être un ajustement à faire dans le fichier de configuration de nginx pour que l'application "lise" à partir de l'URL fournie et que le href de base de l'application soit toujours "/".

Actuellement mon fichier nginx.conf est :

server {
    listen 80;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html =404;
    }

    location /documentation {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /documentation/index.html =404;
    }
}

Où le chemin d'emplacement '/' est mon application angulaire et le chemin d'emplacement '/documentation' est le chemin réservé pour la documentation du projet.

Toute aide sera la bienvenue.

<--- Editer --->

Ces derniers jours, j'ai essayé plusieurs choses, et la vérité est que je ne sais pas trop comment continuer, puisque rien ne semble fonctionner.

J'ai configuré Nginx pour ajouter la propriété env $uri+$basepath, mais je me rends compte que la relation est exactement l'inverse. J'ai besoin que mon index.html fasse des requêtes vers la route du proxy au lieu du chemin '/', parce qu'à cause de cela, la requête n'entre même pas dans le log (le proxy ne me redirige pas parce qu'il n'a pas le préfixe de mon application).

Je vous donne un exemple : Mon application est hébergée sur : myHost:8080/myapp/front Lorsque j'entre cette url (myHost:8080/myapp/front), l'application charge l'index, mais les ressources associées (styles, runtime, polyfills, scripts et main) ne sont pas chargées parce qu'elles envoient la requête à : myHost:8080/{resourcePath} au lieu de myHost:8080/myapp/front/{resourcePath}. enter image description here

À ce stade, je ne sais pas où aller. Je serais capable d'obtenir le résultat que je veux en mettant le flag --base-href /myapp/front, mais je veux que cette route soit dynamique et dépende de ma variable d'environnement du système VIRTUAL_HOST, que bien sûr je ne connais pas au moment de la construction de l'application.

Ensuite, je colle mon fichier docker et le fichier de configuration nginx.

Dockerfile (Les lignes commentées sont les propositions qui n'ont pas fonctionné de la manière attendue).

### STAGE 0: Based on Node.js, to build and compile Angular ###
FROM node:alpine as node

# Create app directory
WORKDIR /app

# Copy the dependencies to install once and let Docker use the cache for the next builds
COPY package*.json /app/

# Install all dependencies
RUN npm install

# Copy all the project into the image
COPY ./ /app/

# Argument to build the image according to the environment
ARG configuration=production

# Compiles our project
RUN npm run build -- --prod --configuration=$configuration

### STAGE 1: Based on Nginx, to have only the compiled app, ready for production with Nginx ###
FROM nginx:1.13.3-alpine

## Remove default nginx website
RUN rm -rf /usr/share/nginx/html/*

## From 'builder' stage copy over the artifacts in dist folder to default nginx public folder
COPY --from=node /app/dist/dsa-frontend /usr/share/nginx/html

# Add directive
# COPY nginx-custom.conf.template /etc/nginx/conf.d/default.conf.template

COPY nginx-custom.conf /etc/nginx/conf.d/default.conf

# CMD /bin/bash -c "envsubst '\$VIRTUAL_SERVICE_LOCATION' < nginx-custom.conf > /etc/nginx/conf.d/default.conf"

CMD ["nginx", "-g", "daemon off;"]

nginx-custom.conf

server {
    listen 80;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /myapp/front$uri /myapp/front$uri/;
        # try_files $uri $uri/ $uri$VIRTUAL_SERVICE_LOCATION $uri$VIRTUAL_SERVICE_LOCATION/ /index.html;
    }

    location /documentation {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /documentation/index.html =404;
    }
}

Mais rien de tout cela ne semble fonctionner.

Enfin, nous avons pris la décision de supprimer le développement du proxy et d'ouvrir un nouveau port pour déployer cette application, de sorte que le chemin racine est désormais "/".

Et maintenant, tout fonctionne bien.

2voto

Andrei Sinitson Points 166

Je pense que j'ai réussi à être complet (bien que probablement assez fragile - n'hésitez pas à recommander des améliorations !) à exactement le même problème : réutiliser une image Docker Angular/NGINX pour plusieurs environnements (donc plusieurs routes de base).

En outre, je voulais résoudre le problème de l'accès aux API externes, de sorte que tout ce que l'application demande passe par le NGINX dans le conteneur, de sorte que vous n'avez pas à penser à CORS . Dans mon exemple, j'ai deux choses :

  • Point d'arrivée du serveur d'images
  • Point de terminaison Socket.IO (WebSocket)

Le routage côté client fonctionne comme prévu avec ROOT_PATH correspondant à / .

Ce que j'ai quand même fait pas (mais je vais le faire) :

  • HTTPS (il est probable que cela ne changera pas grand-chose)

Le résultat final est le suivant :


Construire et exécuter le conteneur localement (une fois que tout est en place)

ROOT_PATH est le paramètre que vous recherchez.

DNS_IP est nécessaire en raison de la présence de NGINX proxy_pass vous voudrez probablement remplacer Google DNS par autre chose .

docker build . -t fancy-web-app && \
docker run -it --rm \
    --name fancy-web-app-configured \
    --publish target=80,published=80 \
    --env ROOT_PATH='staging' \
    --env WEBSOCKET_PATH='socket.io' \
    --env IMAGE_PATH='image' \
    --env API_URL='http://api.server.example.com/api/v1/' \
    --env DNS_IP='8.8.8.8 8.8.4.4' \
    fancy-web-app

Je vais énumérer toutes les choses dont vous avez besoin pour plus de clarté :

NGNIX

  1. nginx.conf

    Modifié au démarrage du conteneur, inutilisable autrement, car il contient des références à des variables d'environnement.

Docker

  1. docker-entrypoint.sh <-- * * * * C'est ici que le magie se produit. * * *

  2. Fichier Docker

    Multi-étapes , a docker-entrypoint.sh par défaut point d'entrée pour la phase d'exécution.

Angulaire

  1. index.html

    Avec la configuration de développement par défaut, modifiée lors du démarrage du conteneur, ce qui la transforme en "configuration d'exécution".

  2. app.conf.ts

    La configuration vous permet d'abstraire la configuration dans des variables globales et de ne pas introduire cette laideur dans vos services.

  3. app.module.ts

    Injection de dépendances Angular permettant de référencer la configuration dans n'importe quel service.

  4. some-angular.service.ts

    C'est ici que vous utilisez la configuration injectée.

Points forts d'Angular

  1. proxy.conf.json

    Pour travailler sur localhost sans conteneurs (équivalent de nginx.conf pour le développement, sera utilisé par WebPack/ ng serve )

  2. angular.json

    Ici, vous pouvez spécifier la configuration du proxy une seule fois et l'appliquer partout par défaut.


S'il y a un intérêt, je peux créer un dépôt GitHub avec tous les éléments nécessaires (merci de commenter/évaluer, pour que je puisse voir qu'il y a un besoin pour cela).


NGINX

1. nginx.conf

server {
  listen 80;

  # Serve static files (HTML, CSS, JS)
  location /${ROOT_PATH} {
    # Using `alias` insead of `root` directive, so ${ROOT_PATH}
    # will be discarded and static file will be fetched straight
    # from the specified folder
    alias /usr/share/nginx/html;
    try_files $uri $uri/ /index.html =404;
  }

  # Re-route calls to external APIs
  location  ~ ^/${ROOT_PATH}/(${WEBSOCKET_PATH}|${IMAGE_PATH})/(.*) {
    resolver ${DNS_IP};
    proxy_pass ${API_URL}/$1/$2$is_args$args;
    proxy_http_version 1.1;
    proxy_cache_bypass $http_upgrade;
    proxy_set_header Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
  }

}

Docker

2. docker-entrypoint.sh

#!/usr/bin/env sh
set -eu

# Inject environment variables into NGINX configuration
# List all variables to be substituted to avoid clashing with
# NGINX own variables: https://serverfault.com/questions/577370
envsubst \
    '${API_URL} \
    ${ROOT_PATH} \
    ${WEBSOCKET_PATH} \
    ${IMAGE_PATH} \
    ${DNS_IP}' \
    < /etc/nginx/conf.d/default.conf.template \
    > /etc/nginx/conf.d/default.conf
cat /etc/nginx/conf.d/default.conf

# Set correct HTML base tag, so static resources are fetched
# from the right path instead of the root path.
# NOTE: Trailing and leading slashes in base href are important!
# Using `~` separator to avoid problems with forward slashes
sed --in-place \
  's~<base href="http://stackoverflow.com/">~<base href="http://stackoverflow.com/'$ROOT_PATH'/">~' \
  /usr/share/nginx/html/index.html

# Set WebSocket API endpoint
# Using `~` separator to avoid problems with forward slashes
sed --in-place \
  "s~webSocketPath.*,~webSocketPath: \`/$ROOT_PATH/$WEBSOCKET_PATH\`,~" \
  /usr/share/nginx/html/index.html

# Set image API endpoint
# Using `~` separator to avoid problems with forward slashes
sed --in-place \
  's~imageBaseUrl.*~imageBaseUrl: `${window.location}'$IMAGE_PATH'`~' \
  /usr/share/nginx/html/index.html

cat /usr/share/nginx/html/index.html

exec "$@"

3. Fichier Docker

# Produce static files
FROM node:10.15.3-alpine
WORKDIR /app
COPY ./package.json ./package.json
COPY ./package-lock.json ./package-lock.json
RUN npm set progress=false && \
    npm install --silent
COPY . /app
RUN npm run ng build -- --prod --output-path=dist

# Use NGINX to serve static files and re-route requests
FROM nginx:1.15.10-alpine
RUN rm -rf /usr/share/nginx/html/*
COPY --from=0 /app/dist/ /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf.template
COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD [ "nginx", "-g", "daemon off;" ]
EXPOSE 80

Angulaire

4. index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Fancy web app</title>
  <base href="http://stackoverflow.com/">
  <script>
    // These default values make sense when you `npm start`
    // They will be substituted during container startup
    // Using global variable on the window object here.
    // Is there a better way?
    window['app-config'] = {
      webSocketUrl: `${window.location.host}`,
      webSocketPath: `/socket.io`,
      imageBaseUrl: `${window.location}image`
    };
    console.log(window['app-config']);
  </script>

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <pdd-root></pdd-root>
</body>
</html>

5. app.conf.ts

import { Injectable } from '@angular/core';

@Injectable()
export class Configuration {
  webSocketUrl: string;
  webSocketPath: string;
  imageBaseUrl: string;
}

// Reading configuration out, so we have it Angular world 
export const AppConfiguration: Configuration = window['app-config'];

6. app.module.ts (extrait des détails inintéressants)

import <--snip-->

@NgModule({
  declarations: [
    <--snip-->
  ],
  imports: [
    <--snip-->
  ],
  providers: [
    <---snip-->
    SomeAngularService,
    { provide: Configuration, useValue: AppConfiguration },
    <---snip--->
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

7. some-angular.service.ts (extrait des détails inintéressants)

<--snip-->
export class BackendService {

  constructor(private configuration: Configuration) {

    const client = io(
      // Do not append route to the hostname
      // otherwise Socket.IO will treat it as `namespace`
      this.configuration.webSocketUrl,
      {
        // Do not auto-connect to exclude racing
        // between setup and connection
        autoConnect: false,
        // You have to specify route here
        path: configuration.webSocketPath
      });
      <--snip-->

Points forts d'Angular

8. proxy.conf.json

{
  "/socket.io/*": {
    "target": "http://localhost:3000/socket.io/",
    "ws": true,
    "secure": false,
    "logLevel": "debug",
    "pathRewrite": { "^/socket.io" : "" }
  },
  "/image/*": {
    "target": "http://localhost:3000/image/",
    "secure": false,
    "logLevel": "debug",
    "pathRewrite": { "^/image" : "" }
  }
}

9. angular.json -> voir la dernière ligne avant l'extrait ci-dessous

<--snip-->
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "fancy-web-app:build",
            "proxyConfig": "proxy.conf.json"
<--snip-->

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X