Get Going with GitOps
Configuring Flipt for a GitOps workflow
Why GitOps?
GitOps is a set of practices centered around storing your application and infrastructure configuration in Git. The goal being to leverage the capabilities of version-control for your entire systems configuration.
If you’re already embracing GitOps practices for your other configuration, then aligning your feature flags with these same practices can complete the experience. No more correlating what changed in Git, with what changed in your feature flag system to understand the entire state of the world. Git is the single source of truth as GitOps intended.
So how do we achieve this with Flipt?
What You’ll Learn
In this guide you will:
- 🏁 Add a feature flag to an existing codebase
- 📝 Define the flag via Flipt’s configuration format
- 🌲 Add, commit, and push the change to a production-serving branch
- 🎯 Adjust our configuration to target an internal group of users in our organization
- 🌓 Progressively enable the flag for proportions of the user base
Setting the Scene
Our guide starts with an imaginary organization with a single web application defined in Go and committed to a GitHub repository. We as the developer, have been tasked to experiment with a new sorting algorithm on an endpoint for our application.
Our application handles requests from authenticated users. These users also happen to be grouped into organizations. We will use this information in our targeting rules later on.
This endpoint happens to list out a bunch of strings in a JSON array. The sorting algorithm previously used was slow (Bubble Sort), and we want to try something new (Quicksort). While we feel confident in our implementation, we’re going to practice caution and release the change behind a feature flag.
Structure
If you want to follow along you can fork our gitops guide repository.
.
├── go.mod
├── go.sum
├── main.go
└── pkg
└── server
└── words.go
Our application lives in a directory committed to a Git repository.
For the example’s sake, we assume the repository will be hosted on GitHub at https://github.com/organization/repository.git
.
The target of our change is a http.HandlerFunc
definition (in the file pkg/server/words.go
) with the name ListWords
.
Currently, the function uses a sorting function bubblesort
and we’re going to swap this for the quicksort
function.
func (s *Server) ListWords(w http.ResponseWriter, r *http.Request) {
words, err := getWords(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
bubblesort(words)
if err := json.NewEncoder(w).Encode(words); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
Calling Flipt
Instead of calling bubblesort
directly, we’re going to use the Flipt Go SDK to switch this call based on the feature flag use-quicksort-algorithm
.
This flag is going to be a boolean type flag, and so we use the sdk.Evaluation().Boolean()
call to evaluate the enabled
property of our flag.
We provide this evaluation call with a request containing the flags key, an entity ID and a context map.
The entity ID used here is going to be an identifier for the requests authenticated user. This is returned by the call to getUser(r.Context())
.
Our context map is going to contain a single key organization
, which is populated by a call to getOrganization(r.Context())
.
This will also return an identifier, only this time for the requesting user’s organization.
Flipt’s Declarative Backends
The focus of this guide is to leverage Flipt’s new “declarative backends” to enable a GitOps workflow. The name comes from the fact that the backend for Flipt is modeled around configuration files in a directory structure. These configuration files can coexist in a directory alongside other content (application code or other configuration code).
There currently exist four top-level declarative backend types:
local
(local directory)git
(remote Git repository and branch)object
(object storage, AWS S3 or Azure Blob Storage or Google Cloud Storage)oci
(OCI registry storage)
In this guide we will explore both the local
and git
backend types.
Defining Flag State Locally
In order for our application to work, it now depends on communicating with an instance of Flipt.
We’re going to configure our instance using a features.yml
file in the root of our existing project.
Then we will configure Flipt to serve directly from our local directory.
You don’t have to call your file features.yml
and you can spread your flag
definitions across multiple files. Checkout our docs on locating flag
state to learn more.
.
+├── features.yml
├── go.mod
├── go.sum
├── main.go
└── pkg
└── server
└── words.go
The contents of this file is going to start out with the definition of the use-quicksort-algorithm
flag.
This flag will be a boolean type flag and be in a disabled (enabled = false
) state.
version: "1.2"
namespace: default
flags:
- key: use-quicksort-algorithm
name: Use Quicksort Algorithm
type: BOOLEAN_FLAG_TYPE
enabled: false
Running Flipt Locally
Now the flag is defined in the current directory, we can run Flipt and configure the directory as the source of truth. This is useful for validating behaviour locally, before committing and pushing flag state to a production tracked Git repository.
The following command runs Flipt in Docker, with the local directory mounted and Flipt configured appropriately.
docker run -it --rm \
-p 8080:8080 \
-p 9000:9000 \
-v "$(pwd):/data" \
-e FLIPT_STORAGE_TYPE=local \
-e FLIPT_STORAGE_LOCAL_PATH=/data \
flipt/flipt:latest
This image demonstrates what can be seen in the Flipt UI with the configuration file we defined being served.
Now that Flipt is running locally, our application can also be run and configured to target our local instance of Flipt available at both http://localhost:8080
and grpc://localhost:9000
(depending on your protocol of choice). The UI is also available on port 8080
, however, it’s running in read-only mode since flag state is configured via the configuration file we defined before.
Running Flipt Over Git
The local
backend is useful for experimenting and exploring flag state in your development environment.
However, in a production setting, both the git
and the object
storage types are more appropriate.
Focussing on git
, the following command runs Flipt with a remote Git repository hosted on GitHub and tracking the main
branch.
docker run -it --rm \
-p 8080:8080 \
-p 9000:9000 \
-e FLIPT_STORAGE_TYPE=git \
-e FLIPT_STORAGE_GIT_REPOSITORY=https://github.com/organization/repository.git \
-e FLIPT_STORAGE_GIT_REF=main \
-e FLIPT_STORAGE_GIT_AUTHENTICATION_BASIC_USERNAME=username \
-e FLIPT_STORAGE_GIT_AUTHENTICATION_BASIC_PASSWORD=github-personal-access-token \
flipt/flipt:latest
In this example, Flipt has been configured to serve directly from our pretend repository with our application code in it.
Flipt will track the HEAD of this repository’s main
branch.
Changes will eventually propagate into our running instance of Flipt.
Head to Configuration: Storage: Declarative Backends to learn more about configuring these backend types for Flipt.
Pushing Our New Flag To Production
For sake of this demonstration, we’re going to assume Flipt has been deployed and configured in this way for our production environment. Our production deployment of our words endpoint will also have been configured to connect to this running instance of Flipt.
Given our repository is now being tracked and served by Flipt, we can add, commit, and push both our changes to our endpoint, as well as our new features.yml
file to our branch main
.
git add pkg/server/words.go
git add features.yml
git commit -m "feat: define the use-quicksort-algorithm flag"
git push origin main
Once Flipt has received the updated reference, our flag should be available through Flipt’s API.
The code change we added can now safely reference the present flag, which is currently in a disabled
state.
You can use curl
to ensure the service is still behaving as expected:
# the -w option prints timing output at the end
curl -w "\nTotal: %{time_total}s\n" "http://localhost:8000/words"
Next we will begin to enable the flag under different conditions.
Targeting and Rollouts
Now that our application is deployed with our code change and is referencing our new flag, we can adjust the state via the configuration file and push the changes to Git.
We will start by checking out a new branch, as we’re going to propose our change as a pull request and get review from a colleague.
git checkout -b enable-flag-for-internal-organization
Internal Users
We open the features.yml
file and update the definition with a new segment and add a rollout rule on our flag which returns enable = true
when the request matches our new segment.
Breaking this change down we’ve got:
The internal-users
Segment
# ...
segments:
- key: internal-users
name: Internal Users
match_type: ANY_MATCH_TYPE
constraints:
- property: organization
operator: eq
value: internal
type: STRING_COMPARISON_TYPE
This segment definition matches any evaluation request where there exists a key organization
on the context, with a value internal
.
Remember we added the key organization
earlier when defining the flag in code.
We used a value derived from the request (getOrganization(r.Context())
).
This got the organization identifier for the calling user.
Now, when the user happens to be associated with the internal
organization, it will match the internal-users
segment in Flipt.
A New Segment Rollout Rule
flags:
- key: use-quicksort-algorithm
# ...
rollouts:
- segment:
key: internal-users
value: true
Finally, we add a rollout rule to our boolean flag.
This rule allows us to override the enabled
property of the flag under certain conditions.
In this instance, we’re using the segment
type rule to say when the request matches the internal-users
segment, return the value true
.
Now our flag is configured to target internal users and enable the Quicksort algorithm for those users instead.
Proposing and Integrating Our Change
Next, we add, commit, and push the change to our branch.
git add features.yml
git commit -m "feat: enable use-quicksort-algorithm for internal-users"
git push enable-flag-for-internal-organization
From here, we can open a pull-request and get feedback from our team.
Once approval has been given, we can merge the PR and the change goes live.
When the change has been merged into main
, Flipt will eventually start serving this new configuration change.
We can now request our application as an authenticated user.
Users inside the internal
organization should get results sorted with the new Quicksort algorithm.
Whereas, the rest of users should still be served using the old Bubble Sort algorithm.
Validating the change before exposing it to a wider audience is your opportunity to identify and fix any issues. Perhaps the algorithm is incorrect, maybe the order has reversed or has become unstable for some entries. You can fix those changes now before targeting more users.
curl -w "\nTotal: %{time_total}s\n" "http://localhost:8000/words?org=internal-users"
Proportional Rollout
Once we’re confident our change is working as expected, since we’ve validated the change in production for internal
users, we can start to roll it out to external users.
The flipt
binary has the sub-command flipt validate
.
This can be used to statically validate your Flipt feature configuration files.
You can install this to run during a CI step and catch bugs before merging changes into main
.
For GitHub, try our pre-built Flipt Setup Action.
We could enable the flag for all users at once, but there is always a chance we’ve missed something during manual validation. Everyone has the best intentions, however. things get missed. So instead we’re going to start slow and gradually enable it for percentages of our user base. This will give the change time to bake with your audience. If anything is wrong, we’ve minimized the effected users to a small subset.
Once again we’re going to checkout a branch.
git checkout -b enable-flag-for-20-percent
Then we’re going to edit features.yml
and add a threshold percentage rule.
Again, breaking this change down:
A New Threshold Rollout Rule
Here we’re adding a new, different rollout rule type to our existing flag. Note that we’ve left our segment targeting rule intact.
This means our flag will remain enabled for internal users.
Rollout rules are evaluated on each request in order.
The first rule to match will result in the flags enabled property returning the configured value
.
If no rules match, then the flags top-level enabled
property is used as the final default return value.
flags:
- key: use-quicksort-algorithm
# ...
- segment:
key: internal-users
value: true
- threshold:
percentage: 20
value: true
This threshold percentage is set to 20
, meaning roughly 20%
of entity IDs will match and cause the flag to return enabled = true
.
Head to Concepts: Bucketing to learn how this mechanism is actually achieved.
Proposing and Integrating Our Change
Once again, we add, commit, and push the change to our branch.
git add features.yml
git commit -m "feat: enable use-quicksort-algorithm for 20% of users"
git push enable-flag-for-20-percent
From here, we can open a pull-request and get feedback from our team. Once we’ve got approval, we can merge the PR and the change goes live.
Closing the Loop
This process can be repeated, each time increasing the percentage
property of this newly added threshold
rollout rule.
Once we get to the stage of enabling the flag for 100% of users, we can either set the percentage to 100
or we can remove the targeting rules altogether and set the enabled
property to true
.
version: "1.2"
namespace: default
flags:
- key: use-quicksort-algorithm
name: Use Quicksort Algorithm
type: BOOLEAN_FLAG_TYPE
enabled: true
Beyond this, we can further close the loop by removing the feature flag call from our application code and simply use the new quicksort
function.
func (s *Server) ListWords(w http.ResponseWriter, r *http.Request) {
words, err := getWords(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
quicksort(words)
if err := json.NewEncoder(w).Encode(words); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
Recap
We’ve successfully rolled a feature out to production using GitOps practices via Flipt’s declarative feature flag configuration files and declarative backends. Along the way, we had the opportunity to use Git to understand the current state of the world. At each commit, we could’ve fully recreated the configuration of our entire application, including the state of Flipt itself.
Further Considerations
Now you have all the tools necessary to practice GitOps with your feature flags.
You might want to consider the object
or the oci
backend if your source Git repositories are too large (we’re working on a guide for that now).
The declarative storage backends currently mandate that the UI is read-only. We’ve thoughts on how this could change in the future, but for now, this is a limitation. You always have your editor, Git and the SCMs (GitHub, Gitlab etc) for state management in the meantime.
Each of these backends work by polling their sources (git, oci, local directory or object store) and the interval can be configured. Checkout the Configuration: Storage: Declarative for details on adjusting these intervals.
Was this page helpful?