Skip to content

Commit 258c399

Browse files
odarbelaezeWBHpFJxWizmartydill
committed
Open source rightsizer
Co-authored-by: Marco Ippolito <[email protected]> Co-authored-by: Marty Dill <[email protected]>
0 parents  commit 258c399

File tree

22 files changed

+1530
-0
lines changed

22 files changed

+1530
-0
lines changed

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.git
2+
.github
3+
.buildkite
4+
Dockerfile
5+
rightsizer

.github/workflows/go.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Go
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Go
16+
uses: actions/setup-go@v5
17+
with:
18+
go-version: 1.23
19+
20+
- name: Build
21+
run: go build -v ./...
22+
23+
- name: Test
24+
run: go test -v ./...

.github/workflows/ship-latest.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: "Publish latest artifacts"
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
jobs:
8+
docker:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Set up QEMU
12+
uses: docker/setup-qemu-action@v3
13+
14+
- name: Set up Docker Buildx
15+
uses: docker/setup-buildx-action@v3
16+
17+
- name: Login to DockerHub
18+
uses: docker/login-action@v3
19+
with:
20+
username: ${{ secrets.DOCKERHUB_USER }}
21+
password: ${{ secrets.DOCKERHUB_TOKEN }}
22+
23+
- name: Build and push
24+
id: docker_build
25+
uses: docker/build-push-action@v6
26+
with:
27+
push: true
28+
platforms: linux/amd64,linux/arm64
29+
tags: |
30+
nextroll/rightsizer:latest
31+
32+
- name: Image digest
33+
run: echo ${{ steps.docker_build.outputs.digest }}

.github/workflows/ship-version.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: "Publish versioned artifacts"
2+
3+
on:
4+
release:
5+
types: ["published"]
6+
7+
jobs:
8+
docker:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Set up QEMU
12+
uses: docker/setup-qemu-action@v3
13+
14+
- name: Set up Docker Buildx
15+
uses: docker/setup-buildx-action@v3
16+
17+
- name: Login to DockerHub
18+
uses: docker/login-action@v3
19+
with:
20+
username: ${{ secrets.DOCKERHUB_USER }}
21+
password: ${{ secrets.DOCKERHUB_TOKEN }}
22+
23+
- name: Build and push
24+
id: docker_build
25+
uses: docker/build-push-action@v6
26+
with:
27+
push: true
28+
platforms: linux/amd64,linux/arm64
29+
tags: |
30+
nextroll/rightsizer:v3
31+
nextroll/rightsizer:v3.0
32+
nextroll/rightsizer:v3.0.0
33+
34+
- name: Image digest
35+
run: echo ${{ steps.docker_build.outputs.digest }}

.gitignore

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# Test binary, built with `go test -c`
9+
*.test
10+
11+
# Output of the go coverage tool, specifically when used with LiteIDE
12+
*.out
13+
14+
# Dependency directories (remove the comment below to include it)
15+
# vendor/
16+
17+
# Just in case I go build
18+
rightsizer
19+
rightsizer.report.json
20+
.rightsizer.json

Dockerfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM golang:1.23-alpine AS builder
2+
COPY . /src/github.com/adroll/rightsizer/
3+
RUN cd /src/github.com/adroll/rightsizer/ && CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' .
4+
5+
FROM alpine
6+
COPY --from=builder /src/github.com/adroll/rightsizer/rightsizer /usr/bin/
7+
COPY --from=nextroll/ecs-ship:v2.0.0 /usr/bin/ecs-ship /usr/bin/
8+
CMD [ "rightsizer" ]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2025 AdRoll, Inc. and rightsizer contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9+
of the Software, and to permit persons to whom the Software is furnished to do
10+
so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# rightsizer
2+
3+
`rightsizer` allows you to compute the appropriate size of your ECS services
4+
based on the average resource usage in a given period of time. You can use the
5+
output of the program to update your task definitions and keep your services on
6+
the perfect size.
7+
8+
## Usage
9+
10+
In order to use `rightsizer` you need to be logged in to your `AWS Cli` using
11+
[aws config files][aws-config] or a credential manager like our own
12+
[hologram][hologram]. Then you invoque the `rightsizer` command using your
13+
target **cluster** and **service**:
14+
15+
```sh
16+
$ rightsizer some-cluster some-service
17+
containerDefinitions:
18+
some-database:
19+
cpu: 1
20+
memory: 500
21+
memoryReservation: 95
22+
some-nginx-thing:
23+
cpu: 1
24+
memory: 500
25+
memoryReservation: 95
26+
some-language-api:
27+
cpu: 1
28+
memory: 500
29+
memoryReservation: 95
30+
```
31+
32+
The program will output the suggested configuration for your service based on
33+
your actual usage. Then you can use this output to patch your service or patch
34+
your deploy configuration.
35+
36+
## Getting rightsizer
37+
38+
The best way to get `rightsizer` is from dockerhub:
39+
40+
```sh
41+
docker pull nextroll/rightsizer
42+
```
43+
44+
Then you can run the container with the following command:
45+
46+
```sh
47+
docker run --rm nextroll/rightsizer rightsizer some-cluster some-service
48+
```
49+
50+
## Building rightsizer
51+
52+
If you want to build `rightsizer` from source you can use the following,
53+
54+
```sh
55+
go build .
56+
```
57+
58+
This will generate a binary called `rightsizer` that you can use to run the
59+
program.
60+
61+
## Testing rightsizer
62+
63+
We use [gomock][gomock] to generate mocks for our custom clients. If you update
64+
or create a new client, you need to regenerate the mocks. You can do this by
65+
running:
66+
67+
```sh
68+
go generate ./...
69+
```
70+
71+
Then you can run the tests with the following command:
72+
73+
```sh
74+
go test ./...
75+
```
76+
77+
[aws-config]: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html "AWS Config"
78+
[hologram]: https://github.com/AdRoll/hologram "Hologram"
79+
[gomock]: https://github.com/uber-go/mock "gomock"

clients/cloudwatch.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package clients
2+
3+
//go:generate mockgen -destination=mocks/cloudwatch.go . CloudwatchClient
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"time"
9+
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
12+
cloudwatchTypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
13+
)
14+
15+
type CloudwatchClient interface {
16+
// GetAverage returns the average value of a metric over a period of time
17+
GetAverage(ctx context.Context, input *GetAverageInput) (*float64, error)
18+
}
19+
20+
// GetAverageInput is the input for the GetAverage function
21+
type GetAverageInput struct {
22+
// MetricName is the name of the metric to get the average value of
23+
MetricName string
24+
// ClusterName is the name of the cluster to get the average value of
25+
ClusterName string
26+
// ServiceName is the name of the service to get the average value of
27+
ServiceName string
28+
// TimeFrame is the period of time to get the average value over
29+
TimeFrame time.Duration
30+
}
31+
32+
type cloudWatchClient struct {
33+
client *cloudwatch.Client
34+
}
35+
36+
// NewCloudWatchClient returns a new CloudWatchClient
37+
func NewCloudWatchClient(client *cloudwatch.Client) CloudwatchClient {
38+
return &cloudWatchClient{client: client}
39+
}
40+
41+
func (c *cloudWatchClient) GetAverage(ctx context.Context, input *GetAverageInput) (*float64, error) {
42+
now := time.Now()
43+
period := int32(input.TimeFrame.Seconds())
44+
startTime := now.Add(-input.TimeFrame)
45+
getMetricsInput := &cloudwatch.GetMetricStatisticsInput{
46+
MetricName: &input.MetricName,
47+
Dimensions: []cloudwatchTypes.Dimension{
48+
{
49+
Name: aws.String("ClusterName"),
50+
Value: &input.ClusterName,
51+
},
52+
{
53+
Name: aws.String("ServiceName"),
54+
Value: &input.ServiceName,
55+
},
56+
},
57+
StartTime: &startTime,
58+
EndTime: &now,
59+
Namespace: aws.String("AWS/ECS"),
60+
Period: &period,
61+
Statistics: []cloudwatchTypes.Statistic{
62+
cloudwatchTypes.StatisticAverage,
63+
},
64+
}
65+
metricsResponse, err := c.client.GetMetricStatistics(ctx, getMetricsInput)
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to get metrics: %w", err)
68+
}
69+
if len(metricsResponse.Datapoints) != 1 {
70+
return nil, fmt.Errorf("failed to get %s data points from CloudWatch", input.MetricName)
71+
}
72+
return metricsResponse.Datapoints[0].Average, nil
73+
}

clients/ecs.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package clients
2+
3+
//go:generate mockgen -destination=mocks/ecs.go . ECSClient
4+
5+
import (
6+
"context"
7+
"errors"
8+
"fmt"
9+
10+
"github.com/aws/aws-sdk-go-v2/service/ecs"
11+
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
12+
)
13+
14+
type ECSClient interface {
15+
// GetService returns the service with the given name in the given cluster
16+
GetService(ctx context.Context, input *GetServiceInput) (*ecsTypes.Service, error)
17+
// GetTaskDefinition returns the task definition with the given name
18+
GetTaskDefinition(ctx context.Context, input *GetTaskDefinitionInput) (*ecsTypes.TaskDefinition, error)
19+
}
20+
21+
// GetServiceInput is the input for GetService
22+
type GetServiceInput struct {
23+
// Cluster is the name of the ECS cluster
24+
Cluster string
25+
// Service is the name of the ECS service
26+
Service string
27+
}
28+
29+
type GetTaskDefinitionInput struct {
30+
// TaskDefinition is the name of the ECS task definition
31+
TaskDefinition string
32+
}
33+
34+
type ecsClient struct {
35+
client *ecs.Client
36+
}
37+
38+
func NewECSClient(client *ecs.Client) ECSClient {
39+
return &ecsClient{client: client}
40+
}
41+
42+
func (c *ecsClient) GetService(ctx context.Context, input *GetServiceInput) (*ecsTypes.Service, error) {
43+
output, err := c.client.DescribeServices(ctx, &ecs.DescribeServicesInput{
44+
Cluster: &input.Cluster,
45+
Services: []string{input.Service},
46+
})
47+
if err != nil {
48+
return nil, fmt.Errorf("failed to describe service: %w", err)
49+
}
50+
if len(output.Services) == 0 {
51+
return nil, errors.New("service not found")
52+
}
53+
return &output.Services[0], nil
54+
}
55+
56+
func (c *ecsClient) GetTaskDefinition(ctx context.Context, input *GetTaskDefinitionInput) (*ecsTypes.TaskDefinition, error) {
57+
output, err := c.client.DescribeTaskDefinition(ctx, &ecs.DescribeTaskDefinitionInput{
58+
TaskDefinition: &input.TaskDefinition,
59+
})
60+
if err != nil {
61+
return nil, fmt.Errorf("failed to describe task definition: %w", err)
62+
}
63+
return output.TaskDefinition, nil
64+
}

0 commit comments

Comments
 (0)