From 42da1d4c2147021aeb894acc2a84d7a1653d6e1d Mon Sep 17 00:00:00 2001 From: rafajpet Date: Thu, 21 Mar 2024 12:15:40 +0100 Subject: [PATCH 1/6] task file init --- .gitignore | 1 + .tool-versions | 1 + Taskfile.yml | 16 ++++++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 Taskfile.yml diff --git a/.gitignore b/.gitignore index 2f222d3..a647d6f 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ go.work .idea coverage.out +bin/* diff --git a/.tool-versions b/.tool-versions index 556137a..2ca4293 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,4 @@ helm 3.14.2 awscli 2.7.14 pre-commit 2.20.0 +task 3.35.1 diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..150e702 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,16 @@ +version: '3' + +vars: + APP_NAME: exporter + +tasks: + lint: + cmds: + - golangci-lint run + build: + cmds: + - go build -o bin/{{ .APP_NAME }} cmd/{{ .APP_NAME}}/main.go + run-example: + deps: [build] + cmds: + - ./bin/{{ .APP_NAME }} --config ./config/example.yaml From 7beeb1fd684fa6f21475568ae3f02259c168952d Mon Sep 17 00:00:00 2001 From: rafajpet Date: Fri, 22 Mar 2024 08:04:00 +0100 Subject: [PATCH 2/6] quota client default list --- Dockerfile | 2 +- config/example.yaml | 11 ++++++++++ internal/scrape/quotas/collector.go | 4 ++-- internal/scrape/quotas/config.go | 1 + pkg/quota/client.go | 26 +++++++++++++++++----- pkg/quota/options.go | 34 ++++++++++++++++------------- 6 files changed, 54 insertions(+), 24 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0f6d327..c5b22fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ RUN cd cmd/exporter && \ FROM alpine:${ALPINE_VERSION} -RUN apk update && apk add jq +RUN apk update && apk add jq bash COPY --from=builder_aws_cli /usr/local/lib/aws-cli/ /usr/local/lib/aws-cli/ RUN ln -s /usr/local/lib/aws-cli/aws /usr/local/bin/aws diff --git a/config/example.yaml b/config/example.yaml index 6ec5e14..e8b6895 100644 --- a/config/example.yaml +++ b/config/example.yaml @@ -1,7 +1,18 @@ quotas: + # Quota for EC2/ EIPs - serviceCode: "ec2" quotaCode: "L-0263D0A3" + # Quota for number of records in route53 zone + - serviceCode: "route53" + quotaCode: "L-E209CC9F" + region: "us-east-1" + default: true metrics: + # Usage of ec2 elastic ips in account + - name: "ec2_elastic_ips_usage" + help: "Usage of ec2 elastic ips" + script: "echo \"quota_code=L-0263D0A3,$(aws ec2 describe-addresses --query \'length(Addresses[])\')\"" + # Number of records in route53 zone - name: "route53_hosted_zone_records" help: "Number of resource sets in hosted zone" script: "aws route53 list-hosted-zones | jq -r \'.HostedZones[] | \"id=\\(.Id),name=\\(.Name),\\(.ResourceRecordSetCount)\"\'" diff --git a/internal/scrape/quotas/collector.go b/internal/scrape/quotas/collector.go index eba575d..d2c5b1d 100644 --- a/internal/scrape/quotas/collector.go +++ b/internal/scrape/quotas/collector.go @@ -56,7 +56,7 @@ func (c *Collector) Register(ctx context.Context, r *prometheus.Registry) error for _, cf := range c.cfg { qc := cf g.Go(func() error { - res, err := c.qcl.GetQuota(ctx, qc.ServiceCode, qc.QuotaCode, quota.WithRegion(qc.Region)) + res, err := c.qcl.GetQuota(ctx, qc.ServiceCode, qc.QuotaCode, quota.WithDefault(qc.Default), quota.WithRegion(qc.Region)) if err != nil { return err } @@ -97,7 +97,7 @@ type task struct { func (t task) run(ctx context.Context, c Quota) func() error { return func() error { - res, err := c.GetQuota(ctx, t.cfg.ServiceCode, t.cfg.QuotaCode, quota.WithRegion(t.cfg.Region)) + res, err := c.GetQuota(ctx, t.cfg.ServiceCode, t.cfg.QuotaCode, quota.WithDefault(t.cfg.Default), quota.WithRegion(t.cfg.Region)) if err != nil { return err } diff --git a/internal/scrape/quotas/config.go b/internal/scrape/quotas/config.go index 7a3a141..1891a18 100644 --- a/internal/scrape/quotas/config.go +++ b/internal/scrape/quotas/config.go @@ -8,6 +8,7 @@ type Config struct { ServiceCode string `json:"serviceCode,omitempty" yaml:"serviceCode,omitempty"` QuotaCode string `json:"quotaCode,omitempty" yaml:"quotaCode,omitempty"` Region string `json:"region,omitempty" yaml:"region,omitempty"` + Default bool `json:"default,omitempty" yaml:"default,omitempty"` } func (c Config) Validate() error { diff --git a/pkg/quota/client.go b/pkg/quota/client.go index 65e9287..d342296 100644 --- a/pkg/quota/client.go +++ b/pkg/quota/client.go @@ -2,7 +2,6 @@ package quota import ( "context" - "fmt" "github.com/aws/aws-sdk-go-v2/aws" awsConfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/servicequotas" @@ -30,24 +29,39 @@ type Client struct { } func (c *Client) GetQuota(ctx context.Context, serviceCode string, quotaCode string, options ...Option) (*types.ServiceQuota, error) { + cfg := config{} + for _, o := range options { + o(&cfg) + } + if cfg.def { + res, err := c.squ.GetAWSDefaultServiceQuota(ctx, &servicequotas.GetAWSDefaultServiceQuotaInput{ + QuotaCode: aws.String(quotaCode), + ServiceCode: aws.String(serviceCode), + }, cfg.options()) + if err != nil { + return nil, err + } + return res.Quota, err + } res, err := c.squ.GetServiceQuota(ctx, &servicequotas.GetServiceQuotaInput{ QuotaCode: aws.String(quotaCode), ServiceCode: aws.String(serviceCode), - }, buildOptions(options...)) + }, cfg.options()) if err != nil { - return nil, fmt.Errorf("unable to get quota with service: %s, code: %s, %w", serviceCode, quotaCode, err) + return nil, err } - return res.Quota, nil + return res.Quota, err } -func (c *Client) GetQuotas(ctx context.Context, serviceCode string, options ...Option) ([]types.ServiceQuota, error) { +func (c *Client) GetQuotas(ctx context.Context, serviceCode string) ([]types.ServiceQuota, error) { + qs := make([]types.ServiceQuota, 0) var token *string for { res, err := c.squ.ListServiceQuotas(ctx, &servicequotas.ListServiceQuotasInput{ ServiceCode: aws.String(serviceCode), NextToken: token, - }, buildOptions(options...)) + }) if err != nil { return nil, err } diff --git a/pkg/quota/options.go b/pkg/quota/options.go index 34844c9..caca4a0 100644 --- a/pkg/quota/options.go +++ b/pkg/quota/options.go @@ -2,27 +2,31 @@ package quota import "github.com/aws/aws-sdk-go-v2/service/servicequotas" -type Options struct { - *servicequotas.Options +type config struct { + def bool + region string } -type Option func(c *Options) - -func WithRegion(region string) Option { - return func(c *Options) { - if region != "" { - c.Region = region +func (c *config) options() func(*servicequotas.Options) { + return func(ops *servicequotas.Options) { + if c.region != "" { + ops.Region = c.region } } } -func buildOptions(option ...Option) func(*servicequotas.Options) { - return func(awsSq *servicequotas.Options) { - op := Options{ - awsSq, - } - for _, o := range option { - o(&op) +type Option func(c *config) + +func WithDefault(def bool) Option { + return func(c *config) { + c.def = def + } +} + +func WithRegion(region string) Option { + return func(c *config) { + if region != "" { + c.region = region } } } From ebade3d2a37e0d0cb5107fc0036f197a6e8ff180 Mon Sep 17 00:00:00 2001 From: rafajpet Date: Fri, 22 Mar 2024 08:05:06 +0100 Subject: [PATCH 3/6] clean helm test --- .github/workflows/helm-lint-test.yml | 60 ---------------------------- 1 file changed, 60 deletions(-) delete mode 100644 .github/workflows/helm-lint-test.yml diff --git a/.github/workflows/helm-lint-test.yml b/.github/workflows/helm-lint-test.yml deleted file mode 100644 index eaab8b5..0000000 --- a/.github/workflows/helm-lint-test.yml +++ /dev/null @@ -1,60 +0,0 @@ -#name: Lint and Test Charts -# -#on: pull_request -# -#env: -# HELM_VERSION: 3.14.0 -# -#jobs: -# lint-test: -# runs-on: ubuntu-22.04 -# steps: -# - name: Checkout -# uses: actions/checkout@v4 -# with: -# fetch-depth: 0 -# -# - name: Set up Helm -# uses: azure/setup-helm@v4 -# with: -# version: ${{ env.HELM_VERSION }} -# -# - uses: actions/setup-python@v5 -# with: -# python-version: '3.11' -# check-latest: true -# -# - name: Set up chart-testing -# uses: helm/chart-testing-action@v2.6.1 -# -# - name: Run chart-testing (list-changed) -# id: list-changed -# run: | -# changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }}) -# if [[ -n "$changed" ]]; then -# echo "changed=true" >> "$GITHUB_OUTPUT" -# fi -# -# - name: Run chart-testing (lint) -# if: steps.list-changed.outputs.changed == 'true' -# run: ct lint --target-branch ${{ github.event.repository.default_branch }} --validate-maintainers=false -# -# - name: Create kind cluster -# if: steps.list-changed.outputs.changed == 'true' -# uses: helm/kind-action@v1.9.0 -# -# - name: Run chart-testing (install) -# if: steps.list-changed.outputs.changed == 'true' -# run: ct install --target-branch ${{ github.event.repository.default_branch }} -# -# linter-artifacthub: -# runs-on: ubuntu-22.04 -# container: -# image: artifacthub/ah -# options: --user root -# steps: -# - name: Checkout code -# uses: actions/checkout@v4 -# - name: Run ah lint -# working-directory: ./charts/ -# run: ah lint From e188ba5cea3f00eed068e410f9330d507f93ca68 Mon Sep 17 00:00:00 2001 From: rafajpet Date: Fri, 22 Mar 2024 09:36:10 +0100 Subject: [PATCH 4/6] improve docs --- README.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++- Taskfile.yml | 14 +++++- 2 files changed, 148 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b46a96..80a3fe4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,141 @@ # aws-service-quotas-exporter +## Description + AWS service quotas exporter exposes actual quotas for your AWS accounts and allow you to scrape actual usage of AWS resources. Base on those two types of data, you can easily build alert rules to prevent case when you are not able to provision another AWS resources due to reach the limit of AWS quota -## AWS Co +## Usage & Configuration + +This exporter allows you to scrape metrics via two following approach + +### Service quota API + +[Viewing service quotas](https://docs.aws.amazon.com/servicequotas/latest/userguide/gs-request-quota.html) + +Usually if you are interested to see current value of specific quota metric, you have to use following AWS CLI command: + +```bash +aws service-quotas get-service-quota --service-code ec2 --quota-code L-0263D0A3 +# or +aws service-quotas get-aws-default-service-quota --service-code route53 --quota-code L-E209CC9F --region us-east-1 +``` + +This can be configured via `quotas` section of configuration file: + +Example: +```yaml +quotas: + # Quota for EC2/ EIPs + - serviceCode: "ec2" + quotaCode: "L-0263D0A3" + # Quota for number of records in route53 zone + - serviceCode: "route53" + quotaCode: "L-E209CC9F" + region: "us-east-1" + default: true +``` + +### Usage of AwS resources exports via bash scraping + +The pity of AWS service-quota API is that, for most of the resources there is no easy way how to show actual usage of concrete +AWS resources. You have to do it via aws cli/sdk. This exporter try to solve this issue via simple bash script scheduler which +is capable to run specific set of bash scripts and extract actual usage of AWS resources. + +Example: + +You would like to see usage of AWS EC2 EIPs in your account, cli example: +```bash +aws ec2 describe-addresses --query 'length(Addresses[])' +``` +This can be transformed to metric via config: +```yaml +metrics: + # Name of exporter metric + - name: "ec2_elastic_ips_usage" + # Help message for exporter metric + help: "Usage of ec2 elastic ips" + # Script used for scraping. It can be inline script or file path to script + script: "echo \"quota_code=L-0263D0A3,$(aws ec2 describe-addresses --query \'length(Addresses[])\')\"" +``` + +Example above will produce following prometheus metric export: + +``` +# HELP quota_exporter_ec2_elastic_ips_usage Usage of ec2 elastic ips +# TYPE quota_exporter_ec2_elastic_ips_usage gauge +quota_exporter_ec2_elastic_ips_usage{quota_code="L-0263D0A3"} 4 +``` + +There is requirement regarding format output. If you want to export data. For every unique +combination of labels, it has to be one line stdout of you bash script in following csv format + +`lable_name_a=label_value,label_name_b=label_value_b,label_name_c=label_value_c,value_for_metric` + +## Development + +This application requires Go1.22, or later. For common tasks, you can use predefined tasks +via [https://taskfile.dev/](https://taskfile.dev/) + +```bash +task --list-all +``` + +### Docker + +#### Build +``` +docker build -t ghcr.io/lablabs/aws-service-quotas-exporter:latest . +``` +#### Run +Authenticating with AWS credentials: + +``` +docker run --rm -p 8080:8080 \ + -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + -e AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \ + ghcr.io/lablabs/aws-service-quotas-exporter:latest +``` + +Access help: +``` +docker run --rm -p 8080:8080 -i ghcr.io/lablabs/aws-service-quotas-exporter:latest --help +``` + +## Contributing and reporting issues +Feel free to create an issue in this repository if you have questions, suggestions or feature requests. + +### Validation, linters and pull-requests + +We want to provide high quality code and modules. For this reason we are using +several [pre-commit hooks](.pre-commit-config.yaml) and +[GitHub Actions workflow](.github/workflows/golangci-lint.yml). A pull-request to the +master branch will trigger these validations and lints automatically. Please +check your code before you will create pull-requests. See +[pre-commit documentation](https://pre-commit.com/) and +[GitHub Actions documentation](https://docs.github.com/en/actions) for further +details. + +## License +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +See [LICENSE](LICENSE) for full details. + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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 + + https://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. diff --git a/Taskfile.yml b/Taskfile.yml index 150e702..0d45b98 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,6 +2,7 @@ version: '3' vars: APP_NAME: exporter + DOCKER_IMAGE: ghcr.io/lablabs/aws-service-quotas-exporter tasks: lint: @@ -10,7 +11,18 @@ tasks: build: cmds: - go build -o bin/{{ .APP_NAME }} cmd/{{ .APP_NAME}}/main.go - run-example: + bin:run: deps: [build] cmds: - ./bin/{{ .APP_NAME }} --config ./config/example.yaml + test: + cmds: + - go clean -testcache + - go test ./... -v + test:cover: + cmds: + - go test -coverprofile=coverage.out ./... + - go tool cover -func=coverage.out + docker:build: + cmds: + - docker build -t {{ .DOCKER_IMAGE }} . From 9f2f9499a187de5edf1cc775d717d2ef49061f21 Mon Sep 17 00:00:00 2001 From: rafajpet Date: Wed, 27 Mar 2024 07:34:27 +0100 Subject: [PATCH 5/6] docs improvements --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80a3fe4..f76264b 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ quota_exporter_ec2_elastic_ips_usage{quota_code="L-0263D0A3"} 4 ``` There is requirement regarding format output. If you want to export data. For every unique -combination of labels, it has to be one line stdout of you bash script in following csv format +combination of labels, it has to be one line stdout of your bash script in following csv format `lable_name_a=label_value,label_name_b=label_value_b,label_name_c=label_value_c,value_for_metric` From 7848f1da6fda0239e808ca856a77c221cb6a8d0e Mon Sep 17 00:00:00 2001 From: rafajpet Date: Wed, 3 Apr 2024 07:49:55 +0200 Subject: [PATCH 6/6] docs PR update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f76264b..1884a80 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ quotas: default: true ``` -### Usage of AwS resources exports via bash scraping +### Usage of AWS resources exports via bash scraping The pity of AWS service-quota API is that, for most of the resources there is no easy way how to show actual usage of concrete AWS resources. You have to do it via aws cli/sdk. This exporter try to solve this issue via simple bash script scheduler which @@ -68,7 +68,7 @@ Example above will produce following prometheus metric export: quota_exporter_ec2_elastic_ips_usage{quota_code="L-0263D0A3"} 4 ``` -There is requirement regarding format output. If you want to export data. For every unique +There is requirement regarding format output. For every unique combination of labels, it has to be one line stdout of your bash script in following csv format `lable_name_a=label_value,label_name_b=label_value_b,label_name_c=label_value_c,value_for_metric`