1. 2

I’ve seen some bad environment configuration patterns for single page applications. I won’t get into them now but, ultimately, they’ve all boiled down to one hard-to-solve problem: how do you serve a SPA which requires environment-specific settings, such as logging endpoints, be applied first-thing without running your code through a string-replace script? Where does that configuration live?

If you put the config in a separate file, you risk that file not loading in time, not being available, etc. If you put it at the top of the index, you have to inject environment-specific values into it.

I’m curious how others have solved this problem or even overcome the need to.

  1.  

  2. 2

    Mainly, I use a separate file served by the backend. But to point to the backend I often use a HTTP header, which in turn is configurable through an environment variable. (In reality, I use two headers : an environment name and the url to the backend)

    1. 1

      Are you serving the SPA off of a box, since it has access to ENV variables?

      1. 1

        I’m not sure to understand your question.

        I’m serving the SPA usually from a dedicated web server (nowadays from a nginx docker image but was a long time an apache server).

        As a static typing programmer, I stay far away from node or other front-end “server”. My SPAs are static files.

        1. 1

          Serving off of a container/server as opposed to static hosting like S3 and Cloudfront. That answered my question :)

          1. 1

            S3, Cloudfront and other performat static hosting did not exist when I begun.

            For one “new” project using S3. The bucket creation is configured by terraform, and a file is created close to the index.html

            But I think it is possible now to configure custom header with cloudfront, isn’t it?

    2. 2

      At $JOB, we just hard-code the settings for all four of our environments (dev, QA, staging, and prod) into the SPAs and choose the correct configuration at runtime based on the domain the app was loaded from ($APP.$ENV.$JOB.com; prod of course omitting the .$ENV). I suspect this probably qualifies as a bad pattern too for various reasons, but it’s the least inconvenient way any of us were able to come up with, and the risks of URLs to our non-prod environments being accessible to someone curious enough to wade through our JS bundles are pretty low for us.

      Our SPAs are all in React and bundled with Webpack, so we considered using the DefinePlugin to embed settings on a per-environment basis, but of course that would mean having a separate build artifact per environment, which sucks even more than what we ended up doing… plus it is basically a string-replace script anyway, albeit one smart enough to only operate on identifiers.

      1. 2

        We’re also using Babel and Webpack to bundle the SPA. We use DefinePlugin to inject an environment-specific config object at build-time. For a few specific things, we also have Babel plugins that replace some code constructors with others, based on environment.

        We prefer to generate environment-specific artifacts than to either bundle all the configs, or make a separate HTTP request.

        1. 2

          That’s fair. There’s definitely not a one-size-fits-all answer to this question. Looking back on it now I realize it wasn’t entirely accurate to say that we hard-code our configuration. In our case, our only environment-dependent runtime configuration is which URLs to use for the API and CDN, so our “configuration” is basically just const hostname = window.location.hostname; const domain = hostname.substring(hostname.indexOf('.')); const apiBaseUrl = 'https://api' + domain;. It used to be the case that we had to hard-code this, because our “environments” used to just be Azure web app deployment slots, but once we were able to set up actual independent environments we were able to stop doing that.

      2. 1

        Here is what we do.

        Our webapps are built off React, and use underneath react-create-app machinery (with some overrides).

        That machinery allows to create ‘environment’ variables in .env, that, in turn, can also be ovewritten with command line switches when making the optimized webapp build.

        Those env variables are then available via cra-build process as ‘const’ variables in the Javascript files (see comment in ansible task below).

        The default env variables (stored in .env next to package.json) default to ‘localhost’ , ‘3232’ port, if not overwritten by the build process.

        So as long as developer on their local machine, instanciate the backend server on port 3232. It will work But production and integration enviroments ports/hosts are different of course.

        So with that webapp builds we produce, have ‘hardcoded’ the build-time environment variables that represent the END point hosts, build numbers, and all the other switches.

        We manage production vs integration test environment variables, using ansible ’ host variables’ concepts, and other nuances. So the ‘real’ host IPs, and their ports, are known to the webapp build process via ansible .

        This is an example of an Ansible task that builds one of our webapps, and copies the resulting build into a special folder, from which Anisble’s, another deployment task will run

        # notice how build-relevant env variables are passed.
        # this is because react-app build scripts allow for this kind of overrides of .env
        # https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables
        #
        - name: build webapp amolith
          run_once: true
          shell: |
           DFLT_ENDPOINT_HOSTIP={{ restapihost__webapp__amolith }} DFLT_ENDPOINT_HOSTPORT={{ restapiport__webapp__amolith }}  REACT_APP_LRbuildRef={{ myAnsibleBuildNumGen }}__amolith    npm run obuild
          args:
            chdir: "{{ srcdir__webapp__amolith }}"
          register: result__amolith__obuild
          failed_when: >
           (result__amolith__obuild.stdout.find('The build folder is ready to be deployed') == -1 ) or
           (1 != 1)
        

        The {{ restapihost__webapp__amolith }} , {{ restapiport__webapp__amolith }} , {{ myAnsibleBuildNumGen }} come, as I noted earlier, from Ansible’s host inventory that are specific to each of the environments (in our case prod and integration testing, but of course you can have as many of those environments as needed)

        The same variables are used to generate the nginx config files (again using ansible’s role for nginx).

        This way nginx configs that we use to define loadbalancing, redirecting/etc use the same env variables are are known to the webapp… And as you can imagine, we use similar (often not the same) variables to define the configs for the backend – so when the backends start up, they listen on the ports are that are expected by the loadbalance, and the loadbalancer defines end points that were used to build the webapp.

        If we decide to change the port or end point address that the webapp has to go to, it will require a re-release of the webapp, and the whole ansible deployment onto the backends. But the actual change of the hostname+port will be done just in one place.

        1. 1

          I’ve used 1.1.1.1’s API to use SRV records.