Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
packer.fmt:
cd packer && packer fmt . && cd -

packer.validate:
cd packer && packer validate . && cd -

packer.build:
cd packer && packer build linux-ami.pkr.hcl && cd -
62 changes: 54 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,58 @@
# Welcome to your CDK JavaScript project!
# Semaphore agent AWS stack

This is a blank project for JavaScript development with CDK.
This project is a CDK application used to deploy a Semaphore agent stack in AWS.

The `cdk.json` file tells the CDK Toolkit how to execute your app. The build step is not required when using JavaScript.
## Requisites

## Useful commands
### AMI

* `npm run test` perform the jest unit tests
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk synth` emits the synthesized CloudFormation template
The agents need an AMI id to use. If you haven't created one yet, just run:

```bash
make packer.build
```

This command uses packer to create an AWS EC2 AMI with everything the agent needs.

### CDK bootstrap

The AWS CDK requires a few resources to be around for it to work. It creates them with the `bootstrap` command:

```bash
cdk bootstrap aws://<YOUR_AWS_ACCOUNT_ID>/<YOUR_AWS_REGION>
```

## Generate the cloudformation stack

Under the hood, all the AWS CDK does is create a cloudformation stack. You can check the one it creates with:

```bash
cdk synth
```

## Deploying the stack

In order to deploy it, you are required to pass a few parameters:
- `imageId`: this is the AMI you created with `make packer.build` above.
- `semaphoreOrganization`: this is your Semaphore organization.
- `semaphoreToken`: this is the registration token for your agent type.

Other optional arguments are available:
- `instanceType`: this is the instance type the stack will use for your agents. By default, this is `t2.micro`.
- `minSize`: the minimum size for your agent auto scaling group. By default, this is 0.
- `maxSize`: the maximum size for your agent auto scaling group. By default, this is 1.
- `desiredCapacity`: the initial desired capacity for your agent auto scaling group. By default, this is 1
- `semaphoreAgentVersion`: the version of the agent to deploy. By default, the latest one.

```bash
cdk deploy \
--parameters imageId=ami-099f98f5c31d8ba1e \
--parameters semaphoreOrganization=semaphore \
--parameters semaphoreToken=YOUR_VERY_SENSITIVE_TOKEN
```

## Destroying the stack

```bash
cdk destroy
```
4 changes: 2 additions & 2 deletions lambda/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ function startAgentOnInstance(instanceId) {
InstanceIds: [instanceId],
DocumentName: 'AWS-RunShellScript',
Parameters: {
commands: ['sudo systemctl start semaphore-agent'],
executionTimeout: ['10']
commands: ['/opt/semaphore/install-agent.sh'],
executionTimeout: ['20']
},
};

Expand Down
82 changes: 76 additions & 6 deletions lib/aws-semaphore-agent-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,35 @@ const lambda = require("@aws-cdk/aws-lambda");
const events = require("@aws-cdk/aws-events");
const eventTargets = require("@aws-cdk/aws-events-targets");
const autoscaling = require("@aws-cdk/aws-autoscaling");
const ssm = require("@aws-cdk/aws-ssm");

const stackPrefix = "semaphore-agent";
const autoscalingGroupName = `${stackPrefix}-asg`;
const ssmParameterName = `${stackPrefix}-params`;

class AwsSemaphoreAgentStack extends cdk.Stack {
constructor(scope, id, props) {
super(scope, id, props);

/**
* When processing the lifecycle hook event for the instance going into rotation,
* the lambda executes a script in the instance to install and start the agent.
* That script needs the agent parameters (organization, token, ...). We expose
* those parameters through an SSM parameter.
*
* Note that this creates a tight coupling between this cdk application and the script
* being executed to install and start the agent, as both of them need to use the proper
* parameter name and structure. So, if you change anything about the SSM Parameter here,
* the install script used by the lambda will have to change as well.
*/
let ssmParameter = this.createSSMParameter();

/**
* The lambda and EventBridge rule is created before the scaling group
* because it needs to be present before the auto scaling group exists.
* Otherwise, the initial instances will not have the agent started.
*/
let lambdaRole = this.createRoleForLambda();
let lambdaRole = this.createRoleForLambda(ssmParameter);
let lambda = this.createLambdaFunction(lambdaRole);
this.createEventBridgeRule(lambda);

Expand All @@ -27,6 +42,45 @@ class AwsSemaphoreAgentStack extends cdk.Stack {
this.createWarmPool(autoScalingGroup);
}

createSSMParameter() {
const semaphoreOrganizationParameter = new cdk.CfnParameter(this, "semaphoreOrganization", {
type: "String",
description: "The semaphore organization to use for the agent."
});

const semaphoreTokenParameter = new cdk.CfnParameter(this, "semaphoreToken", {
type: "String",
description: "The semaphore registration token to use for the agent.",
noEcho: true,
});

const semaphoreAgentVersionParameter = new cdk.CfnParameter(this, "semaphoreAgentVersion", {
type: "String",
description: "The agent version to use.",
default: "v2.0.17"
});

const machineUserParameter = new cdk.CfnParameter(this, "machineUser", {
type: "String",
description: "The user to run the agent on the machine.",
default: "ubuntu",
});

let parameters = {
agentVersion: semaphoreAgentVersionParameter.valueAsString,
organization: semaphoreOrganizationParameter.valueAsString,
token: semaphoreTokenParameter.valueAsString,
vmUser: machineUserParameter.valueAsString
}

return new ssm.StringParameter(this, `SemaphoreAgentParameter`, {
description: 'Parameters required by the semaphore agent',
parameterName: ssmParameterName,
stringValue: JSON.stringify(parameters),
tier: ssm.ParameterTier.STANDARD,
});
}

createIamInstanceProfile() {
let account = cdk.Stack.of(this).account;
let ec2Role = new iam.Role(this, 'ec2Role', {
Expand All @@ -41,6 +95,14 @@ class AwsSemaphoreAgentStack extends cdk.Stack {
resources: [`arn:aws:autoscaling:*:${account}:autoScalingGroup:*:autoScalingGroupName/${autoscalingGroupName}`]
}));

ec2Role.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["ssm:GetParameter"],
resources: [
`arn:aws:ssm:*:*:parameter/${ssmParameterName}`
]
}))

const instanceProfileDeps = new cdk.ConcreteDependable();
instanceProfileDeps.add(ec2Role);

Expand All @@ -54,7 +116,10 @@ class AwsSemaphoreAgentStack extends cdk.Stack {
return iamInstanceProfile;
}

createRoleForLambda() {
createRoleForLambda(ssmParameter) {
const lambdaRoleDependencies = new cdk.ConcreteDependable();
lambdaRoleDependencies.add(ssmParameter);

let account = cdk.Stack.of(this).account;
let lambdaRole = new iam.Role(this, 'lambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
Expand Down Expand Up @@ -87,6 +152,7 @@ class AwsSemaphoreAgentStack extends cdk.Stack {
resources: ["*"]
}));

lambdaRole.node.addDependency(lambdaRoleDependencies);
return lambdaRole;
}

Expand Down Expand Up @@ -136,7 +202,8 @@ class AwsSemaphoreAgentStack extends cdk.Stack {

const instanceTypeParameter = new cdk.CfnParameter(this, "instanceType", {
type: "String",
description: "The instance type to use to launch auto scaling instances."
description: "The instance type to use to launch auto scaling instances.",
default: "t2.micro"
});

let launchConfig = new autoscaling.CfnLaunchConfiguration(this, 'launchConfiguration', {
Expand All @@ -157,17 +224,20 @@ class AwsSemaphoreAgentStack extends cdk.Stack {

const minSizeParameter = new cdk.CfnParameter(this, "minSize", {
type: "String",
description: "The minSize for the semaphore-agent auto scaling group."
description: "The minSize for the semaphore-agent auto scaling group.",
default: "0"
});

const maxSizeParameter = new cdk.CfnParameter(this, "maxSize", {
type: "String",
description: "The maxSize for the semaphore-agent auto scaling group."
description: "The maxSize for the semaphore-agent auto scaling group.",
default: "1"
});

const desiredCapacityParameter = new cdk.CfnParameter(this, "desiredCapacity", {
type: "String",
description: "The desired capacity for the semaphore-agent auto scaling group."
description: "The desired capacity for the semaphore-agent auto scaling group.",
default: "1"
});

const autoScalingGroupDependencies = new cdk.ConcreteDependable();
Expand Down
Loading