You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
277 lines
13 KiB
277 lines
13 KiB
5 years ago
|
# Build-Specific Configuration
|
||
|
|
||
|
## tl;dr's
|
||
|
|
||
|
ngx-rocket comes with a very helpful `env` script that will save environment-variables set at build time to constants that can be used as configuration for your code. When combined with the `dotenv-cli` package, it enables maximum configurability
|
||
|
while maintaining lots of simplicity for local development and testing.
|
||
|
|
||
|
### Cookbook for maximum independence of deployment-specific configuration
|
||
|
|
||
|
Disclaimer: If you have a full-stack app in a monorepo, keep separate `.env` files for server-side and client-side
|
||
|
configs, and make sure `.env` files are .gitignore'd and that secrets never make it into client-side `.env` file.
|
||
|
|
||
|
For each configurable variable (e.g. BROWSER_URL, API_URL):
|
||
|
|
||
|
- Add it to package.json's env script so that the build-time variables will be saved for runtime:
|
||
|
|
||
|
```javascript
|
||
|
{
|
||
|
"scripts": {
|
||
|
"env": "ngx-scripts env npm_package_version BROWSER_URL API_URL",
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
- Add it to or edit it in src/environments/environment.ts to expose it to your app as e.g. environment.API_URL:
|
||
|
```typescript
|
||
|
export const environment = {
|
||
|
// ...
|
||
|
API_URL: env.API_URL,
|
||
|
BROWSER_URL: env.BROWSER_URL
|
||
|
// ...
|
||
|
};
|
||
|
```
|
||
|
- Configure your CI's deployment to set the variables and export them to the build script before building - if your CI
|
||
|
gives you a shell script to run, make it something like this:
|
||
|
```shell
|
||
|
# bourne-like shells...
|
||
|
export API_URL='https://api.staging.example.com'
|
||
|
export BROWSER_URL='https://staging.example.com'
|
||
|
# ...
|
||
|
npm run build:ssr-and-client
|
||
|
```
|
||
|
- Finally, to have your cake and eat it too and avoid having to do all that for local development and testing (or clutter
|
||
|
your package.json up), install the `dotenv-cli` package and update your development-related npm scripts to take advantage
|
||
|
of it:
|
||
|
```shell
|
||
|
# environment.development.env.sh
|
||
|
BROWSER_URL='http://localhost:4200'
|
||
|
API_URL='http://localhost:4200'
|
||
|
```
|
||
|
```javascript
|
||
|
{
|
||
|
"scripts": {
|
||
|
"start": "dotenv -e environment.development.env.sh -- npm run env && ng serve --aot",
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
This way, app configurations will always come from deploy-specific environment variables, and your development environments
|
||
|
are still easy to work with.
|
||
|
|
||
|
For configuring the build itself (for example, if you want your QA build to be similar to your production build, but with
|
||
|
source maps enabled), consider avoiding adding a build configuration to angular.json, and instead adding the respective
|
||
|
overriding flag to the `ng` command in package.json:
|
||
|
|
||
|
```javascript
|
||
|
{
|
||
|
"scripts": {
|
||
|
"build:client-and-server-bundles:qa": "NG_BUILD_OVERRIDES='--sourceMap=true' npm run build:client-and-server-bundles",
|
||
|
"build:client-and-server-bundles": "npm run build:client-bundles && npm run build:server-bundles",
|
||
|
"build:client-bundles": "npm run env && ng build --prod $NG_BUILD_OVERRIDES",
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
The development server API proxy config can read runtime environment variables, so you can avoid having a superficial
|
||
|
dev-server configuration by taking advantage of them:
|
||
|
|
||
|
```javascript
|
||
|
{
|
||
|
"scripts": {
|
||
|
"start": "dotenv -e environment.development.env.sh -- npm run env && API_PROXY_HOST='http://localhost:9000' ng serve --aot",
|
||
|
"e2e": "ngtw build && npm run env && API_PROXY_HOST='http://localhost:7357' ng e2e --webdriverUpdate=false",
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
```javascript
|
||
|
const proxyConfig = [
|
||
|
{
|
||
|
context: '/api',
|
||
|
pathRewrite: { '^/api': '' },
|
||
|
target: `${process.env.API_PROXY_HOST}/api`,
|
||
|
changeOrigin: true,
|
||
|
secure: false
|
||
|
},
|
||
|
{
|
||
|
context: '/auth',
|
||
|
pathRewrite: { '^/auth': '' },
|
||
|
target: `${process.env.API_PROXY_HOST}/auth`,
|
||
|
changeOrigin: true,
|
||
|
secure: false
|
||
|
}
|
||
|
];
|
||
|
```
|
||
|
|
||
|
Quick SSR note: SSR works by building all the client bundles like normal, but then rendering them in real-time. So,
|
||
|
|
||
|
- the rest of your app from `main.server.ts` down has access to your build-time environment only, like your normal
|
||
|
client bundles
|
||
|
- but `server.ts` (the file configuring and running express) has access to your serve-time environment variables
|
||
|
|
||
|
### Less optimal alternatives
|
||
|
|
||
|
- On the opposite extreme of the spectrum, you can keep all build-specific configuration in a separate environment
|
||
|
file for each environment using Angular's built-in `fileReplacements`, but then you'll need a separate environment
|
||
|
file even for deployment-specific configuration (like hostnames), which can get out of hand fast.
|
||
|
- For a middle-of-the-road approach, you can divide configuration into two groups:
|
||
|
- Configuration shared by each environment-type:
|
||
|
- Environment-type examples include local development, staging/qa, test, production...
|
||
|
- Examples of configuration like this include:
|
||
|
- In test, animations are always disabled, but for all other environments, they're enabled
|
||
|
- In production, the payment gateway's publishable key is the live key, but all other environments use the
|
||
|
test key
|
||
|
- Configuration that sometimes needs to be specific to an individual deployment of a given environment:
|
||
|
- Examples of configuration like this include:
|
||
|
- This particular staging/qa server's base for constructing URLs is qa-stable.example.com, but qa/staging
|
||
|
environments could also be deployed to preprod.example.com or localhost:8081 or anywhereelse:7000.
|
||
|
- This particular deployment uses a specific bucket for Amazon S3 uploads
|
||
|
- In this approach, you can use Angular's `fileReplacements` for anything environment-specific and ngx-rocket's
|
||
|
`env` for anything deployment-specific. You can even have certain deployment-specific configuration fall back
|
||
|
to environment-specific defaults for certain environments like so:
|
||
|
```javascript
|
||
|
export const environment = {
|
||
|
// ...
|
||
|
BROWSER_URL: env.BROWSER_URL || 'https://qa.example.com'
|
||
|
// ...
|
||
|
};
|
||
|
```
|
||
|
- If you don't have lots of environment variables, you can avoid dotenv-cli and use your particular shell's method
|
||
|
to expose the variables before running the ngx-rocket env tool.
|
||
|
|
||
|
## Introduction
|
||
|
|
||
|
When building any maintainable application, a separation of configuration and code is always desired. In the case
|
||
|
of configuration, some of it will need to vary from environment to environment, build to build, or deployment to
|
||
|
deployment.
|
||
|
|
||
|
This guide focuses on this type of build-specific configuration in a very broad sense of an Angular app, describing
|
||
|
the specific Angular ways of controlling these configurations, detailing some angular-specific challenges, and
|
||
|
highlighting some ngx-rocket tooling that help with them in mind.
|
||
|
|
||
|
For an even broader non-Angular introduction of these concepts, see the
|
||
|
[The Twelve-Factor App](https://12factor.net/config) methodology's opinions on how this type of configuration
|
||
|
should be managed.
|
||
|
|
||
|
## Types of configuration
|
||
|
|
||
|
At the highest level, build-specific configuration can be divided into two categories:
|
||
|
|
||
|
1. Configuration for how your app is built and served
|
||
|
2. Configuration used by your codebase
|
||
|
|
||
|
### Configuration for how your app is built and served
|
||
|
|
||
|
This type of build-specific configuration is not used by your code, but is used to control the build system itself.
|
||
|
Configuration like this goes into Angular's
|
||
|
[workspace configuration](https://angular.io/guide/workspace-config#alternate-build-configurations). Instead of
|
||
|
rehashing existing documentation on this, this document will highlight how it relates to this subject. Namely, the
|
||
|
fact that in addition to specifying _HOW_ the app is built for each build configuration, the workspace configuration
|
||
|
allows mapping each build configuration to a separate environment configuration file for your codebase as well. It
|
||
|
also allows for making separate dev-server configurations in case you need to run it differently.
|
||
|
|
||
|
Therefore, each build configuration in the workspace configuration file is a tuple of
|
||
|
(how-to-build, environment-file-for-codebase), and you'll need a separate configuration for each combination.
|
||
|
|
||
|
## Angular's out-of-the-box environment configuration
|
||
|
|
||
|
### When it works well
|
||
|
|
||
|
This setup works quite well for configuration that's shared among all instances of an environment, like the following
|
||
|
examples:
|
||
|
|
||
|
- **test** environment always builds without source maps, disables animations, uses a recaptcha test key, and disables
|
||
|
analytics
|
||
|
- **dev** environment always builds with source maps, enables animations, uses a recaptcha live key, and disables
|
||
|
analytics
|
||
|
- **qa** environment always builds with source maps, enables animations, uses a recaptcha live key, and disables
|
||
|
analytics
|
||
|
- **prod** environment always builds without source maps, enables animations, uses a recaptcha live key, and enables
|
||
|
analytics
|
||
|
|
||
|
### Limitations of Angular's `fileReplacements`
|
||
|
|
||
|
But for certain deployment-specific configuration, things start to get really hairy, like in these examples:
|
||
|
|
||
|
- QA build configuration needs to be built for local deployment, deployment to a server on the internet for QA
|
||
|
purposes, and also deployment to another server on the internet for staging purposes
|
||
|
- Production build needs multiple different deployments of the same app to different servers
|
||
|
|
||
|
These cases can cause problems when:
|
||
|
|
||
|
- Each deployment needs a separate API URL
|
||
|
- Each deployment needs a separate URL for building its own URLs to where it's deployed
|
||
|
- Each deployment needs separate API keys, bucket names, etc
|
||
|
|
||
|
You _COULD_ start creating separate configurations for each deployment, each with its own `fileReplacements`, but that
|
||
|
would be really messy.
|
||
|
|
||
|
### Workarounds that don't work well
|
||
|
|
||
|
One workaround would be to keep such configurations as globals in a separate deployment-specific script file. But
|
||
|
that's pretty messy too. More importantly, there are limitations to where they can be used. For example, because
|
||
|
of AOT, such configuration variables cannot be used in Angular's decorators, because they're not statically
|
||
|
analyzable (i.e. their values knowable at build-time). So it would be better if we can keep everything in the same place.
|
||
|
|
||
|
### ngx-rocket to the rescue
|
||
|
|
||
|
The ngx-rocket `env` task solves this problem really well, and avoids the need for separate `environment.ts` files for
|
||
|
deployment-specific configuration.
|
||
|
|
||
|
To add a deployment-specific configuration:
|
||
|
|
||
|
1. edit the existing `environment.ts` files for whichever environments you'd like to make that variable
|
||
|
deployment-specific for by having it come from the imported "env" object - pro tip: you can even make it fall
|
||
|
back to an environment-based default and still be statically analyzable!
|
||
|
2. add that variable name to the npm script's `env` task
|
||
|
|
||
|
Now, as long as you have that environment variable set in the shell running the build, the `env` task will save it into
|
||
|
the `.env.ts` file before building.
|
||
|
|
||
|
If you really want, you can take things even further to the twelve-factor extreme, and you can even eliminate the
|
||
|
need for `fileReplacements` entirely, and make all configuration come from environment variables. Whether this will be
|
||
|
the right approach for your project will be up to you.
|
||
|
|
||
|
This makes separate deployments awesome and flexible, but unfortunately makes things a little bit of a hassle for your
|
||
|
local development, test, etc. environments because you have the burden of providing all those keys, settings, etc. as
|
||
|
environment variables.
|
||
|
|
||
|
To avoid having to do that, you'll can create a .gitignore'd `.env` file with all the variables set, and source it
|
||
|
with your shell (e.g. `source .env.sh && npm env` in bourne-like shells or `env.bat; npm env` in windows).
|
||
|
|
||
|
```shell
|
||
|
# bourne-like .env.sh
|
||
|
export BROWSER_URL=localhost:4200
|
||
|
```
|
||
|
|
||
|
```shell
|
||
|
REM windows env.bat
|
||
|
SET BROWSER_URL=localhost:4200
|
||
|
```
|
||
|
|
||
|
Luckily for us, there's a package called `dotenv-cli` that uses the `dotenv` package and does this in a cleaner and
|
||
|
cross-platform way and comes with even more bells and whistles. You should use that instead, and make your env file
|
||
|
like this instead:
|
||
|
|
||
|
```shell
|
||
|
BROWSER_URL=localhost:4200
|
||
|
```
|
||
|
|
||
|
## When you can use environment variables directly without ngx-rocket `env`
|
||
|
|
||
|
As a sidenote, ngx-rocket `env` isn't used for the proxy config file, because it isn't built and ran separately.
|
||
|
Fortunately, for that same reason, you can directly use `process.env` within the proxy config file to avoid having
|
||
|
separate proxy configs in most cases.
|
||
|
|
||
|
On that same note, the `server.ts` for SSR builds can also access `process.env` as it's set at runtime. But keep in mind
|
||
|
that it stops there - the app itself is built, so even in SSR the client app can't access process environment variables.
|
||
|
|
||
|
## Security Considerations
|
||
|
|
||
|
Never forget that your entire Angular app goes to the client, including its configuration, including the environment
|
||
|
variables you pass to the env task! As usual, you should **never add sensitive keys or secrets to the env task**.
|
||
|
|
||
|
Finally, if your Angular project is the client-side of a full-stack monorepo, make sure to keep the client-side `.env`
|
||
|
file separate from the server-side `.env` file, since your server-side is bound to have secrets.
|