Close

Using Launch Darkly feature flags with Bitbucket pipelines

Warren Marusiak headshot
Warren Marusiak

Senior Technical Evangelist

Deploying new code to a production environment is risky. Bugs can make it into production even after the code is unit tested, integration tested, and system tested in test and staging environments. Traditionally, developers have two choices once a bug makes it to production, and users are impacted. They can roll back the buggy code, or roll a fix forward; both of these solutions take time. Now, developers can turn a feature on or off in an environment at the click of a button by wrapping the related code changes in a feature flag. The impact of buggy code on users can be mitigated immediately, and a fix can be developed and rolled forward safely. This article demonstrates this using Bitbucket pipelines, and Launch Darkly feature flags in the ImageLabeller demo application.

An ImageLabeller feature flag demo

ImageLabeller is a small application that uses machine learning to label images. ImageLabeller is deployed to five environments: Test, Staging, Production-us-west-2, Production-us-east-1, and Production-ca-central-1. This article demonstrates how to use feature flags to manage changes to the SubmitImage component of ImageLabeller. SubmitImage is an AWS Lambda written in Go. You will use Launch Darkly to manage feature flags, Bitbucket for source control, and Bitbucket pipelines for CI/CD functionality.

How to use Launch Darkly feature flags with Bitbucket pipelines

Have your local Launch Darkly administrator create a project and environment. In the screenshot below there is a project, PMMImageLabellerDemo, with five environments. Test and Staging are preproduction environments. Make a note of the SDK key for each environment. Later, the SDK keys will be added as repository variables in Bitbucket.

In this example, Bitbucket pipelines deploy to these environments when code is committed to a feature branch. Production-us-west-2, Production-us-east-1, and Production-ca-central-1 are production environments that correspond to AWS environments. Bitbucket pipelines deploy to these environments when code is merged into mainline, from a feature branch, via pull request.

Screenshot of Bitbucket Pipelines

In Launch Darkly, create a feature flag for the project. Select the Test environment, and adjust the feature flag settings. In the screenshot below the feature flag is set to return true by default in the Test region. If a specific user, AtlassianTestUser@atlassian.com, is making the request the feature flag will return false. In this way, the specific named user, like test users in a system test suit, can get the code to execute one way while normal users in the same environment get the code to execute differently.

This behavior can be adjusted on a per-environment basis. Feature flags allow a developer to deploy new code to all regions, while only allowing the code to execute in particular environments. In the case of this demo, the flag is set to return false in Staging, and all three production environments. The new code will only execute in the test environment.

User targeting screenshot

Grab the SDK keys for each environment from Launch Darkly. Then, go to Bitbucket, and add repository variables to each repository that will use this flag. The screenshot below shows that five repository variables were added. ld_test_env contains the Launch Darkly SDK key for the Test environment. ld_staging_env contains the Launch Darkly SDK key for the Staging environment. These repository variables are later references in the bitbucket-pipelines.yml file for the repository.

SDK Keys

The SDK key values can be references in the repository’s bitbucket-pipeline.yml file after the SDK keys are added as repository variables. STACK_PARAMETERS is added to the deployment step for production-ca-central-1 in the snippet below. STACK_PARAMETERS sends the value of the corresponding SDK key to the AWS CloudFormation template.yml file as a parameter.

- pipe: atlassian/aws-sam-deploy:1.2.0
  variables:
    AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
    AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
    AWS_DEFAULT_REGION: 'ca-central-1'
    STACK_NAME: 'OpenDevOpsSubmitImage'
    CAPABILITIES: [ 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND' ]
    TEMPLATE: 'https://s3.amazonaws.com/open-devops-code-ca-central-1-${AWS_ACCOUNT_ID}/submit-image-packaged.yml'
    WAIT: 'true'
    DEBUG: 'true'
    S3_BUCKET: 'open-devops-code-ca-central-1-${AWS_ACCOUNT_ID}'
    SAM_TEMPLATE: 'build/template.yaml'
    STACK_PARAMETERS: '[{
      "ParameterKey": "LaunchDarklySDKKey",
      "ParameterValue": "${ld_prod_cac1_env}"
    }]'

Add a Parameters section with LaunchDarklySDKKey of type String to the Parameters section of the repository’s template.yml file. This parameter receives the value of the LaunchDarklySDKKey STACK_PARAMETER set in the bitbucket-pipelines.yml file.

Parameters:
  LaunchDarklySDKKey:
    Type: String

Also update the AWS Lambda resource for the SubmitImage function in the template.yml file. Add LaunchDarklySDKKey as an environment variable.

Resources:
  SubmitImageFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: submitImage/
      Handler: submit-image
      Runtime: go1.x
      Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html
      Policies:
        - AmazonDynamoDBFullAccess
        - AmazonS3FullAccess
      Events:
        CatchAll:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /submit-image
            Method: GET
      Environment:
        Variables:
          LaunchDarklySDKKey:
            Ref: LaunchDarklySDKKey

The LaunchDarklySDKKey environment variable will be visible in the AWS Lambda console after Bitbucket pipelines deploy to the environment. This value of this key is unique to the environment. For example, the LaunchDarklySDKKey environment variable in Test will be different than in Production-us-west-2.

Environment Configurations screenshot

SubmitImage is an AWS Lambda written in Go. To use Launch Darkly with Go import the following dependencies.

"gopkg.in/launchdarkly/go-sdk-common.v2/lduser"
ld "gopkg.in/launchdarkly/go-server-sdk.v5"

Add a function to retrieve the feature flag value from Launch Darkly at runtime.

func getLaunchDarklyFlags(username string) (bool, error) {
  client, _ := ld.MakeClient(os.Getenv("LaunchDarklySDKKey"), 5 * time.Second)
  flagKey := "SubmitImageDemoFeature"

  userUuid, uuidErr := uuid.NewRandom()
  if uuidErr != nil {
    return false, uuidErr
  }

  var user lduser.User
  if(username == "") {
    user = lduser.NewAnonymousUser(userUuid.String())
  } else {
    user = lduser.NewUser(username)
  }

  showFeature, _ := client.BoolVariation(flagKey, user, false)

  if showFeature {
    return true, nil
  } else {
    return false, nil
  }
}

Call the function with an empty string to grab the default flag value, or with a user email to grab the targeted value. The setup shown above should pull true for the anonymous user, and false for the AtlasianTestUser@atlassian.com user.

flagVal, flagErr  := getLaunchDarklyFlags("")
  if flagErr != nil {
    return "", flagErr
  }
  fmt.Println("DEMO flagVal for anonymous user: ", flagVal)

  flagVal, flagErr  = getLaunchDarklyFlags("AtlassianTestUser@atlassian.com")
  if flagErr != nil {
    return "", flagErr
  }
  fmt.Println("DEMO flagVal for AtlassianTestUser@atlassian.com: ", flagVal)

Go to AWS CloudWatch logs after running the code to verify that the correct flag values get pulled.

Log events screenshot
Copying target rules

Go to Admin settings, then API keys to get a list of the API keys for each environment. These api keys are sent back to split during API calls in code to get the correct version of a split. This guide uses the Server-side keys for each environment.

Admin settings

Go to your Bitbucket repository, then Repository settings, then Repository variables, and add variables for each API key.

Respository variables in repository settings

Edit the bitbucket-pipelines.yml file, and add STACK_PARAMETERS to the AWS SAM deployment step. This is done on a per-environment basis. The YAML snippet below shows the deployment step for the TEST region which is in AWS US-WEST-1. Hence, the step references the split_test_env repository variable setup above. Use the appropriate repository variable for each environment.

- pipe: atlassian/aws-sam-deploy:1.2.0
  variables:
    AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
    AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
    AWS_DEFAULT_REGION: 'us-west-1'
    STACK_NAME: 'OpenDevOpsSubmitImage'
    CAPABILITIES: [ 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND' ]
    TEMPLATE: 'https://s3.amazonaws.com/open-devops-code-us-west-1-${AWS_ACCOUNT_ID}/submit-image-packaged.yml'
    WAIT: 'true'
    DEBUG: 'true'
    S3_BUCKET: 'open-devops-code-us-west-1-${AWS_ACCOUNT_ID}'
    SAM_TEMPLATE: 'build/template.yaml'
    STACK_PARAMETERS: '[{
      "ParameterKey": "SplitIOSDKKey",
      "ParameterValue": "${split_test_env}"
    }]'

Edit the AWS CloudFormation template.yml file and add a Parameters section referencing the Split SDK key.

Parameters:
  SplitIOSDKKey:
    Type: String

In the template.yml file add an Environment section to each AWS Lambda resource that needs to access Split. This guide demonstrates

Environment:
  Variables:
    SplitIOSDKKey:
      Ref: SplitIOSDKKey

Import the following dependencies into the Go file that will use the Split SDK.

"github.com/splitio/go-client/v6/splitio/client"
"github.com/splitio/go-client/v6/splitio/conf"

This function creates a client, and retrieves the feature flag value for the “SubmitImageDemoSplit” created in the Split UI. It takes a single parameter, username.

func getSplitIOFlag(username string) (string, error) {
  splitIOSDKKey := os.Getenv("SplitIOSDKKey")

  cfg := conf.Default()
  factory, err := client.NewSplitFactory(splitIOSDKKey, cfg)
  if err != nil {
    fmt.Printf("SDK init error: %s\n", err)
    return "", err
  }

  splitClient := factory.Client()
  err = splitClient.BlockUntilReady(10)
  if err != nil {
    fmt.Printf("SDK timeout: %s\n", err)
    return "", err
  }

  treatment := splitClient.Treatment(username, "SubmitImageDemoSplit", nil)
  fmt.Printf("SPLIT_DEMO treatment is %s, username is %s\n", treatment, username)

  return treatment, nil
}

Call the function with an email address. In this case, someRandomUser@atlassian.com will pull the default value of the feature flag since it is not a member of an allow list associated with the feature flag. AtlassianTestUser@atlassian.com will pull the value of the feature flag associated with the allowlist to which it is a member.

Look at the output in AWS CloudWatch logs after the code has been executed. Observe that the feature flag returns off when it is accessed by someRandomUser@atlassian.com, and the feature flag returns on when accessed by AtlassianTestUser@atlassian.com.

Log events

In conclusion…

Launch Darkly feature flags integrate easily into an application deployed via Bitbucket pipelines. Feature flags enable developers to control the execution of deployed code. This can make responding to buggy deployments faster, reducing impacts on users. Take the time to spin up an instance of Bitbucket, and Launch Darkly, and test the capabilities for your team.

Warren Marusiak
Warren Marusiak

Warren is a Canadian developer from Vancouver, BC with over 10 years of experience. He came to Atlassian from AWS in January of 2021.


Share this article

Recommended reading

Bookmark these resources to learn about types of DevOps teams, or for ongoing updates about DevOps at Atlassian.

Devops illustration

DevOps community

Devops illustration

DevOps learning path

Map illustration

Get started for free

Sign up for our DevOps newsletter

Thank you for signing up