Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project are documented in this file.

## Unreleased

#### Improvements
- Add Amazon Managed Prometheus metrics provider
[#962](https://github.com/fluxcd/flagger/issues/962)

## 1.45.0

**Release date:** 2026-09-01
Expand Down
1 change: 1 addition & 0 deletions artifacts/flagger/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,7 @@ spec:
- keptn
- splunk
- azuremonitor
- amp
address:
description: API address of this provider
type: string
Expand Down
1 change: 1 addition & 0 deletions charts/flagger/crds/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,7 @@ spec:
- keptn
- splunk
- azuremonitor
- amp
address:
description: API address of this provider
type: string
Expand Down
58 changes: 58 additions & 0 deletions docs/gitbook/usage/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,64 @@ Reference the template in the canary analysis:

**Note** that Flagger need AWS IAM permission to perform `cloudwatch:GetMetricData` to use this provider.

## Amazon Managed Prometheus

You can create custom metric checks targeting [Amazon Managed Service for Prometheus](https://docs.aws.amazon.com/prometheus/latest/userguide/what-is-Amazon-Managed-Service-Prometheus.html) by setting the provider type to `amp`. Queries are written in PromQL and sent to the workspace Prometheus-compatible query API.

Set `address` to the AMP workspace query URL and `region` to the workspace AWS region. Flagger signs requests with AWS Signature Version 4 using the default credential chain (environment variables, IRSA, or shared config). Grant the identity that Flagger uses the `aps:QueryMetrics` permission.

AMP template example:

```yaml
apiVersion: flagger.app/v1beta1
kind: MetricTemplate
metadata:
name: not-found-percentage
namespace: istio-system
spec:
provider:
type: amp
address: https://aps-workspaces.us-west-2.amazonaws.com/workspaces/ws-xxx-yyy
region: us-west-2
query: |
100 - sum(
rate(
istio_requests_total{
reporter="destination",
destination_workload_namespace="{{ namespace }}",
destination_workload="{{ target }}",
response_code!="404"
}[{{ interval }}]
)
)
/
sum(
rate(
istio_requests_total{
reporter="destination",
destination_workload_namespace="{{ namespace }}",
destination_workload="{{ target }}"
}[{{ interval }}]
)
) * 100
```

Reference the template in the canary analysis:

```yaml
analysis:
metrics:
- name: "404s percentage"
templateRef:
name: not-found-percentage
namespace: istio-system
thresholdRange:
max: 5
interval: 1m
```

**Note** that Flagger needs AWS IAM permission to perform `aps:QueryMetrics` to use this provider.

## New Relic

You can create custom metric checks using the New Relic provider.
Expand Down
1 change: 1 addition & 0 deletions kustomize/base/flagger/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,7 @@ spec:
- keptn
- splunk
- azuremonitor
- amp
address:
description: API address of this provider
type: string
Expand Down
88 changes: 88 additions & 0 deletions pkg/metrics/providers/amp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2026 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package providers

import (
"fmt"
"net/http"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/aws/signer/v4"

flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
)

const ampService = "aps"

// AmpProvider executes promQL queries against Amazon Managed Service for Prometheus
type AmpProvider struct {
*PrometheusProvider
}

type ampSigningRoundTripper struct {
next http.RoundTripper
signer *v4.Signer
region string
}

// NewAmpProvider takes a provider spec, validates the address and region, and
// returns a Prometheus client that signs queries with AWS SigV4
func NewAmpProvider(provider flaggerv1.MetricTemplateProvider) (*AmpProvider, error) {
if provider.Region == "" {
return nil, fmt.Errorf("region not specified")
}

// AMP uses the default AWS credential chain rather than Prometheus secrets
promProvider := provider
promProvider.SecretRef = nil

prom, err := NewPrometheusProvider(promProvider, nil)
if err != nil {
return nil, err
}

sess, err := session.NewSession(aws.NewConfig().WithRegion(provider.Region))
if err != nil {
return nil, fmt.Errorf("error creating aws session: %s", err.Error())
}

next := http.DefaultTransport
if prom.client != nil && prom.client.Transport != nil {
next = prom.client.Transport
}

prom.client = &http.Client{
Transport: &ampSigningRoundTripper{
next: next,
signer: v4.NewSigner(sess.Config.Credentials),
region: provider.Region,
},
}

return &AmpProvider{PrometheusProvider: prom}, nil
}

func (rt *ampSigningRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
signed := req.Clone(req.Context())
_, err := rt.signer.Sign(signed, nil, ampService, rt.region, time.Now())
if err != nil {
return nil, fmt.Errorf("failed to sign AWS request: %w", err)
}
return rt.next.RoundTrip(signed)
}
122 changes: 122 additions & 0 deletions pkg/metrics/providers/amp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
Copyright 2026 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package providers

import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
)

func ampAWSEnv(t *testing.T) {
t.Helper()
t.Setenv("AWS_ACCESS_KEY_ID", "AKIDEXAMPLE")
t.Setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")
t.Setenv("AWS_SESSION_TOKEN", "")
t.Setenv("AWS_EC2_METADATA_DISABLED", "true")
t.Setenv("AWS_CONFIG_FILE", "/dev/null")
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "/dev/null")
}

func TestNewAmpProvider(t *testing.T) {
t.Run("ok", func(t *testing.T) {
ampAWSEnv(t)
p, err := NewAmpProvider(flaggerv1.MetricTemplateProvider{
Type: "amp",
Address: "https://aps-workspaces.us-west-2.amazonaws.com/workspaces/ws-example",
Region: "us-west-2",
})
require.NoError(t, err)
assert.Equal(t, "https://aps-workspaces.us-west-2.amazonaws.com/workspaces/ws-example", p.url.String())
})

t.Run("missing region", func(t *testing.T) {
_, err := NewAmpProvider(flaggerv1.MetricTemplateProvider{
Type: "amp",
Address: "https://aps-workspaces.us-west-2.amazonaws.com/workspaces/ws-example",
})
require.Error(t, err)
assert.EqualError(t, err, "region not specified")
})

t.Run("invalid address", func(t *testing.T) {
_, err := NewAmpProvider(flaggerv1.MetricTemplateProvider{
Type: "amp",
Region: "us-west-2",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "address")
})
}

func TestAmpProvider_RunQuery(t *testing.T) {
ampAWSEnv(t)
query := `sum(envoy_cluster_upstream_rq)`

t.Run("ok", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
promql := r.URL.Query()["query"][0]
assert.Equal(t, query, promql)

auth := r.Header.Get("Authorization")
assert.True(t, strings.HasPrefix(auth, "AWS4-HMAC-SHA256"), "Authorization header %q", auth)

json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1545905245.458,"100"]}]}}`
w.Write([]byte(json))
}))
defer ts.Close()

p, err := NewAmpProvider(flaggerv1.MetricTemplateProvider{
Type: "amp",
Address: ts.URL,
Region: "us-west-2",
})
require.NoError(t, err)

val, err := p.RunQuery(query)
require.NoError(t, err)
assert.Equal(t, float64(100), val)
})

t.Run("no values", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
assert.True(t, strings.HasPrefix(auth, "AWS4-HMAC-SHA256"), "Authorization header %q", auth)

json := `{"status":"success","data":{"resultType":"vector","result":[]}}`
w.Write([]byte(json))
}))
defer ts.Close()

p, err := NewAmpProvider(flaggerv1.MetricTemplateProvider{
Type: "amp",
Address: ts.URL,
Region: "us-west-2",
})
require.NoError(t, err)

_, err = p.RunQuery(query)
require.True(t, errors.Is(err, ErrNoValuesFound))
})
}
2 changes: 2 additions & 0 deletions pkg/metrics/providers/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ func (factory Factory) Provider(metricInterval string, provider flaggerv1.Metric
return NewSplunkProvider(metricInterval, provider, credentials)
case "azuremonitor":
return NewAzureMonitorProvider(provider, credentials)
case "amp":
return NewAmpProvider(provider)
default:
factory.logger.Warnf("unknown metrics provider '%s', using prometheus", provider.Type)
return NewPrometheusProvider(provider, credentials)
Expand Down