diff --git a/README.md b/README.md index 763d160b..b9018725 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. diff --git a/lib/ci-stack.ts b/lib/ci-stack.ts index bd3ded41..41696e87 100644 --- a/lib/ci-stack.ts +++ b/lib/ci-stack.ts @@ -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 { @@ -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') { @@ -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()); const externalLoadBalancer = new JenkinsExternalLoadBalancer(this, { vpc, diff --git a/lib/compute/agent-node-config.ts b/lib/compute/agent-node-config.ts index b74966d1..23b00dfa 100644 --- a/lib/compute/agent-node-config.ts +++ b/lib/compute/agent-node-config.ts @@ -25,7 +25,6 @@ export interface AgentNodeProps { numExecutors: number; initScript: string } - export interface AgentNodeNetworkProps { readonly agentNodeSecurityGroup: string; readonly subnetId: string; @@ -104,6 +103,7 @@ export class AgentNodeConfig { }), ); + /* eslint-disable eqeqeq */ if (assumeRole.toString() !== 'undefined') { // policy to allow assume role AssumeRole AgentNodeRole.addToPolicy( @@ -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 { 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: { @@ -190,4 +193,61 @@ export class AgentNodeConfig { useEphemeralDevices: false, }; } + + private getMacTemplate(stack: Stack, props: AgentNodeNetworkProps): { [x: string]: any; } { + return { + ami: 'ami-0379811a08268a97e', + 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, + }; + } } diff --git a/lib/compute/agent-nodes.ts b/lib/compute/agent-nodes.ts index ed830c94..f2c06521 100644 --- a/lib/compute/agent-nodes.ts +++ b/lib/compute/agent-nodes.ts @@ -6,9 +6,6 @@ * compatible open source license. */ -import { - AmazonLinuxCpuType, AmazonLinuxGeneration, MachineImage, -} from '@aws-cdk/aws-ec2'; import { AgentNodeProps } from './agent-node-config'; export class AgentNodes { diff --git a/lib/compute/jenkins-main-node.ts b/lib/compute/jenkins-main-node.ts index 7179faad..4baf0d59 100644 --- a/lib/compute/jenkins-main-node.ts +++ b/lib/compute/jenkins-main-node.ts @@ -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) { this.ec2InstanceMetrics = { cpuTime: new Metric({ metricName: 'procstat_cpu_usage', @@ -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, @@ -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); } diff --git a/lib/network/ci-external-load-balancer.ts b/lib/network/ci-external-load-balancer.ts index 4d642815..548705fb 100644 --- a/lib/network/ci-external-load-balancer.ts +++ b/lib/network/ci-external-load-balancer.ts @@ -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, port: accessPort, open: true, certificates: props.useSsl ? [props.listenerCertificate] : undefined, diff --git a/test/ci-cdn-stack.test.ts b/test/ci-cdn-stack.test.ts index abc22f52..19811118 100644 --- a/test/ci-cdn-stack.test.ts +++ b/test/ci-cdn-stack.test.ts @@ -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)); +}); diff --git a/test/compute/agent-node-config.test.ts b/test/compute/agent-node-config.test.ts index b5085c0f..119c1d23 100644 --- a/test/compute/agent-node-config.test.ts +++ b/test/compute/agent-node-config.test.ts @@ -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'; +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({ @@ -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'); + }); +}); diff --git a/test/data/jenkins.yaml b/test/data/jenkins.yaml index 662c1e64..2d1a3367 100644 --- a/test/data/jenkins.yaml +++ b/test/data/jenkins.yaml @@ -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: