Skip to content

Commit

Permalink
feat: update props and add CfnOutputs (#37)
Browse files Browse the repository at this point in the history
Fixes #
  • Loading branch information
rajyan authored Sep 12, 2022
1 parent a7a7d38 commit 05c7d60
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 50 deletions.
1 change: 1 addition & 0 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
projenrcTs: true,
});

project.tsconfigDev.addInclude('examples/**/*.ts');
// workaround until fixed https://youtrack.jetbrains.com/issue/WEB-57089/ESLint823-TypeError-thislibOptionsparse-is-not-a-function
project.addDevDeps('[email protected]');

Expand Down
55 changes: 55 additions & 0 deletions API.md

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

13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ Edit settings in `bin/low-cost-ecs.ts` and deploy the cdk construct. [Public hos

```
git clone https://github.com/rajyan/low-cost-ecs.git
yarn install
# edit settings in bin/low-cost-ecs.ts
npx cdk deploy
./node_modules/.bin/cdk deploy
```

Access to configured `recordDomainNames` and see that the nginx sample server has been deployed.
Expand All @@ -48,8 +49,8 @@ class SampleStack extends Stack {
super(scope, id, props);

const vpc = { /** Your VPC */ };
const securityGroup = {/** Your security group */ };
const serverTaskDefinition = {/** Your task definition */ };
const securityGroup = { /** Your security group */ };
const serverTaskDefinition = { /** Your task definition */ };

new LowCostECS(this, 'LowCostECS', {
hostedZoneDomain: "rajyan.net",
Expand Down Expand Up @@ -79,8 +80,8 @@ Resources generated in this stack
* ECS-optimized Amazon Linux 2 AMI instance auto-scaling group
* Automatically associated with Elastic IP on instance initialization
* ECS Service
* TLS/SSL certificate installation on default container startup
* Certificate EFS mounted on `/etc/letsencrypt`
* TLS/SSL certificate installation before default container startup
* Certificate EFS mounted on default container as `/etc/letsencrypt`
* Others
* VPC with only public subnets (no NAT Gateways to decrease cost)
* Security groups with minimum inbounds
Expand Down Expand Up @@ -129,7 +130,7 @@ aws ecs execute-command \

# Limitations

The ECS service occupies the host port, only one service can be run at a time.
The ECS service occupies the host port, so only one service can be run at a time.
The old task must be terminated before the new task launches, and this causes downtime on release.

Also, if you make changes that require recreating service, you may need to manually terminate the task of old the service.
15 changes: 0 additions & 15 deletions bin/low-cost-ecs.ts

This file was deleted.

2 changes: 1 addition & 1 deletion cdk.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"app": "npx ts-node --prefer-ts-exts bin/low-cost-ecs.ts"
"app": "npx ts-node --prefer-ts-exts examples/minimum.ts"
}
13 changes: 13 additions & 0 deletions examples/minimum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { App } from 'aws-cdk-lib';
import { LowCostECS } from '../src';

const app = new App();

new LowCostECS(app, 'LowCostECSStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
hostedZoneDomain: 'rajyan.net',
email: '[email protected]',
});
32 changes: 32 additions & 0 deletions examples/scheduled-autoscaling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { App } from 'aws-cdk-lib';
import { Schedule } from 'aws-cdk-lib/aws-autoscaling';
import { LowCostECS } from '../src';

const app = new App();

const stack = new LowCostECS(app, 'LowCostECSStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
hostedZoneDomain: 'rajyan.net',
recordDomainNames: ['test1.rajyan.net', 'test2.rajyan.net'],
email: '[email protected]',
hostInstanceSpotPrice: '0.0050',
});
stack.hostAutoScalingGroup.scaleOnSchedule('IncreaseAtMorning', {
timeZone: 'Asia/Tokyo',
schedule: Schedule.cron({
minute: '0',
hour: '8',
}),
desiredCapacity: 1,
});
stack.hostAutoScalingGroup.scaleOnSchedule('DecreaseAtNight', {
timeZone: 'Asia/Tokyo',
schedule: Schedule.cron({
minute: '0',
hour: '23',
}),
desiredCapacity: 0,
});
64 changes: 38 additions & 26 deletions src/low-cost-ecs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as path from 'path';
import * as lib from 'aws-cdk-lib';
import { AutoScalingGroup } from 'aws-cdk-lib/aws-autoscaling';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import { FileSystem } from 'aws-cdk-lib/aws-efs';
Expand Down Expand Up @@ -121,10 +122,16 @@ export interface LowCostECSProps extends lib.StackProps {
};

export class LowCostECS extends lib.Stack {
readonly vpc: ec2.IVpc;
readonly hostAutoScalingGroup: AutoScalingGroup;
readonly certFileSystem: FileSystem;
readonly cluster: ecs.Cluster;
readonly service: ecs.Ec2Service;

constructor(scope: Construct, id: string, props: LowCostECSProps) {
super(scope, id, props);

const vpc =
this.vpc =
props.vpc ??
new ec2.Vpc(this, 'Vpc', {
natGateways: 0,
Expand All @@ -136,12 +143,12 @@ export class LowCostECS extends lib.Stack {
],
});

const cluster = new ecs.Cluster(this, 'Cluster', {
vpc,
this.cluster = new ecs.Cluster(this, 'Cluster', {
vpc: this.vpc,
containerInsights: props.containerInsights,
});

const hostAutoScalingGroup = cluster.addCapacity('HostInstanceCapacity', {
this.hostAutoScalingGroup = this.cluster.addCapacity('HostInstanceCapacity', {
machineImage: ecs.EcsOptimizedImage.amazonLinux2(
ecs.AmiHardwareType.STANDARD,
{
Expand All @@ -157,15 +164,15 @@ export class LowCostECS extends lib.Stack {
});

if (props.securityGroup) {
hostAutoScalingGroup.addSecurityGroup(props.securityGroup);
this.hostAutoScalingGroup.addSecurityGroup(props.securityGroup);
} else {
hostAutoScalingGroup.connections.allowFromAnyIpv4(ec2.Port.tcp(80));
hostAutoScalingGroup.connections.allowFromAnyIpv4(ec2.Port.tcp(443));
hostAutoScalingGroup.connections.allowFrom(
this.hostAutoScalingGroup.connections.allowFromAnyIpv4(ec2.Port.tcp(80));
this.hostAutoScalingGroup.connections.allowFromAnyIpv4(ec2.Port.tcp(443));
this.hostAutoScalingGroup.connections.allowFrom(
ec2.Peer.anyIpv6(),
ec2.Port.tcp(80),
);
hostAutoScalingGroup.connections.allowFrom(
this.hostAutoScalingGroup.connections.allowFrom(
ec2.Peer.anyIpv6(),
ec2.Port.tcp(443),
);
Expand All @@ -174,13 +181,13 @@ export class LowCostECS extends lib.Stack {
/**
* Add managed policy to allow ssh through ssm manager
*/
hostAutoScalingGroup.role.addManagedPolicy(
this.hostAutoScalingGroup.role.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
);
/**
* Add policy to associate elastic ip on startup
*/
hostAutoScalingGroup.role.addToPrincipalPolicy(
this.hostAutoScalingGroup.role.addToPrincipalPolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['ec2:DescribeAddresses', 'ec2:AssociateAddress'],
Expand All @@ -193,23 +200,23 @@ export class LowCostECS extends lib.Stack {
hostInstanceIp.tags.setTag('Name', tagUniqueId);

const awsCliTag = props.awsCliDockerTag ?? 'latest';
hostAutoScalingGroup.addUserData(
this.hostAutoScalingGroup.addUserData(
'INSTANCE_ID=$(curl --silent http://169.254.169.254/latest/meta-data/instance-id)',
`ALLOCATION_ID=$(docker run --net=host amazon/aws-cli:${awsCliTag} ec2 describe-addresses --region ${hostAutoScalingGroup.env.region} --filter Name=tag:Name,Values=${tagUniqueId} --query 'Addresses[].AllocationId' --output text | head)`,
`docker run --net=host amazon/aws-cli:${awsCliTag} ec2 associate-address --region ${hostAutoScalingGroup.env.region} --instance-id "$INSTANCE_ID" --allocation-id "$ALLOCATION_ID" --allow-reassociation`,
`ALLOCATION_ID=$(docker run --net=host amazon/aws-cli:${awsCliTag} ec2 describe-addresses --region ${this.hostAutoScalingGroup.env.region} --filter Name=tag:Name,Values=${tagUniqueId} --query 'Addresses[].AllocationId' --output text | head)`,
`docker run --net=host amazon/aws-cli:${awsCliTag} ec2 associate-address --region ${this.hostAutoScalingGroup.env.region} --instance-id "$INSTANCE_ID" --allocation-id "$ALLOCATION_ID" --allow-reassociation`,
);

const certFileSystem = new FileSystem(this, 'FileSystem', {
vpc,
this.certFileSystem = new FileSystem(this, 'FileSystem', {
vpc: this.vpc,
encrypted: true,
securityGroup: new ec2.SecurityGroup(this, 'FileSystemSecurityGroup', {
vpc,
vpc: this.vpc,
allowAllOutbound: false,
}),
removalPolicy: props.removalPolicy ?? lib.RemovalPolicy.DESTROY,
});
certFileSystem.connections.allowDefaultPortTo(hostAutoScalingGroup);
certFileSystem.connections.allowDefaultPortFrom(hostAutoScalingGroup);
this.certFileSystem.connections.allowDefaultPortTo(this.hostAutoScalingGroup);
this.certFileSystem.connections.allowDefaultPortFrom(this.hostAutoScalingGroup);

/**
* ARecord to Elastic ip
Expand Down Expand Up @@ -288,14 +295,14 @@ export class LowCostECS extends lib.Stack {
},
);

certFileSystem.grant(
this.certFileSystem.grant(
certbotTaskDefinition.taskRole,
'elasticfilesystem:ClientWrite',
);
certbotTaskDefinition.addVolume({
name: 'certVolume',
efsVolumeConfiguration: {
fileSystemId: certFileSystem.fileSystemId,
fileSystemId: this.certFileSystem.fileSystemId,
},
});
certbotContainer.addMountPoints({
Expand All @@ -316,7 +323,7 @@ export class LowCostECS extends lib.Stack {
});

const certbotRunTask = new sfn_tasks.EcsRunTask(this, 'CreateCertificate', {
cluster: cluster,
cluster: this.cluster,
taskDefinition: certbotTaskDefinition,
launchTarget: new sfn_tasks.EcsEc2LaunchTarget(),
integrationPattern: sfn.IntegrationPattern.RUN_JOB,
Expand Down Expand Up @@ -346,14 +353,14 @@ export class LowCostECS extends lib.Stack {
*/
const serverTaskDefinition =
props.serverTaskDefinition ?? this.sampleSeverTask(records, logGroup);
certFileSystem.grant(
this.certFileSystem.grant(
serverTaskDefinition.taskRole,
'elasticfilesystem:ClientMount',
);
serverTaskDefinition.addVolume({
name: 'certVolume',
efsVolumeConfiguration: {
fileSystemId: certFileSystem.fileSystemId,
fileSystemId: this.certFileSystem.fileSystemId,
},
});
serverTaskDefinition.defaultContainer?.addMountPoints({
Expand Down Expand Up @@ -396,8 +403,8 @@ export class LowCostECS extends lib.Stack {
);
certbotStateMachine.grantStartExecution(serverTaskDefinition.taskRole);

new ecs.Ec2Service(this, 'Service', {
cluster: cluster,
this.service = new ecs.Ec2Service(this, 'Service', {
cluster: this.cluster,
taskDefinition: serverTaskDefinition,
desiredCount: 1,
minHealthyPercent: 0,
Expand All @@ -407,6 +414,11 @@ export class LowCostECS extends lib.Stack {
},
enableExecuteCommand: true,
});

new lib.CfnOutput(this, 'PublicIpAddress', { value: hostInstanceIp.ref });
new lib.CfnOutput(this, 'certbotStateMachineName', { value: certbotStateMachine.stateMachineName });
new lib.CfnOutput(this, 'ClusterName', { value: this.cluster.clusterName });
new lib.CfnOutput(this, 'ServiceName', { value: this.service.serviceName });
}

private sampleSeverTask(
Expand Down
28 changes: 28 additions & 0 deletions test/__snapshots__/low-cost-ecs.test.ts.snap

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

Loading

0 comments on commit 05c7d60

Please sign in to comment.