Skip to content

Commit

Permalink
9.1.0 - IAM role management (#395)
Browse files Browse the repository at this point in the history
* add options.autoscalingRole

* combine lambda roles into primary watchbot role

* document new autoscalingRoleArn option

* 9.1.0-dev.1

* pipline version 2

* just github v2

* actually v2

* update branch source

* 9.1.0-dev.2

* 9.1.0-dev.3

* 9.x branch build

* 9.1.0

* update snapshots

* add some tests
  • Loading branch information
mapsam authored Jun 13, 2024
1 parent 1088763 commit da57105
Show file tree
Hide file tree
Showing 9 changed files with 3,321 additions and 1,079 deletions.
2 changes: 1 addition & 1 deletion bin/dead-letter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 3,7 @@

/* eslint-disable no-console */

const { CloudFormation } = require('@aws-sdk/client-cloudformation')
const { CloudFormation } = require('@aws-sdk/client-cloudformation');
const { SQS } = require('@aws-sdk/client-sqs');
const inquirer = require('inquirer');
const stream = require('stream');
Expand Down
23 changes: 23 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 1,26 @@
### 9.1.0

- Merge ScalingLambdaRole and LambdaTotalMessagesRole into the primary WatchbotRole resource to reduce number of distinct IAM roles
- Add option `autoScalingRole` for using a predefined auto scaling role instead of creating one for the stack. This role should have the following permissions, with the resouce scope being as strict as desired:

```JSON
{
"Statement": [
{
"Action": [
"application-autoscaling:*",
"cloudwatch:DescribeAlarms",
"cloudwatch:PutMetricAlarm",
"ecs:UpdateService",
"ecs:DescribeServices"
],
"Resource": "*",
"Effect": "Allow"
}
]
}
```

### 9.0.1

- Bug fix: TotalMessagesLambda now working as expected; watchbot stacks now scaling down when no tasks in queue
Expand Down
2 changes: 1 addition & 1 deletion cloudformation/ecs-watchbot-generate-binaries.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 233,7 @@ const Resources = {
Owner: 'mapbox',
Repo: 'ecs-watchbot',
PollForSourceChanges: 'false',
Branch: 'master',
Branch: '9.x',
OAuthToken: '{{resolve:secretsmanager:code-pipeline-helper/access-token}}'
}
}
Expand Down
23 changes: 23 additions & 0 deletions docs/building-a-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 99,7 @@ When creating your watchbot stacks with the `watchbot.template()` method, you no
**placementConstraints** | ECS service [placement constraints](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-placementconstraint.html). This value is ignored for `capacity` values other than `'EC2'`. | Object[]/Ref | No | false
**placementStrategies** | ECS service [placement strategies](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-placementstrategy.html). This value is ignored for `capacity` values other than `'EC2'`. | Object[]/Ref | No | false
**structuredLogging** | Whether to emit logs in JSON format or not | Boolean | No | `false`
**autoscalingRoleArn** | A custom autoscaling role to use instead of building a distinct role for the stack | String/Ref | No | If not provided, an autoscaling role will be built with the permissions described in [Custom Autoscaling Role](#custom-autoscaling-role)

### writableFilesystem mode explained

Expand Down Expand Up @@ -137,3 138,25 @@ var outputs = {

cloudfriend.merge(myTemplate, watcher, outputs);
```

### Custom Autoscaling Role

You can provide a custom autoscaling role for your service. If you do not provide a custom role, a role with the following permissions will be created, which can only be assumed by the `application-autoscaling.amazonaws.com` principal.

```JSON
{
"Statement": [
{
"Action": [
"application-autoscaling:*",
"cloudwatch:DescribeAlarms",
"cloudwatch:PutMetricAlarm",
"ecs:UpdateService",
"ecs:DescribeServices"
],
"Resource": "*",
"Effect": "Allow"
}
]
}
```
190 changes: 73 additions & 117 deletions lib/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 38,8 @@ module.exports = (options = {}) => {
dashboard: true,
fifo: false,
fargatePublicIp: 'DISABLED',
structuredLogging: false
structuredLogging: false,
autoscalingRoleArn: false
},
options
);
Expand Down Expand Up @@ -246,7 247,7 @@ module.exports = (options = {}) => {
Statement: [
{
Effect: 'Allow',
Principal: { Service: ['ecs-tasks.amazonaws.com'] },
Principal: { Service: ['ecs-tasks.amazonaws.com', 'lambda.amazonaws.com'] },
Action: ['sts:AssumeRole']
}
]
Expand Down Expand Up @@ -285,6 286,39 @@ module.exports = (options = {}) => {
)
]
}
},
// roles for log-based lambda scaling utility
{
PolicyName: cf.join([cf.stackName, '-lambda-scaling']),
PolicyDocument: {
Statement: [
{
Effect: 'Allow',
Action: [
'logs:*'
],
Resource: cf.join([
'arn:',
cf.partition,
':logs:*:*:*'
])
},
{
Effect: 'Allow',
Action: [
'cloudwatch:PutMetricData'
],
Resource: '*'
},
{
Effect: 'Allow',
Action: [
'sqs:GetQueueAttributes'
],
Resource: cf.getAtt(prefixed('Queue'), 'Arn')
}
]
}
}
]
}
Expand Down Expand Up @@ -433,41 467,43 @@ module.exports = (options = {}) => {
Resources[prefixed('Service')].Properties.PlacementStrategies =
options.placementStrategies;

Resources[prefixed('ScalingRole')] = {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Statement: [
if (!options.autoscalingRoleArn) {
Resources[prefixed('ScalingRole')] = {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Statement: [
{
Effect: 'Allow',
Principal: { Service: ['application-autoscaling.amazonaws.com'] },
Action: ['sts:AssumeRole']
}
]
},
Path: '/',
Policies: [
{
Effect: 'Allow',
Principal: { Service: ['application-autoscaling.amazonaws.com'] },
Action: ['sts:AssumeRole']
PolicyName: 'watchbot-autoscaling',
PolicyDocument: {
Statement: [
{
Effect: 'Allow',
Action: [
'application-autoscaling:*',
'cloudwatch:DescribeAlarms',
'cloudwatch:PutMetricAlarm',
'ecs:UpdateService',
'ecs:DescribeServices'
],
Resource: '*'
}
]
}
}
]
},
Path: '/',
Policies: [
{
PolicyName: 'watchbot-autoscaling',
PolicyDocument: {
Statement: [
{
Effect: 'Allow',
Action: [
'application-autoscaling:*',
'cloudwatch:DescribeAlarms',
'cloudwatch:PutMetricAlarm',
'ecs:UpdateService',
'ecs:DescribeServices'
],
Resource: '*'
}
]
}
}
]
}
};
}
};
}

Resources[prefixed('ScalingTarget')] = {
Type: 'AWS::ApplicationAutoScaling::ScalableTarget',
Expand All @@ -482,7 518,7 @@ module.exports = (options = {}) => {
]),
MinCapacity: options.minSize,
MaxCapacity: options.maxSize,
RoleARN: cf.getAtt(prefixed('ScalingRole'), 'Arn')
RoleARN: options.autoscalingRoleArn || cf.getAtt(prefixed('ScalingRole'), 'Arn')
}
};

Expand Down Expand Up @@ -712,44 748,11 @@ module.exports = (options = {}) => {
}
};

Resources[prefixed('LambdaScalingRole')] = {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Statement: [
{
Effect: 'Allow',
Principal: { Service: ['lambda.amazonaws.com'] },
Action: ['sts:AssumeRole']
}
]
},
Policies: [{
PolicyName: 'CustomcfnScalingLambdaLogs',
PolicyDocument: {
Statement: [
{
Effect: 'Allow',
Action: [
'logs:*'
],
Resource: cf.join([
'arn:',
cf.partition,
':logs:*:*:*'
])
}
]
}
}]
}
};

Resources[prefixed('ScalingLambda')] = {
Type: 'AWS::Lambda::Function',
Properties: {
Handler: 'index.handler',
Role: cf.getAtt(prefixed('LambdaScalingRole'), 'Arn'),
Role: cf.getAtt(prefixed('Role'), 'Arn'),
Code: {
ZipFile: cf.sub(`
const response = require('./cfn-response');
Expand All @@ -775,7 778,7 @@ module.exports = (options = {}) => {
Type: 'AWS::Lambda::Function',
Properties: {
Handler: 'index.handler',
Role: cf.getAtt(prefixed('LambdaTotalMessagesRole'), 'Arn'),
Role: cf.getAtt(prefixed('Role'), 'Arn'),
Timeout: 60,
Code: {
ZipFile: cf.sub(`
Expand Down Expand Up @@ -812,53 815,6 @@ module.exports = (options = {}) => {
}
};

Resources[prefixed('LambdaTotalMessagesRole')] = {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Statement: [
{
Effect: 'Allow',
Principal: { Service: ['lambda.amazonaws.com'] },
Action: ['sts:AssumeRole']
}
]
},
Policies: [{
PolicyName: 'LambdaTotalMessagesMetric',
PolicyDocument: {
Statement: [
{
Effect: 'Allow',
Action: [
'logs:*'
],
Resource: cf.join([
'arn:',
cf.partition,
':logs:*:*:*'
])
},
{
Effect: 'Allow',
Action: [
'cloudwatch:PutMetricData'
],
Resource: '*'
},
{
Effect: 'Allow',
Action: [
'sqs:GetQueueAttributes'
],
Resource: cf.getAtt(prefixed('Queue'), 'Arn')
}
]
}
}]
}
};

Resources[prefixed('TotalMessagesSchedule')] = {
Type: 'AWS::Events::Rule',
Properties: {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 1,6 @@
{
"name": "@mapbox/watchbot",
"version": "9.0.1",
"version": "9.1.0",
"description": "",
"main": "index.js",
"engines": {
Expand Down
Loading

0 comments on commit da57105

Please sign in to comment.