Skip to content

Commit

Permalink
feat(ecs-exec): add ecs exec setup in CDK
Browse files Browse the repository at this point in the history
  • Loading branch information
briancaffey committed Sep 13, 2021
1 parent 7becda8 commit 77345f4
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 18 deletions.
10 changes: 10 additions & 0 deletions .projen/deps.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@
"version": "^1.122.0",
"type": "peer"
},
{
"name": "@aws-cdk/aws-kms",
"version": "^1.122.0",
"type": "peer"
},
{
"name": "@aws-cdk/aws-lambda",
"version": "^1.122.0",
Expand Down Expand Up @@ -325,6 +330,11 @@
"version": "^1.122.0",
"type": "runtime"
},
{
"name": "@aws-cdk/aws-kms",
"version": "^1.122.0",
"type": "runtime"
},
{
"name": "@aws-cdk/aws-lambda",
"version": "^1.122.0",
Expand Down
1 change: 1 addition & 0 deletions .projenrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const project = new AwsCdkConstructLibrary({
'@aws-cdk/aws-rds',
'@aws-cdk/aws-iam',
'@aws-cdk/custom-resources',
'@aws-cdk/aws-kms',
],

python: {
Expand Down
55 changes: 53 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Name|Description
[DjangoEcs](#django-cdk-djangoecs)|Configures a Django project using ECS Fargate.
[DjangoEks](#django-cdk-djangoeks)|Configures a Django project using EKS.
[S3BucketResources](#django-cdk-s3bucketresources)|Construct that configures an S3 bucket.
[StaticSite](#django-cdk-staticsite)|Construct for a static website hosted with S3 and CloudFront.


**Structs**
Expand All @@ -16,6 +17,7 @@ Name|Description
[DjangoEcsProps](#django-cdk-djangoecsprops)|Options to configure a Django ECS project.
[DjangoEksProps](#django-cdk-djangoeksprops)|Options to configure a Django EKS project.
[S3BucketProps](#django-cdk-s3bucketprops)|Properties for the S3 bucket.
[StaticSiteProps](#django-cdk-staticsiteprops)|*No description*



Expand All @@ -39,12 +41,14 @@ new DjangoEcs(scope: Construct, id: string, props: DjangoEcsProps)
* **id** (<code>string</code>) *No description*
* **props** (<code>[DjangoEcsProps](#django-cdk-djangoecsprops)</code>) *No description*
* **imageDirectory** (<code>string</code>) The location of the Dockerfile used to create the main application image.
* **apiDomainName** (<code>string</code>) Domain name for backend (including sub-domain). __*Optional*__
* **bucketName** (<code>string</code>) Name of existing bucket to use for media files. __*Optional*__
* **certificateArn** (<code>string</code>) Certificate ARN. __*Optional*__
* **domainName** (<code>string</code>) Domain name for backend (including sub-domain). __*Optional*__
* **useCeleryBeat** (<code>boolean</code>) Used to enable the celery beat service. __*Default*__: false
* **useEcsExec** (<code>boolean</code>) This allows you to exec into the backend API container using ECS Exec. __*Default*__: false
* **vpc** (<code>[IVpc](#aws-cdk-aws-ec2-ivpc)</code>) The VPC to use for the application. It must contain PUBLIC, PRIVATE and ISOLATED subnets. __*Optional*__
* **webCommand** (<code>Array<string></code>) The command used to run the API web service. __*Optional*__
* **zoneName** (<code>string</code>) *No description* __*Optional*__



Expand Down Expand Up @@ -135,6 +139,35 @@ Name | Type | Description



## class StaticSite <a id="django-cdk-staticsite"></a>

Construct for a static website hosted with S3 and CloudFront.

https://github.com/aws-samples/aws-cdk-examples/blob/master/typescript/static-site/static-site.ts

__Implements__: [IConstruct](#constructs-iconstruct), [IConstruct](#aws-cdk-core-iconstruct), [IConstruct](#constructs-iconstruct), [IDependable](#aws-cdk-core-idependable)
__Extends__: [Construct](#aws-cdk-core-construct)

### Initializer




```ts
new StaticSite(scope: Construct, id: string, props: StaticSiteProps)
```

* **scope** (<code>[Construct](#aws-cdk-core-construct)</code>) *No description*
* **id** (<code>string</code>) *No description*
* **props** (<code>[StaticSiteProps](#django-cdk-staticsiteprops)</code>) *No description*
* **frontendDomainName** (<code>string</code>) Domain name for static site (including sub-domain).
* **pathToDist** (<code>string</code>) Path to static site distribution directory.
* **zoneName** (<code>string</code>) The zoneName of the hosted zone.
* **certificateArn** (<code>string</code>) Certificate ARN. __*Optional*__




## struct DjangoEcsProps <a id="django-cdk-djangoecsprops"></a>


Expand All @@ -145,12 +178,14 @@ Options to configure a Django ECS project.
Name | Type | Description
-----|------|-------------
**imageDirectory** | <code>string</code> | The location of the Dockerfile used to create the main application image.
**apiDomainName**? | <code>string</code> | Domain name for backend (including sub-domain).<br/>__*Optional*__
**bucketName**? | <code>string</code> | Name of existing bucket to use for media files.<br/>__*Optional*__
**certificateArn**? | <code>string</code> | Certificate ARN.<br/>__*Optional*__
**domainName**? | <code>string</code> | Domain name for backend (including sub-domain).<br/>__*Optional*__
**useCeleryBeat**? | <code>boolean</code> | Used to enable the celery beat service.<br/>__*Default*__: false
**useEcsExec**? | <code>boolean</code> | This allows you to exec into the backend API container using ECS Exec.<br/>__*Default*__: false
**vpc**? | <code>[IVpc](#aws-cdk-aws-ec2-ivpc)</code> | The VPC to use for the application. It must contain PUBLIC, PRIVATE and ISOLATED subnets.<br/>__*Optional*__
**webCommand**? | <code>Array<string></code> | The command used to run the API web service.<br/>__*Optional*__
**zoneName**? | <code>string</code> | __*Optional*__



Expand Down Expand Up @@ -186,3 +221,19 @@ Name | Type | Description



## struct StaticSiteProps <a id="django-cdk-staticsiteprops"></a>






Name | Type | Description
-----|------|-------------
**frontendDomainName** | <code>string</code> | Domain name for static site (including sub-domain).
**pathToDist** | <code>string</code> | Path to static site distribution directory.
**zoneName** | <code>string</code> | The zoneName of the hosted zone.
**certificateArn**? | <code>string</code> | Certificate ARN.<br/>__*Optional*__



22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,32 @@ This project uses [projen](https://github.com/projen/projen).

For development of this library, a sample Django application is included as a git submodule in `test/django-step-by-step`. This Django project is used when deploying the application, and can be replaced with your own project for testing purposes.

## ECS Exec

ECS Exec is a relatively new feature that allows us to open an internactive shell in container running in a Fargate task. In order to use ECS Exec please refer to the `helper.sh` file that defines `ecs_exec_service` and `ecs_exec_task`.

Additionally, the user that is calling these commands will need to have the following IAM permissions:

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ecs:ExecuteCommand",
"Resource": "arn:aws:ecs:<aws-region>:<aws-account-id>:cluster/*"
}
]
}
```

The `Resource` can be more narrowly scoped to the scpecific clusters in which you want to allow the user to run commands.

## Current Development Efforts

This project is under active development. Here are some of the things that I'm curently working on:

- [x] Add ECS Exec for ECS construct
- [ ] Go over this Kubernetes checklist: [https://www.weave.works/blog/production-ready-checklist-kubernetes](https://www.weave.works/blog/production-ready-checklist-kubernetes)
- [ ] Add snapshot tests and refactor the application
- [ ] Add unit tests
19 changes: 19 additions & 0 deletions helper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ecs_exec_service CLUSTER SERVICE CONTAINER
function ecs_exec_service() {
CLUSTER=$1
SERVICE=$2
CONTAINER=$3
TASK=$(aws ecs list-tasks --service-name $SERVICE --cluster $CLUSTER --query 'taskArns[0]' --output text)
ecs_exec_task $CLUSTER $TASK $CONTAINER
}

# ecs_exec_task CLUSTER TASK CONTAINER
function ecs_exec_task() {
aws ecs execute-command \
--region "us-east-1" \
--cluster $1 \
--task $2 \
--container $3 \
--command "/bin/bash" \
--interactive
}
2 changes: 2 additions & 0 deletions package.json

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

98 changes: 96 additions & 2 deletions src/django-ecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as ec2 from '@aws-cdk/aws-ec2';
import * as ecs from '@aws-cdk/aws-ecs';
import * as patterns from '@aws-cdk/aws-ecs-patterns';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as logs from '@aws-cdk/aws-logs';
import * as route53 from '@aws-cdk/aws-route53';
import * as route53targets from '@aws-cdk/aws-route53-targets';
Expand Down Expand Up @@ -69,6 +71,16 @@ export interface DjangoEcsProps {
*/
readonly useCeleryBeat?: boolean;

/**
*
* This allows you to exec into the backend API container using ECS Exec
*
* https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html
*
* @default false
*/
readonly useEcsExec?: boolean;

}

/**
Expand Down Expand Up @@ -296,9 +308,91 @@ export class DjangoEcs extends cdk.Construct {
// optionally disable the admin interface
// albfs.listener.addAction


/**
* Health check for the application load balancer
*/
* Configure ECS Exec if enabled in props
*
* ECS Exec allows you to open an interactive shell in a container running in a ECS Fargate task
*
* Based on https://github.com/pahud/ecs-exec-cdk-demo/blob/main/src/main.ts
*
* Helpful: https://github.com/aws-containers/amazon-ecs-exec-checker
*
* TODO: clean up permissions
*/
if (props.useEcsExec ?? false) {

// create kms key
const kmsKey = new kms.Key(this, 'KmsKey');
// create log group
const logGroup = new logs.LogGroup(this, 'LogGroup');
// ecs exec bucket
const execBucket = new s3.Bucket(this, 'EcsExecBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});

logGroup.grantWrite(taskDefinition.taskRole);
kmsKey.grantDecrypt(taskDefinition.taskRole);
execBucket.grantPut(taskDefinition.taskRole);

// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html#ecs-exec-logging
taskDefinition.taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: [
'logs:DescribeLogGroups',
'logs:DescribeLogStreams',
],
resources: ['*'],
}));

// TODO: this can be removed
// check the resources property of the policy statement`
taskDefinition.taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: [
'logs:DescribeLogGroups',
'logs:CreateLogStream',
'logs:DescribeLogStreams',
'logs:PutLogEvents',
],
resources: [`arn:aws:logs:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:log-group:/aws/ecs/${logGroup.logGroupName}:*`],
}));

taskDefinition.taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
resources: ['*'],
actions: [
'ssmmessages:CreateControlChannel',
'ssmmessages:CreateDataChannel',
'ssmmessages:OpenControlChannel',
'ssmmessages:OpenDataChannel',
],
}));


// // we need ExecuteCommandConfiguration
const cfnCluster = this.cluster.node.defaultChild as ecs.CfnCluster;
cfnCluster.addPropertyOverride('Configuration.ExecuteCommandConfiguration', {
KmsKeyId: kmsKey.keyId,
LogConfiguration: {
CloudWatchLogGroupName: logGroup.logGroupName,
S3BucketName: execBucket.bucketName,
S3KeyPrefix: 'exec-output',
},
Logging: 'OVERRIDE',
});

// enable EnableExecuteCommand for the service
const cfnService = albfs.service.node.findChild('Service') as ecs.CfnService;
cfnService.addPropertyOverride('EnableExecuteCommand', true);

// Output the command that will allow us to use ECS Exec on our backend container
new cdk.CfnOutput(this, 'EcsExecCommand', {
value:
`ecs_exec_service ${this.cluster.clusterName} ${albfs.service.serviceName} ${taskDefinition.defaultContainer?.containerName}`,
});
}
/**
* Health check for the application load balancer
*/
albfs.targetGroup.configureHealthCheck({
path: '/api/health-check/',
});
Expand Down
1 change: 1 addition & 0 deletions src/integ/integ.django-ecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const construct = new DjangoEcs(stack, 'DjangoEcsSample', {
useCeleryBeat: true,
apiDomainName: process.env.API_DOMAIN_NAME,
zoneName: process.env.ZONE_NAME,
useEcsExec: true,

// certificateArn: process.env.CERTIFICATE_ARN,
});
Expand Down
Loading

0 comments on commit 77345f4

Please sign in to comment.