diff --git a/flagd-demo/assets/gitea.app.ini b/flagd-demo/assets/gitea.app.ini new file mode 100644 index 0000000..8c1fc6f --- /dev/null +++ b/flagd-demo/assets/gitea.app.ini @@ -0,0 +1,19 @@ +APP_NAME = "Gitea: Git with a cup of tea" +RUN_USER = "git" +[server] +PROTOCOL = "http" +DOMAIN = "http://0.0.0.0:3000" +ROOT_URL = "http://0.0.0.0:3000" +HTTP_ADDR = "0.0.0.0" +HTTP_PORT = "3000" +[database] +DB_TYPE = "postgres" +HOST = "0.0.0.0:5432" +NAME = "giteadb" +USER = "gitea" +PASSWD = "gitea" +[repository] +ENABLE_PUSH_CREATE_USER = true +DEFAULT_PUSH_CREATE_PRIVATE = false +[security] +INSTALL_LOCK = true \ No newline at end of file diff --git a/flagd-demo/assets/gitea.service b/flagd-demo/assets/gitea.service new file mode 100644 index 0000000..819bfb8 --- /dev/null +++ b/flagd-demo/assets/gitea.service @@ -0,0 +1,20 @@ +[Unit] +Description=Gitea (Git with a cup of tea) +After=syslog.target +After=network.target + +Wants=postgresql.service +After=postgresql.service + +[Service] +RestartSec=2s +Type=simple +User=git +Group=git +WorkingDirectory=/var/lib/gitea/ +ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini +Restart=always +Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/flagd-demo/assets/images/flagd-logical-architecture.png b/flagd-demo/assets/images/flagd-logical-architecture.png new file mode 100644 index 0000000..c0933f9 Binary files /dev/null and b/flagd-demo/assets/images/flagd-logical-architecture.png differ diff --git a/flagd-demo/assets/images/flagd_logical_architecture.png b/flagd-demo/assets/images/flagd_logical_architecture.png new file mode 100644 index 0000000..bba40d3 Binary files /dev/null and b/flagd-demo/assets/images/flagd_logical_architecture.png differ diff --git a/flagd-demo/assets/images/fractional-evaluation.png b/flagd-demo/assets/images/fractional-evaluation.png new file mode 100644 index 0000000..2a72a67 Binary files /dev/null and b/flagd-demo/assets/images/fractional-evaluation.png differ diff --git a/flagd-demo/assets/scripts/intro_foreground.sh b/flagd-demo/assets/scripts/intro_foreground.sh new file mode 100644 index 0000000..1c30e5a --- /dev/null +++ b/flagd-demo/assets/scripts/intro_foreground.sh @@ -0,0 +1,166 @@ +#!/bin/bash + +DEBUG_VERSION=11 +GITEA_VERSION=1.19 +TEA_CLI_VERSION=0.9.2 +FLAGD_VERSION=0.4.4 + +# Download and install flagd +wget -O flagd.tar.gz https://github.com/open-feature/flagd/releases/download/flagd%2Fv${FLAGD_VERSION}/flagd_${FLAGD_VERSION}_Linux_x86_64.tar.gz +tar -xf flagd.tar.gz +mv flagd_linux_x86_64 flagd +chmod +x flagd +mv flagd /usr/local/bin + +# Download and install 'gitea' CLI: 'tea' +wget -O tea https://dl.gitea.com/tea/${TEA_CLI_VERSION}/tea-${TEA_CLI_VERSION}-linux-amd64 +chmod +x tea +mv tea /usr/local/bin + +################# +# Install postgresql for Gitea +################### +# Create the file repository configuration: +sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + +# Import the repository signing key: +wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + +# Update the package lists: +sudo apt-get update + +# Install the latest version of PostgreSQL. +# If you want a specific version, use 'postgresql-12' or similar instead of 'postgresql': +sudo apt-get -y install postgresql < /dev/null + +# Add 'git' user +adduser \ + --system \ + --shell /bin/bash \ + --gecos 'Git Version Control' \ + --group \ + --disabled-password \ + --home /home/git \ + git + +# Configure git for 'ubuntu' and 'git' users +git config --system user.email "me@faas.com" +git config --system user.name "OpenFeature" + +# Download 'gitea' +wget -O gitea https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64 +chmod +x gitea +mv gitea /usr/local/bin +chown git:git /usr/local/bin/gitea + +# Set up directory structure for 'gitea' +mkdir -p /var/lib/gitea/{custom,data,log} +chown -R git:git /var/lib/gitea/ +chmod -R 750 /var/lib/gitea/ +mkdir /etc/gitea +chown git:git /etc/gitea +chmod 770 /etc/gitea + +# Create systemd service for 'gitea' +# Ref: https://github.com/go-gitea/gitea/blob/main/contrib/systemd/gitea.service +mv ~/gitea.service /etc/systemd/system/gitea.service +# cat < /etc/systemd/system/gitea.service +# [Unit] +# Description=Gitea (Git with a cup of tea) +# After=syslog.target +# After=network.target + +# Wants=postgresql.service +# After=postgresql.service + +# [Service] +# RestartSec=2s +# Type=simple +# User=git +# Group=git +# WorkingDirectory=/var/lib/gitea/ +# ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini +# Restart=always +# Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea + +# [Install] +# WantedBy=multi-user.target +# EOF + +mv ~/gitea.app.ini /etc/gitea/app.ini +# cat < /etc/gitea/app.ini +# APP_NAME = "Gitea: Git with a cup of tea" +# RUN_USER = "git" +# [server] +# PROTOCOL = "http" +# DOMAIN = "http://0.0.0.0:3000" +# ROOT_URL = "http://0.0.0.0:3000" +# HTTP_ADDR = "0.0.0.0" +# HTTP_PORT = "3000" +# [database] +# DB_TYPE = "postgres" +# HOST = "0.0.0.0:5432" +# NAME = "giteadb" +# USER = "gitea" +# PASSWD = "gitea" +# [repository] +# ENABLE_PUSH_CREATE_USER = true +# DEFAULT_PUSH_CREATE_PRIVATE = false +# [security] +# INSTALL_LOCK = true +# EOF +chown -R git:git /etc/gitea + +# Set up gitea DB +sudo -u postgres -H -- psql --command "CREATE ROLE gitea WITH LOGIN PASSWORD 'gitea';" > /dev/null 2>&1 +sudo -u postgres -H -- psql --command "CREATE DATABASE giteadb WITH OWNER gitea TEMPLATE template0 ENCODING UTF8 LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8';" > /dev/null 2>&1 + +# Start gitea +systemctl start gitea +# Migrate the DB to create all required tables and config +sudo -u git gitea migrate -c=/etc/gitea/app.ini + +# Create a user called 'openfeature' +# With password 'openfeature' +sudo -u git gitea admin user create \ + --username=openfeature \ + --password=openfeature \ + --email=me@faas.com \ + --must-change-password=false \ + -c=/etc/gitea/app.ini + +sudo -u git gitea admin user generate-access-token \ + --username=openfeature \ + --scopes=repo \ + -c=/etc/gitea/app.ini \ + --raw > /tmp/output.log + +ACCESS_TOKEN=$(tail -n 1 /tmp/output.log) + +# Wait for Gitea to be available +# Timeout after 2mins +timeout 120 bash -c 'while [[ "$(curl --insecure -s -o /dev/null -w ''%{http_code}'' http://0.0.0.0:3000)" != "200" ]]; do sleep 5; done' + +# Authenticate the 'tea' CLI +tea login add \ + --name=openfeature \ + --user=openfeature \ + --password=openfeature \ + --url=http://0.0.0.0:3000 \ + --token=$ACCESS_TOKEN > /dev/null 2>&1 + +# Create an empty repo called 'flags' +# Clone the template repo +tea repo create --name=flags --branch=main --init=true > /dev/null 2>&1 +git clone http://openfeature:openfeature@0.0.0.0:3000/openfeature/flags +wget -O ~/flags/example_flags.flagd.json https://raw.githubusercontent.com/open-feature/flagd/main/config/samples/example_flags.flagd.json +cd ~/flags +git config credential.helper cache +git add -A +git commit -m "add flags" +git push + +# ---------------------------------------------# +# 🎉 Installation Complete 🎉 # +# Please proceed now... # +# ---------------------------------------------# \ No newline at end of file diff --git a/flagd-demo/finish.md b/flagd-demo/finish.md new file mode 100644 index 0000000..b15ebc4 --- /dev/null +++ b/flagd-demo/finish.md @@ -0,0 +1,42 @@ + +## Congratulations! 🎉 +In this tutorial you have built an OpenFeature compliant feature flag backend using flagd. + +You now have a pure GitOps feature flagging system. You can change only a JSON file and your application will automatically leverage the changes. + +This is just the beginning. flagd is capable of a lot more, such as providing multiple flag sources, local file usage or retrieval over HTTPS or gRPC. + +## What's Next? + +In this tutorial, interaction with the flagd API was via `curl`{{}}. In reality, you wouldn't want your application becoming reliant on flagd - that's the entire premise of OpenFeature. Your application should be able to say "getAFlag" without caring about the backend system (flagd in this case). + +To achieve this, OpenFeature offers [language specific flagd providers](https://github.com/open-feature/flagd/blob/main/docs/usage/flagd_providers.md) which interact and "translate" to flagd code for you. If you are using Kubernetes - use the OpenFeature Operator which handles all of this complexity for you. Follow the [OpenFeature Operator hands-on tutorial](https://killercoda.com/open-feature/scenario/openfeature-operator-demo) or read the [OFO docs](https://github.com/open-feature/open-feature-operator/tree/main/docs). + +For example, in Golang, your application code would look like this: + +``` +package main + +import ( + "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg" + "github.com/open-feature/go-sdk/pkg/openfeature" +) + +func main() { + openfeature.SetProvider(flagd.NewProvider( + flagd.WithHost("flagDHost"), + flagd.WithPort(8013), + )) + + // Get an openFeature client + client := openfeature.NewClient("myApp") + + // Get flag values + value, err := client.BooleanValue( + context.Background(), "myFlagValue", false, openfeature.EvaluationContext{}, + ) +} +``` + +- [Get started with flagd](https://github.com/open-feature/flagd) +- Questions? [Join the community](https://docs.openfeature.dev/community/) diff --git a/flagd-demo/index.json b/flagd-demo/index.json new file mode 100644 index 0000000..361c1eb --- /dev/null +++ b/flagd-demo/index.json @@ -0,0 +1,52 @@ +{ + + "title": "OpenFeature flagd Demo", + "description": "New to feature flags? flagd provides a shortcut to a in-house feature flag solution. Follow this tutorial to see how.", + "details": { + "steps": [{ + "title": "Initial Setup and Configuration", + "text": "step1.md" + }, { + "title": "What is flagd?", + "text": "step2.md" + }, { + "title": "Flag Evaluation and GitOps Changes", + "text": "step3.md" + }, { + "title": "Multiple Flag Sources", + "text": "step4.md" + }, { + "title": "Targeting Rules", + "text": "step5.md" + }, { + "title": "Fractional Evaluations", + "text": "step6.md" + }], + "intro": { + "text": "intro.md", + "foreground": "assets/scripts/intro_foreground.sh" + }, + "finish": { + "text": "finish.md" + }, + "assets": { + "host01": [{ + "file": "gitea.app.ini", + "chmod": "+x", + "target": "~" + }, { + "file": "gitea.service", + "chmod": "+x", + "target": "~" + }] + } + }, + "environment": { + "showdashboard": true, + "dashboard": "Dashboard", + "uilayout": "terminal" + }, + "backend": { + "imageid": "ubuntu" + } +} diff --git a/flagd-demo/intro.md b/flagd-demo/intro.md new file mode 100644 index 0000000..1caa8f6 --- /dev/null +++ b/flagd-demo/intro.md @@ -0,0 +1,16 @@ +# OpenFeature and flagd Demonstration + +This demonstration will show a GitOps-based system and approach for storing, changing and evaluating feature flags. + +The demo will use [flagd](https://github.com/open-feature/flagd), an OpenFeature compliant feature flag daemon & API layer but these principles apply to many open source or commercial feature flag vendors. + + +## The System +As you can see, things are being installed for you. When completed, the system will consist of: + +- A local Git repo (using Gitea) +- `flagd`{{}} binary is downloaded +- `git`{{}} and `tea`{{}} binaries are available to interact with Git + +## Be Patient... +Please wait until you see `🎉 Installation Complete 🎉` then click `Start` to begin. \ No newline at end of file diff --git a/flagd-demo/step1.md b/flagd-demo/step1.md new file mode 100644 index 0000000..c62220e --- /dev/null +++ b/flagd-demo/step1.md @@ -0,0 +1,10 @@ +# Gitea +Gitea is a local Git server. You can access it via these links. +- [Login to Gitea]({{TRAFFIC_HOST1_3000}}/user/login) +- [Open openfeature/flags repository]({{TRAFFIC_HOST1_3000}}/openfeature/flags) + +The gitea username and password is: +- Username: `openfeature` +- Password: `openfeature` + +You can use these details to log in to the browser interface and / or if prompted during command line exercises. \ No newline at end of file diff --git a/flagd-demo/step2.md b/flagd-demo/step2.md new file mode 100644 index 0000000..fff1bad --- /dev/null +++ b/flagd-demo/step2.md @@ -0,0 +1,29 @@ +# flagd +In this tutorial, we will use [flagd](https://github.com/open-feature/flagd). So what is flagd? + +![flagd logical architecture](./assets/images/flagd-logical-architecture.png) + +flagd is a open source self-contained feature flag evaluation engine which also provides an API so that you can retrieve flag values. + +flagd doesn't just read a source of feature flags and present the information to you, flagd is **active**. flagd has built in ruleset capabilities and can (if you wish) **evaluate**. This tutorial will explore these capabilities later. + +flagd is a fully featured reference implementation of OpenFeature. Use it to run your feature flag system at scale or as a stepping stone to a paid-for vendor. + +> If you are familiar with OpenTelemetry, flagd is Prometheus or Jaeger. + +flagd is OpenFeature compliant and can read flag configurations from many sources including `files`{{}}, `http(s)`{{}} endpoints and `kubernetes Custom Resource Definitions (CRDs)`{{}}. + +# How does flagd work? +flagd reads one or more feature flag source(s), interprets them and presents an OpenFeature compliant API endpoint that can be queried. + +flagd is compatible with `gRPC`{{}} and `HTTP`{{}} and has native support for metrics using Prometheus. In this demo we will read flags directly from a local Git repo using `HTTPS`{{}}. + +## Start flagd + +Start flagd and ask it to read files from a JSON source hosted online: + +``` +flagd start \ + --port 8013 \ + --uri {{TRAFFIC_HOST1_3000}}/openfeature/flags/raw/branch/main/example_flags.flagd.json +```{{exec}} diff --git a/flagd-demo/step3.md b/flagd-demo/step3.md new file mode 100644 index 0000000..2c84cf2 --- /dev/null +++ b/flagd-demo/step3.md @@ -0,0 +1,40 @@ +We will now simulate what your application would do to retrieve a flag value. + +Open a new tab (click the `+`{{}} icon near the top of the right hand panel) and run this command: + +``` +curl -X POST {{TRAFFIC_HOST1_8013}}/schema.v1.Service/ResolveString \ + -H "Content-Type: application/json" \ + -d '{"flagKey": "headerColor", "context": {} }' +```{{exec}} + +This should return `red` because the `defaultVariant` is set to `red` in Git ([see here]({{TRAFFIC_HOST1_3000}}/openfeature/flags/src/branch/main/example_flags.flagd.json#L95)). + +## Change Flag Color + +Using GitOps, change the `defaultVariant` from `red` to `yellow`: + +Open the Editor and find this file: `~/flags/example_flags.flagd.json`{{}} + +Change the `defaultVariant`{{}} (line `95`{{}}) from `red`{{}} to `yellow`{{}}. + +Change back to Tab 2 and commit these changes to your Git repository by clicking this text: + +``` +cd ~/flags +git add example_flags.flagd.json +git commit -m "update header color" +git push +```{{exec}} + +[Line 95]({{TRAFFIC_HOST1_3000}}/openfeature/flags/src/branch/main/example_flags.flagd.json#L95) should now be `"defaultVariant": "yellow",` + +## Retrieve the Flag Value Again + +This time you should receive `yellow`. + +``` +curl -X POST {{TRAFFIC_HOST1_8013}}/schema.v1.Service/ResolveString \ + -H "Content-Type: application/json" \ + -d '{"flagKey": "headerColor", "context": {} }' +```{{exec}} \ No newline at end of file diff --git a/flagd-demo/step4.md b/flagd-demo/step4.md new file mode 100644 index 0000000..3080625 --- /dev/null +++ b/flagd-demo/step4.md @@ -0,0 +1,59 @@ +## Multiple Flag Sources + +What if another team has their set of flags defined elsewhere? What if you are experimenting and have some flag values saved in a local file? + +In both cases, you need flagd to read **both** sources, evaluate and return the value to you. You shouldn't need to care *where* the flag is stored. + +## Create New Flags + +In tab 2 (leave flagd running), click the following to create a new file containing a single flag called `brandNewFlag` +``` +cat < /tmp/localFlags.json +{ + "flags": { + "brandNewFlag": { + "state": "ENABLED", + "variants": { + "A": "this", + "B": "that" + }, + "defaultVariant": "A" + } + } +} +EOF +```{{exec}} + +## Attempt to retrieve brandNewFlag +In tab 2, attempt to retrieve `brandNewFlag`. It should fail because flagd isn't yet aware of our new flag source (the JSON file): + +``` +curl -X POST {{TRAFFIC_HOST1_8013}}/schema.v1.Service/ResolveString \ + -H "Content-Type: application/json" \ + -d '{"flagKey": "brandNewFlag", "context": {} }' +```{{exec}} + +Expect the following output: `{"code":"not_found","message":"FlagdError:, FLAG_NOT_FOUND"}`{{}} + +## flagd Monitors New Flags + +Switch to Tab 1 and restart flagd, this time providing both flag sources: + +``` +flagd start \ +--port 8013 \ +--uri {{TRAFFIC_HOST1_3000}}/openfeature/flags/raw/branch/main/example_flags.flagd.json \ +--uri file:/tmp/localFlags.json +```{{exec interrupt}} + +## Retrieve Flag + +Change back to tab 2 and again try to retrieve the flag value. + +``` +curl -X POST {{TRAFFIC_HOST1_8013}}/schema.v1.Service/ResolveString \ + -H "Content-Type: application/json" \ + -d '{"flagKey": "brandNewFlag", "context": {} }' +```{{exec}} + +This time you should see: `{"value":"this", "reason":"STATIC", "variant":"A"}`{{}} \ No newline at end of file diff --git a/flagd-demo/step5.md b/flagd-demo/step5.md new file mode 100644 index 0000000..99f58f2 --- /dev/null +++ b/flagd-demo/step5.md @@ -0,0 +1,34 @@ +So far you've seen a very basic feature flag. But often you need more flexibility *within* a given flag rule. + +For this, OpenFeature provides a concept of targeting rules. Targeting rules allow you to be more specific in *who* receives a given flag value. + +For example, look at [targetedFlag]({{TRAFFIC_HOST1_3000}}/openfeature/flags/src/branch/main/example_flags.flagd.json#L126-L149). + +The rules can be read like this: + +- By default, everyone receieves the flag `first` with the value of `AAA` **except**... +- When an `email` key is present containing `@openfeature.dev`, the returned flag is `second` with a value of `BBB`. +- When an `userAgent` key is present containing `Chrome`, the returned flag is `third` with a value of `CCC`. + +Try this out now: + +This command should return the `first` variant with a value of `AAA`. +``` +curl -X POST {{TRAFFIC_HOST1_8013}}/schema.v1.Service/ResolveString \ + -H "Content-Type: application/json" \ + -d '{"flagKey": "targetedFlag", "context": {} }' +```{{exec}} + +This command should return the `second` variant with a value of `BBB`. +``` +curl -X POST {{TRAFFIC_HOST1_8013}}/schema.v1.Service/ResolveString \ + -H "Content-Type: application/json" \ + -d '{"flagKey": "targetedFlag", "context": { "email": "me@openfeature.dev" } }' +```{{exec}} + +This command should return the `third` variant with a value of `CCC`. +``` +curl -X POST {{TRAFFIC_HOST1_8013}}/schema.v1.Service/ResolveString \ + -H "Content-Type: application/json" \ + -d '{"flagKey": "targetedFlag", "context": { "userAgent": "Chrome 1.2.3" } }' +```{{exec}} \ No newline at end of file diff --git a/flagd-demo/step6.md b/flagd-demo/step6.md new file mode 100644 index 0000000..b540b40 --- /dev/null +++ b/flagd-demo/step6.md @@ -0,0 +1,61 @@ +Need *even more* flexibility? Fractional evaluations allow for even more powerful flag targeting. + +Look at the [headerColor]({{TRAFFIC_HOST1_3000}}/openfeature/flags/src/branch/main/example_flags.flagd.json#L88-L125) flag. + +The available variants are `red`, `blue`, `green` and `yellow`. The `defaultVariant` should now be `yellow` (you changed it from `red` in the previous step). + +So everyone receives `yellow`, right? No. There is a [targeting rule]({{TRAFFIC_HOST1_3000}}/openfeature/flags/src/branch/main/example_flags.flagd.json#L97-L125) which can be read like this: + +Everyone receives the `defaultVariant`{{}} of `yellow`{{}} **except**... + +If an `email`{{}} field is present during evaluation context and the value contains `@faas.com`{{}}: +- 25% of the email addresses will get `red` +- 25% of the email addresses will get `blue` +- 25% of the email addresses will get `green` +- 25% of the email addresses will get `yellow` + +If an `email`{{}} field is NOT present during evaluation OR it is present but the email does not contain `@faas.com`{{}}: +- 100% of these users get `yellow` (ie. fallback to `defaultVariant`) + +### Fractional Evaluations are Sticky +The first time an `email` is evaluated, it is psuedorandomly assigned to a bucket of one of the available `variants`{{}}(in this case, either `red`, `blue`, `green` or `yellow`). + +Any further evaluations for the same `email` will always give the same value. + +In other words, if for `user@faas.com` you received `red`, `user@faas.com` will always receive `red`. + +`user2@faas.com` can receive a different colour. + +However, whatever colour `user2@faas.com` receives initially (say `green`), they will continue to receive that colour. + +![fractional evaluation](./assets/images/fractional-evaluation.png) + +This behaviour makes sense. If you were using this to control the colour of your website, you wouldn't want to the webpage colour changing each time you refreshed the page! + +## Experiment with Fractional Evaluations + +Send this request a few times and it should always return the same colour (initially psuedo-randomly generated): + +``` +curl -X POST {{TRAFFIC_HOST1_8013}}/schema.v1.Service/ResolveString \ + -H "Content-Type: application/json" \ + -d '{"flagKey": "headerColor", "context": { "email": "user1@faas.com" } }' +```{{exec}} + +Now try with a different user. This user will probably get a different colour to `user1` but whatever they do get, they should stay with that colour: + +``` +curl -X POST {{TRAFFIC_HOST1_8013}}/schema.v1.Service/ResolveString \ + -H "Content-Type: application/json" \ + -d '{"flagKey": "headerColor", "context": { "email": "user2@faas.com" } }' +```{{exec}} + +Feel free to change the email address to experiment with the behaviour. + +Finally, prove that the `defaultVariant` is working (request the flag value without providing an `email`{{}}) and returns `yellow`{{}}: + +``` +curl -X POST {{TRAFFIC_HOST1_8013}}/schema.v1.Service/ResolveString \ + -H "Content-Type: application/json" \ + -d '{"flagKey": "headerColor", "context": { } }' +```{{exec}} \ No newline at end of file