Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Mac agent support #158

Merged
merged 4 commits into from
Jul 20, 2022
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Data Retention](#data-retention)
- [Add environment variable](#add-environment-variables)
- [Assume role](#cross-account-assume-role)
- [Mac agents](#mac-agents)
- [Troubleshooting](#troubleshooting)
- [Main Node](#main-node)
- [Useful commands](#useful-commands)
Expand Down Expand Up @@ -141,6 +142,17 @@ npm run cdk deploy OpenSearch-CI-Dev -- -c useSsl=false -c runWithOidc=false -c
```
NOTE: The assume role has to be pre-created for the agents to assume. Once CDK stack is deployed with `-c agentAssumeRole` flag, make sure this flag is passed for next CDK operations to make sure this created policy that assumes cross-account role is not removed.

#### Mac agents
##### Prerequisite
To deploy mac agents, as a prerequisites make sure the backend AWS account has dedicated hosts setup done with instance family as `mac1` and instance type as `mac1.metal`. For More details check the [getting-started](https://aws.amazon.com/getting-started/hands-on/launch-connect-to-amazon-ec2-mac-instance/) guide.

##### Configuration
To configure ec2 Mac agent setup run the stack with `-c macAgent=true`.
Example:
```
npm run cdk deploy OpenSearch-CI-Dev -- -c useSsl=false -c runWithOidc=false -c macAgent=true
```

#### Runnning additional commands
In cases where you need to run additional logic/commands, such as adding a cron to emit ssl cert expiry metric, you can pass the commands as a script using `additionalCommands` context parameter.
Below sample will write the python script to $HOME/hello-world path on jenkins master node and then execute it once the jenkins master node has been brought up.
Expand Down
5 changes: 4 additions & 1 deletion lib/ci-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export interface CIStackProps extends StackProps {
readonly agentAssumeRole?: string;
/** File path containing global environment variables to be added to jenkins enviornment */
readonly envVarsFilePath?: string;
/** Add Mac agent to jenkins */
readonly macAgent?: boolean;
}

export class CIStack extends Stack {
Expand All @@ -61,6 +63,7 @@ export class CIStack extends Stack {
});

const agentAssumeRoleContext = `${props?.agentAssumeRole ?? this.node.tryGetContext('agentAssumeRole')}`;
const macAgentParameter = `${props?.macAgent ?? this.node.tryGetContext('macAgent')}`;

const useSslParameter = `${props?.useSsl ?? this.node.tryGetContext('useSsl')}`;
if (useSslParameter !== 'true' && useSslParameter !== 'false') {
Expand Down Expand Up @@ -122,7 +125,7 @@ export class CIStack extends Stack {
adminUsers: props?.adminUsers,
agentNodeSecurityGroup: securityGroups.agentNodeSG.securityGroupId,
subnetId: vpc.publicSubnets[0].subnetId,
}, agentNodes, agentAssumeRoleContext.toString());
}, agentNodes, agentAssumeRoleContext.toString(), macAgentParameter.toString());
gaiksaya marked this conversation as resolved.
Show resolved Hide resolved

const externalLoadBalancer = new JenkinsExternalLoadBalancer(this, {
vpc,
Expand Down
64 changes: 62 additions & 2 deletions lib/compute/agent-node-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export interface AgentNodeProps {
numExecutors: number;
initScript: string
}

export interface AgentNodeNetworkProps {
readonly agentNodeSecurityGroup: string;
readonly subnetId: string;
Expand Down Expand Up @@ -104,6 +103,7 @@ export class AgentNodeConfig {
}),
);

/* eslint-disable eqeqeq */
if (assumeRole.toString() !== 'undefined') {
// policy to allow assume role AssumeRole
AgentNodeRole.addToPolicy(
Expand All @@ -124,13 +124,16 @@ export class AgentNodeConfig {
});
}

public addAgentConfigToJenkinsYaml(stack: Stack, templates: AgentNodeProps[], props: AgentNodeNetworkProps): any {
public addAgentConfigToJenkinsYaml(stack: Stack, templates: AgentNodeProps[], props: AgentNodeNetworkProps, macAgent: string): any {
gaiksaya marked this conversation as resolved.
Show resolved Hide resolved
const jenkinsYaml: any = load(readFileSync(JenkinsMainNode.BASE_JENKINS_YAML_PATH, 'utf-8'));
const configTemplates: any = [];

templates.forEach((element) => {
configTemplates.push(this.getTemplate(stack, element, props));
});
if (macAgent == 'true') {
configTemplates.push(this.getMacTemplate(stack, props));
}

const agentNodeYamlConfig = [{
amazonEC2: {
Expand Down Expand Up @@ -190,4 +193,61 @@ export class AgentNodeConfig {
useEphemeralDevices: false,
};
}

private getMacTemplate(stack: Stack, props: AgentNodeNetworkProps): { [x: string]: any; } {
return {
ami: 'ami-0379811a08268a97e',
gaiksaya marked this conversation as resolved.
Show resolved Hide resolved
amiType:
{ macData: { sshPort: '22' } },
associatePublicIp: false,
connectBySSHProcess: false,
connectionStrategy: 'PRIVATE_IP',
customDeviceMapping: '/dev/sda1=:300:true:gp3::encrypted',
deleteRootOnTermination: true,
description: 'jenkinsAgentNode-Jenkins-Agent-MacOS-x64-Mac1Metal-Multi-Host',
ebsEncryptRootVolume: 'ENCRYPTED',
ebsOptimized: true,
hostKeyVerificationStrategy: 'OFF',
iamInstanceProfile: this.AgentNodeInstanceProfileArn,
labelString: 'Jenkins-Agent-MacOS-x64-Mac1Metal-Multi-Host',
maxTotalUses: -1,
minimumNumberOfInstances: 1,
minimumNumberOfSpareInstances: 0,
mode: 'EXCLUSIVE',
monitoring: true,
numExecutors: '6',
remoteAdmin: 'ec2-user',
remoteFS: '/var/jenkins',
securityGroups: props.agentNodeSecurityGroup,
stopOnTerminate: false,
subnetId: props.subnetId,
t2Unlimited: false,
tags: [
{
name: 'Name',
value: `${stack.stackName}/AgentNode/Jenkins-Agent-MacOS-x64-Mac1Metal-Multi-Host`,
},
{
name: 'type',
value: 'jenkinsAgentNode-Jenkins-Agent-MacOS-x64-Mac1Metal-Multi-Host',
},
],
tenancy: 'Host',
type: 'Mac1Metal',
nodeProperties: [
{
envVars: {
env: [
{
key: 'Path',
/* eslint-disable max-len */
value: '/usr/local/opt/python@3.7/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/Cellar/python@3.7/3.7.13_1/Frameworks/Python.framework/Versions/3.7/bin',
},
],
},
},
],
useEphemeralDevices: false,
};
}
}
3 changes: 0 additions & 3 deletions lib/compute/agent-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
* compatible open source license.
*/

import {
AmazonLinuxCpuType, AmazonLinuxGeneration, MachineImage,
} from '@aws-cdk/aws-ec2';
prudhvigodithi marked this conversation as resolved.
Show resolved Hide resolved
import { AgentNodeProps } from './agent-node-config';

export class AgentNodes {
Expand Down
8 changes: 4 additions & 4 deletions lib/compute/jenkins-main-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class JenkinsMainNode {
foundJenkinsProcessCount: Metric
}

constructor(stack: Stack, props: JenkinsMainNodeProps, agentNode: AgentNodeProps[], assumeRole: string) {
constructor(stack: Stack, props: JenkinsMainNodeProps, agentNode: AgentNodeProps[], assumeRole: string, macAgent: string) {
prudhvigodithi marked this conversation as resolved.
Show resolved Hide resolved
this.ec2InstanceMetrics = {
cpuTime: new Metric({
metricName: 'procstat_cpu_usage',
Expand All @@ -108,7 +108,7 @@ export class JenkinsMainNode {
};

const agentNodeConfig = new AgentNodeConfig(stack, assumeRole);
const jenkinsyaml = JenkinsMainNode.addConfigtoJenkinsYaml(stack, props, props, agentNodeConfig, props, agentNode);
const jenkinsyaml = JenkinsMainNode.addConfigtoJenkinsYaml(stack, props, props, agentNodeConfig, props, agentNode, macAgent);
if (props.dataRetention) {
const efs = new FileSystem(stack, 'EFSfilesystem', {
vpc: props.vpc,
Expand Down Expand Up @@ -399,8 +399,8 @@ export class JenkinsMainNode {
}

public static addConfigtoJenkinsYaml(stack: Stack, jenkinsMainNodeProps:JenkinsMainNodeProps, oidcProps: OidcFederateProps, agentNodeObject: AgentNodeConfig,
props: AgentNodeNetworkProps, agentNode: AgentNodeProps[]): string {
let updatedConfig = agentNodeObject.addAgentConfigToJenkinsYaml(stack, agentNode, props);
props: AgentNodeNetworkProps, agentNode: AgentNodeProps[], macAgent: string): string {
let updatedConfig = agentNodeObject.addAgentConfigToJenkinsYaml(stack, agentNode, props, macAgent);
if (oidcProps.runWithOidc) {
updatedConfig = OidcConfig.addOidcConfigToJenkinsYaml(updatedConfig, oidcProps.adminUsers);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/network/ci-external-load-balancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class JenkinsExternalLoadBalancer {
const accessPort = props.useSsl ? 443 : 80;

this.listener = this.loadBalancer.addListener('JenkinsListener', {
sslPolicy: props.useSsl ? SslPolicy.TLS12 : undefined,
sslPolicy: props.useSsl ? SslPolicy.RECOMMENDED : undefined,
prudhvigodithi marked this conversation as resolved.
Show resolved Hide resolved
port: accessPort,
open: true,
certificates: props.useSsl ? [props.listenerCertificate] : undefined,
Expand Down
25 changes: 25 additions & 0 deletions test/ci-cdn-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,28 @@ test('CDN Stack Resources', () => {
}));
expect(cdnStack).to(countResources('AWS::Lambda::Function', 1));
});

test('CDN Stack Resources With mac agent', () => {
const cdnApp = new App({
context: {
useSsl: 'true', runWithOidc: 'true', additionalCommands: './test/data/hello-world.py', macAgent: true,
},
});

// WHEN
const cdnStack = new CiCdnStack(cdnApp, 'cdnTestStack', {});

// THEN
expect(cdnStack).to(countResources('AWS::IAM::Role', 2));
expect(cdnStack).to(countResources('AWS::IAM::Policy', 2));
expect(cdnStack).to(countResources('AWS::CloudFront::CloudFrontOriginAccessIdentity', 1));
expect(cdnStack).to(countResources('AWS::CloudFront::Distribution', 1));
expect(cdnStack).to(haveResourceLike('AWS::CloudFront::Distribution', {
DistributionConfig: {
DefaultCacheBehavior: {
DefaultTTL: 300,
},
},
}));
expect(cdnStack).to(countResources('AWS::Lambda::Function', 1));
});
28 changes: 27 additions & 1 deletion test/compute/agent-node-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
* compatible open source license.
*/

import { Stack, App } from '@aws-cdk/core';
import {
expect as expectCDK, haveResource, haveResourceLike, countResources,
} from '@aws-cdk/assert';
import { Stack, App } from '@aws-cdk/core';
prudhvigodithi marked this conversation as resolved.
Show resolved Hide resolved
import { readFileSync } from 'fs';
import { load } from 'js-yaml';
import { CIStack } from '../../lib/ci-stack';
import { JenkinsMainNode } from '../../lib/compute/jenkins-main-node';

test('Agents Resource is present', () => {
const app = new App({
Expand Down Expand Up @@ -120,3 +123,26 @@ test('Agents Resource is present', () => {
},
}));
});

describe('JenkinsMainNode Config with macAgent template', () => {
// WHEN
const testYaml = 'test/data/jenkins.yaml';
const yml: any = load(readFileSync(testYaml, 'utf-8'));
// THEN
test('Verify Mac template tenancy ', async () => {
const macConfig = yml.jenkins.clouds[0].amazonEC2.templates[0].tenancy;
expect(macConfig).toEqual('Host');
});
test('Verify Mac template type', async () => {
const macConfig = yml.jenkins.clouds[0].amazonEC2.templates[0].type;
expect(macConfig).toEqual('Mac1Metal');
});
test('Verify Mac template amiType.macData.sshPort', async () => {
const macConfig = yml.jenkins.clouds[0].amazonEC2.templates[0].amiType.macData.sshPort;
expect(macConfig).toEqual('22');
});
test('Verify Mac template customDeviceMapping', async () => {
const macConfig = yml.jenkins.clouds[0].amazonEC2.templates[0].customDeviceMapping;
expect(macConfig).toEqual('/dev/sda1=:300:true:gp3::encrypted');
});
});
46 changes: 46 additions & 0 deletions test/data/jenkins.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,50 @@
jenkins:
clouds:
- amazonEC2:
cloudName: "Amazon_ec2_cloud"
region: "us-east-1"
sshKeysCredentialsId: "jenkins-staging-agent-node-ssh-key"
templates:
- ami: "ami-0379811a08268a97e"
amiType:
macData:
sshPort: "22"
associatePublicIp: false
connectBySSHProcess: false
connectionStrategy: PRIVATE_IP
customDeviceMapping: "/dev/sda1=:300:true:gp3::encrypted"
deleteRootOnTermination: true
description: "Jenkins-Agent-Mac-M1-Single-Host"
ebsEncryptRootVolume: DEFAULT
ebsOptimized: true
hostKeyVerificationStrategy: 'OFF'
iamInstanceProfile: "arn:aws:iam::1234567890:instance-profile/JenkinsStack-AgentNodeInstanceRole"
labelString: 'Jenkins-Agent-MacOS-x64-Mac1Metal-Multi-Host'
maxTotalUses: -1
minimumNumberOfInstances: 1
minimumNumberOfSpareInstances: 0
mode: EXCLUSIVE
monitoring: false
nodeProperties:
- envVars:
env:
- key: "Path"
value: "/usr/local/opt/python@3.7/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/Cellar/python@3.7/3.7.13_1/Frameworks/Python.framework/Versions/3.7/bin"
numExecutors: 5
remoteAdmin: "ec2-user"
remoteFS: "/var/jenkins"
securityGroups: "jenkins-agent-node"
stopOnTerminate: false
subnetId: "subnet-1234567890"
t2Unlimited: false
tags:
- name: "Name"
value: "Jenkins-Agent-Mac-M1-Single-Host"
tenancy: Host
type: Mac1Metal
useEphemeralDevices: false
zone: "us-east-1a"
useInstanceProfileForCredentials: true
authorizationStrategy:
roleBased:
roles:
Expand Down