From 9c850fc004298d3f5a2285b49af690b18613af93 Mon Sep 17 00:00:00 2001 From: "yusheng.wang" Date: Sat, 10 Oct 2026 11:16:17 +0800 Subject: [PATCH] Add Amazon Managed Prometheus metrics provider Add an `amp` metric template provider that queries Amazon Managed Service for Prometheus with AWS SigV4, addressing #962. Signed-off-by: yusheng.wang --- CHANGELOG.md | 6 ++ artifacts/flagger/crd.yaml | 1 + charts/flagger/crds/crd.yaml | 1 + docs/gitbook/usage/metrics.md | 58 ++++++++++++++ kustomize/base/flagger/crd.yaml | 1 + pkg/metrics/providers/amp.go | 88 +++++++++++++++++++++ pkg/metrics/providers/amp_test.go | 122 ++++++++++++++++++++++++++++++ pkg/metrics/providers/factory.go | 2 + 8 files changed, 279 insertions(+) create mode 100644 pkg/metrics/providers/amp.go create mode 100644 pkg/metrics/providers/amp_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index fbbb9aa0e..a89ca89cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml index ec042850c..f3064979d 100644 --- a/artifacts/flagger/crd.yaml +++ b/artifacts/flagger/crd.yaml @@ -1389,6 +1389,7 @@ spec: - keptn - splunk - azuremonitor + - amp address: description: API address of this provider type: string diff --git a/charts/flagger/crds/crd.yaml b/charts/flagger/crds/crd.yaml index ec042850c..f3064979d 100644 --- a/charts/flagger/crds/crd.yaml +++ b/charts/flagger/crds/crd.yaml @@ -1389,6 +1389,7 @@ spec: - keptn - splunk - azuremonitor + - amp address: description: API address of this provider type: string diff --git a/docs/gitbook/usage/metrics.md b/docs/gitbook/usage/metrics.md index 50c580288..27523de8a 100644 --- a/docs/gitbook/usage/metrics.md +++ b/docs/gitbook/usage/metrics.md @@ -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. diff --git a/kustomize/base/flagger/crd.yaml b/kustomize/base/flagger/crd.yaml index ec042850c..f3064979d 100644 --- a/kustomize/base/flagger/crd.yaml +++ b/kustomize/base/flagger/crd.yaml @@ -1389,6 +1389,7 @@ spec: - keptn - splunk - azuremonitor + - amp address: description: API address of this provider type: string diff --git a/pkg/metrics/providers/amp.go b/pkg/metrics/providers/amp.go new file mode 100644 index 000000000..2067bcdca --- /dev/null +++ b/pkg/metrics/providers/amp.go @@ -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: &SigningRoundTripper{ + 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) +} diff --git a/pkg/metrics/providers/amp_test.go b/pkg/metrics/providers/amp_test.go new file mode 100644 index 000000000..9f4602af9 --- /dev/null +++ b/pkg/metrics/providers/amp_test.go @@ -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)) + }) +} diff --git a/pkg/metrics/providers/factory.go b/pkg/metrics/providers/factory.go index 7a8fb0518..f563e8670 100644 --- a/pkg/metrics/providers/factory.go +++ b/pkg/metrics/providers/factory.go @@ -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)