How can I make module federation remote URLs dynamic based on the environment (Test, QA, Dev)?
Asked Answered
C

2

8

I have set up a container app that consumes several remote apps. The issue is that I need the remote urls to be dynamic based on what environment they are in (Test, Dev, QA). As you can see my vehicle remote url is hardcoded pointing to dev. I need this URL to be updated based on the environment. I want to use env variables if possible. I can't find a clear answer and was hoping someone would have some suggestions.

const devConfig = {
  mode: "development",
  devServer: {
    port: 3000,
    historyApiFallback: true,
  },

  plugins: [
    new ModuleFederationPlugin({
      name: "container",
      filename: "remoteEntry.js",
      remotes: {
        vehicle:
          "vehicle@https://vehicle-mf-dev.com/remoteEntry.js",
      }
          exposes: {},
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
  ],
};
Comptroller answered 31/8, 2022 at 21:59 Comment(4)
can you access process.env object in this file ? try console.log(process.env) in this file.Ebneter
The documentation suggests using promise based remotes webpack.js.org/concepts/module-federation/… Haven't tried though. I use dynamic loading and discovery service which return the remote urls, which you can mock in dev environment.Bootless
Hi @ArtBauer,Could you share the detail of "dynamic loading and discovery service" way for this?Jaan
As for today, my discovery service is just a json file "modules.json" with list of "apps" containing urls pointing to apps deployments (but you can obtain it from the server). When building the application I copy this file to the output (/dist) folder. When the application starts it requests "modules.json" and use loadComponent function described here webpack.js.org/concepts/module-federation/…Bootless
A
2

You should use environment variables. Then, just replace your environment with your environment variable. It's to say:

1- Create an environment variable. You can use a .env file or create an environment variable. For instance, in UNIX systems you can do export API=https://vehicle-mf-dev.com.

More info: https://nodejs.dev/en/learn/how-to-read-environment-variables-from-nodejs/

2- Then you can use this environment variable in your code:

const devConfig = {
  mode: "development",
  devServer: {
    port: 3000,
    historyApiFallback: true,
  },

  plugins: [
    new ModuleFederationPlugin({
      name: "container",
      filename: "remoteEntry.js",
      remotes: {
        vehicle:
          `vehicle@${process.env.API}/remoteEntry.js`,
      }
          exposes: {},
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
  ],
};
Anamorphism answered 29/3, 2023 at 19:49 Comment(1)
This helps only at build time. Consider the case when the same js build is deployed to a DEV, QA and PROD environment. And in each environment the urls of the remote should be different.Quant
P
2

I encountered a similar issue that needed a fast fix for a client that had multiple customers with different domains for each customer so I had to get the webpack module federation to use dynamic remotes, I used relative paths for the remotes:

name: 'container',
    //The value is constructed as follows:
    // - the part before the '@' is the name mentioned in the remote ModuleFederationPlugin
    // - the part after the '@' is the URL of the remote entry file
    // WARNING: never name the remote project as a class name, it will cause a conflict
  remotes: {
    media: `media@/media/latest/remoteEntry.js`,
  },

In the browser, relative paths are resolved to the website's root path, which is perfect in this case. because the domain was the only difference in my case between the customers.

if you need other common variables between the micro frontends I found out that the go-to solution is to use a relative request in index.js to /config.json and inject the variables into the window object before calling the bootstrap file something like:

// main.js or index.js in the host app
async function loadConfig() {
  try {
    const response = await fetch('/config.json');
    const config = await response.json();
    window.__sharedConfig__ = config;
  } catch (error) {
    console.error('Error loading config.json:', error);
  }
}
(async () => {
  await loadConfig();
  await import('./bootstrap');
})();

NOTE: didn't test the snippet of the code loadConfig above take it more as an example of what suppose to be done.

if you need to template other env vars in the webpack config file after the bundle (I don't recommend this solution it feels 'hacky' for me; but didn't find alternatives for templating vars in the webpack config):

  • the bash script (you need to specify each env var in the envsubst or it will break the script); in the next example it will replace the string '$SOME_VAR' in all the bundle files with the desired value:
    #!/bin/sh
    
    # Replace the placeholders in the JavaScript files with the actual values
    if [ -n "$SOME_VAR" ]; then
      export SOME_VAR
      for file in /usr/share/nginx/html/container/latest/*.js; do
        envsubst "SOME_VAR" <"$file" >"$file.tmp"
        mv "$file.tmp" "$file"
      done
    fi
    
    # Start Nginx
    nginx -g "daemon off;"
  • the Dockerfile that I used (if you don't use the entrypoint solution comment out the last 3 steps: COPY, RUN chmod adn the ENTRYPOINT)
    FROM node:18-alpine3.17 as base
    
    WORKDIR /usr/src/app
    
    COPY yarn.lock tsconfig.json package.json nx.json lerna.json ./
    
    # copy package.json for install dependencies
    COPY ./packages/eslint-config/package.json ./packages/eslint-config/package.json
    COPY ./web/container/package.json ./web/container/package.json
    # install dependencies
    # TODO: need to move all relevat dependencies from devDependencies to dependencies
    # better even create package for webpack container configuration
    RUN yarn install --frozen-lockfile
    #  --production
    # copy source code
    COPY ./packages/eslint-config ./packages/eslint-config
    COPY ./web/container ./web/container
    # build container service
    RUN yarn build --scope=@premade/container-host
    
    # Use the official Nginx image as the base
    FROM nginx:stable-alpine3.17
    EXPOSE 80
    # Copy your built frontend assets
    COPY --from=base /usr/src/app/web/container/build /usr/share/nginx/html/container/latest
    # Copy your Nginx configuration
    COPY --from=base /usr/src/app/web/container/config/nginx.conf /etc/nginx/conf.d/default.conf
    # TODO: seems there a more popular way to do this
    # /config.json request on bootstrap to relative path
    # to serve common variables or env variables
    # Copy your entrypoint.sh script to handle the environment variables
    COPY ./web/container/scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
    # Set the proper permissions for the entrypoint.sh script
    RUN chmod +x /usr/local/bin/entrypoint.sh
    
    # Set the entrypoint.sh script as the entrypoint
    ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

used Lerna monorepo in the docker file I mention again I didn't use the entrypoint eventually and went with the relative path solution for some reason when you try to template remote paths it duplicates the domain in the remote path request, but it worked like a charm for other env vars in the webpack config.

Panto answered 11/4, 2023 at 21:39 Comment(2)
This is almost perfect, however Webpack is building urls using new URL() and relative path (w/o domain specified) does not want to compile...Rubbico
Hmm, it seems that we are using different versions of Webpack, the relative paths worked for me with version 5.76.2. I didn't encounter any URL Type errors during compilation.Panto

© 2022 - 2024 — McMap. All rights reserved.