How to use Feature Flags
Introduction
MilMove uses Flipt for feature flags. See ADR 0082 Use Flipt for Feature Flags for why Flipt was chosen and ADR 0084 Deploy Flipt using ECS service discovery for how Flipt is deployed in AWS.
While MilMove uses Flipt, it has a generic feature flag API that hides most of Flipt's implementation details. This allows environment variables to be used to simulate feature flags in local development without running a Flipt server. Additionally, this would also facilitate moving to another feature flag provider in the future should that be desirable.
The feature flag data is stored in a separate, private repository transcom/milmove-feature-flags
.
As of 2023-09-06, feature flags have not been used for a full end to end feature deployment. This document may have some gaps because of that.
Feature Flag Evaluation
First, familiarize yourself with the key Flipt Concepts.
Each deployed environment (demo, exp, loadtest, stg, prd) is in its own Flipt Namespace and only that single namespace is available in each environment.
Flipt supports two kinds of feature flags:
Boolean flags are for when a feature can only be turned on or off. Variant flags are for when a feature might have multiple options.
The most common Entity to work with is a user and that is probably how most feature flags will be used: to determine if a particular user should have access to a feature or not.
The Context contains multiple bits of information that can by used by Flipt to determine if a feature should be available to a user or not.
MilMove Backend Feature Flag Service
We use the backend service object for feature flags to wrap the specific endpoints in the Flipt API. This enables environment variable based feature flags in local development, with the additional benefit of protecting against any temporary unavailability on Flipt's end.
For development environments, the feature flag service object will utilize environment variables set in .envrc
. This is because if a Flipt URL is not provided, it will default to the EnvFetcher service object. Internal endpoints to fetch the feature flag value will utilize the same service object, but return different values depending on environment and configuration.
The current APIs for in the backend are GetBooleanFlagForUser
and GetVariantFlagForUser
. These methods have been added in transcom/mymove#11330. The feature flag service object sets the entityID
for those requests to the ID of the user making the request. It populates some default information in the context, including:
- The application name (mil, office, admin)
- If the user is an admin user, service member, or office user
- The permissions of the user
As we get more experience with feature flags, more information may be added to the default context of a user.
In addition, the API allows the caller to provide additional context that could be used for a specific feature flag.
For feature flags that are not specific to a particular user, use the GetBooleanFlag
and GetVariantFlag
APIs for full customization of the EntityID
and Context
. These methods have been added in transcom/mymove#11330.
MilMove Backend Feature Flag Swagger API
The GetBooleanFlagForUse
and GetVariantFlagForUser
APIs are exposed in the internal API via the /feature-flags/user-boolean/{key}
and /feature-flags/user-variant/{key}
endpoints so that the frontend can query feature flag status. These endpoints have been added in transcom/mymove#11330.
MilMove Frontend Feature Flag Component
To allow for customizing the user presentation based on feature flags, we have a FeatureFlag
component that uses the internal API to query the feature flag status. Additionally, an asynchronous util function can be created to be reused for logic handling within components.
Example Feature Flag Usage
Backend Boolean Feature Flag Usage
If you are checking for a boolean flag value in the area of the Prime API, you should use the GetBooleanFlag() function rather than the GetBooleanFlagForUser() function. The Prime API (using primelocal as the base URL for testing) is our way to test mTLS functionality. The GetBooleanFlagForUser() function relies on a session being created, which we don't have in the case of TLS authentication. Using GetBooleanFlagForUser() without that session will result in a nil session error, resulting in the Feature Flag check always returning False. Using GetBooleanFlag() does not rely on a session, and allows the true Feature Flag value to be checked and also ensures the the TLS handshake is truly under test.
Imagine you have a new endpoint that adds new functionality that you aren't ready to expose to every user. Add code to your handler that looks like
const newFeatureFlagName = "can-use-feature"
func (h SomeNewHandler) Handle(params newfeatureop.FeatureParams) middleware.Responder {
return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest,
func(appCtx appcontext.AppContext) (middleware.Responder, error) {
canUseFeature := false
flag, err := h.FeatureFlagFetcher().GetBooleanFlagForUser(params.HTTPRequest.Context(), appCtx, newFeatureFlagName, map[string]string{})
if err != nil {
// Some error reaching the feature flag server. Log it
// and set the default to false
appCtx.Logger().Error("Error fetching feature flag", zap.String("featureFlagKey", newFeatureFlagName), zap.Error(err))
canUseFeature = false
} else {
// the request was successful
canUseFeature = flag.Match
}
if !canUseFeature {
return newfeatureop.NewFeatureForbidden(), nil
}
// the actual handler code goes here
}
}
Backend Variant Feature Flag Usage
Imagine you have want to have some parameter that is different per user.
const newFeatureFlagName = "new-feature"
func (h SomeNewHandler) Handle(params newfeatureop.FeatureParams) middleware.Responder {
return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest,
func(appCtx appcontext.AppContext) (middleware.Responder, error) {
defaultSize := 100
someThingSize := defaultSize
flag, err := h.FeatureFlagFetcher().GetVariantFlagForUser(params.HTTPRequest.Context(), appCtx, newFeatureFlagName, map[string]string{})
if err != nil {
// Some error reaching the feature flag server. Log it
// and set the default to false
appCtx.Logger().Error("Error fetching feature flag", zap.String("featureFlagKey", newFeatureFlagName), zap.Error(err))
someThingSize = defaultSize
} else {
// the request was successful
if flag.Match {
someThingSize, err = strconv.Atoi(flag.Variant)
if err != nil {
someThingSize = defaultSize
}
}
}
things := models.GetThings(appCtx.DB(), someThingSize)
}
}
Frontend Defined Util for Feature Flag Logic
And once you have it created in envrc, navigate to the component you would like to implement feature flag logic for. Then, import your new function and use it
import { isBooleanFlagEnabled } from '../../utils/featureFlags';
For a class, add it to state and then call it in the componentDidMount
export class App extends Component {
constructor() {
this.state = {
featureFlag: false,
};
}
componentDidMount() {
isBooleanFlagEnabled('your_ff_here').then((enabled) => {
this.setState({
featureFlag: enabled,
});
});
}
}
For a function, call and assign it in the useEffect
const [yourFFHere, setYourFFHere] = useState();
useEffect(() => {
const fetchData = async () => {
isBooleanFlagEnlabed('your_ff_here').then((enabled) => {
setYourFFHere(enabled);
});
};
fetchData();
}, [setErrorState]);
Frontend Boolean Feature Flag Rendering
Imagine you want to enable some new workflow only for certain users.
import { FeatureFlag } from 'components/FeatureFlag/FeatureFlag';
export const MyThing = () => {
const enabledThing = <div>You can do the thing!</div>;
const disabledThing = <div>Sorry, you can't do the thing.</div>;
const featureFlagRender = (flagValue) => {
if (flagValue === 'true') {
return enabledThing;
} else {
return disabledThing;
}
};
return <FeatureFlag flagKey="new-feature" render={featureFlagRender} />;
};
Frontend Variant Feature Flag Rendering
Imagine you want to display different things to different users
import { FeatureFlag } from 'components/FeatureFlag/FeatureFlag';
export const MyThing = () => {
const thingOne = <div>This theme is Thing One!</div>;
const thingTwo = <div>This theme is Thing Two!</div>;
const thingThree = <div>This theme is Thing Three!</div>;
const featureFlagRender = (flagValue) => {
switch (flagValue) {
case 'thingOne':
return thingOne;
case 'thingTwo':
return thingTwo;
default:
// default to thingThree
return thingThree;
}
};
return <FeatureFlag flagKey="new-feature" render={featureFlagRender} />;
};
Jest Testing Feature Flags
import { isBooleanFlagEnabled } from 'utils/featureFlags';
Add the following block to the top of your test file:
jest.mock('utils/featureFlags', () => ({
...jest.requireActual('utils/featureFlags'),
isBooleanFlagEnabled: jest.fn().mockImplementation(() => Promise.resolve(false)),
}));
You can mock the boolean value in your tests with:
isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(YOUR_BOOLEAN_VALUE_HERE));
Deploying Feature Flags
Each deployed environment has its own feature flag configuration. The idea is that you can test a configuration in one environment and then copy the relevant config to the other environment(s).
Note that each environment is in its own namespace (e.g. the demo
environment is in the demo
namespace). That is configured in YAML
file, so you wouldn't want to copy an entire file from one environment
to another without ensuring that the namespace is correct for the new
environment.
Step by step
- Create a branch in git on the
transcom/milmove-feature-flags
repository. - Edit the file in the
feature_flags
directory corresponding to the environment. - Commit the file and open a PR
- When the PR is merged, the file for that environment will be copied to the S3 bucket for that environment. Flipt checks for updates in S3 periodically, and so the new settings should automagically appear in Flipt. This applies to the live AWS app, not development.
- Create a branch in git on the
transcom/mymove
repository. - Update the
.envrc
file adding a new exported environment variable with the following naming convention:FEATURE_FLAG_${Feature_Flag_Key}
. The key should be in screaming snake case, and match the key intranscom/milmove-feature-flags
. - Commit the file and open a PR.
- When the PR is merged, the development environment will now have the feature flag.
- Add the feature flag to the CircleCI deployment flags
If for some reason you want to force a copy (e.g. the S3 bucket has changed), add/update a comment at the top of the file and open + merge a PR.
See the Flipt documentation for more on the file format.
Local Testing
Local testing and development run off the "EnvFetcher" which does not use the Flipt container. Please update the .envrc
file accordingly during development to test turning features on and off.
However, to test with a live Flipt container to simulate exactly how it's running in the AWS environments, then please proceed with the following section.
You can run Flipt locally with the feature flags for testing. When running locally in development mode, the namespace is development
.
The idea is to copy the environment you want to test from milmove-feature-flags
to the mymove
project and update the namespace to development
.
# cd some/path/where/you/have/mymove
#
# now replace the namespace with `development` for local testing
# and copy it to our local dev environment
sed 's,^namespace:.*,namespace: development,' \
< ../milmove-feature-flags/feature_flags/someenv_features.yaml \
> config/flipt/storage/development.features.yaml
make feature_flag_docker
# now in another window you can start the server
FEATURE_FLAG_SERVER_URL=http://localhost:9080 make server_run
# and you can start the client in another window
make client_run
The admin console has a limited feature flag testing page you can use. Go to http://adminlocal:3000/system/feature-flags. If actually adding a new feature flag, you would want to test your code path directly.
Use the evaluation console to see if the results are what you expected. Go to http://localhost:9080 to see the Flipt console locally.
CircleCI deployment flags
Failing to set CircleCI deployment flags will lead to faulty builds as CircleCI cannot natively find which feature flags are enabled or disabled. It does not even see .envrc
that developers utilize.
To set a feature flag as enabled or disabled within certain deployment environments, you must update the corresponding config environment variables. CircleCI deployment environments can be found under the config/env folder.
When you export your feature flag variable for a specific environment, make sure you update its "app" and its respective "app-client-tls" file. Example:
// ${environment} can be "prd", "exp", "stg", "loadtest", "demo", "review", and so on as environments change.
config/env/{$environment}.app.env
config/env/{$environment}.app-client-tls.env
Adding FEATURE_FLAG_EXAMPLE=false
or true
per environment. You do not need to add export
to the front like you would in a typical .envrc
file.
Updating feature flag deployments
See the transcom/milmove-feature-flags
repository for information how to update the deployments (e.g. update the image used by flipt).