diff --git a/lib/ci-config-stack-benchmark.ts b/lib/ci-config-stack-benchmark.ts new file mode 100644 index 00000000..ce4fe00d --- /dev/null +++ b/lib/ci-config-stack-benchmark.ts @@ -0,0 +1,93 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib'; +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { Construct } from 'constructs'; + +export class CIConfigStackBenchmark extends Stack { + static readonly CERTIFICATE_ARN_SECRET_EXPORT_VALUE: string = 'certificateArnSecretBenchmark'; + + static readonly CERTIFICATE_CONTENTS_SECRET_EXPORT_VALUE: string = 'certContentsSecretBenchmark'; + + static readonly CERTIFICATE_CHAIN_SECRET_EXPORT_VALUE: string = 'certChainSecretBenchmark'; + + static readonly PRIVATE_KEY_SECRET_EXPORT_VALUE: string = 'privateKeySecretBenchmark'; + + static readonly REDIRECT_URL_SECRET_EXPORT_VALUE: string = 'redirectUrlSecretBenchmark'; + + static readonly OIDC_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE: string = 'OIDCConfigValueSecretBenchmark'; + + static readonly CASC_RELOAD_TOKEN_SECRET_EXPORT_VALUE: string = 'cascBenchmark'; + + constructor(scope: Construct, id: string, props?: StackProps) { + // @ts-ignore + super(scope, id, props); + + const arnSecret = new Secret(this, 'certificateArnBenchmark', { + description: 'Certificate ARN retrieved after uploading certificate to IAM server', + }); + const certContentsSecret = new Secret(this, 'certContentsBenchmark', { + description: 'Contents of public key of the SSL certificate', + }); + const certChainSecret = new Secret(this, 'certChainBenchmark', { + description: 'Contents of the SSL certificate chain', + }); + const privateKeySecret = new Secret(this, 'privateKeyBenchmark', { + description: 'Contents of private key of the SSL certificate', + }); + const redirectUrlSecret = new Secret(this, 'redirectUrlBenchmark', { + description: 'Redirect url for Jenkins', + }); + const OIDCConfigValuesSecret = new Secret(this, 'OIDCConfigValuesBenchmark', { + description: 'OIDC params in JSON format', + }); + const CascReloadTokenValuesSecret = new Secret(this, 'CascReloadTokenValueBenchmark', { + description: 'Reload token (password) required for configuration as code plugin', + generateSecretString: { + excludeCharacters: '#$_!"%&\'()*+,./:;<=>?@[\\]^`{|}~', + passwordLength: 5, + }, + }); + + new CfnOutput(this, 'certificateArnSecretBenchmark', { + value: arnSecret.secretArn, + exportName: CIConfigStackBenchmark.CERTIFICATE_ARN_SECRET_EXPORT_VALUE, + }); + + new CfnOutput(this, 'certContentsSecretBenchmark', { + value: certContentsSecret.secretArn, + exportName: CIConfigStackBenchmark.CERTIFICATE_CONTENTS_SECRET_EXPORT_VALUE, + }); + + new CfnOutput(this, 'certChainSecretBenchmark', { + value: certChainSecret.secretArn, + exportName: CIConfigStackBenchmark.CERTIFICATE_CHAIN_SECRET_EXPORT_VALUE, + }); + + new CfnOutput(this, 'privateKeySecretBenchmark', { + value: privateKeySecret.secretArn, + exportName: CIConfigStackBenchmark.PRIVATE_KEY_SECRET_EXPORT_VALUE, + }); + + new CfnOutput(this, 'redirectUrlSecretBenchmark', { + value: redirectUrlSecret.secretArn, + exportName: CIConfigStackBenchmark.REDIRECT_URL_SECRET_EXPORT_VALUE, + }); + + new CfnOutput(this, 'OIDCConfigValuesSecretBenchmark', { + value: OIDCConfigValuesSecret.secretArn, + exportName: CIConfigStackBenchmark.OIDC_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE, + }); + + new CfnOutput(this, 'cascSecretValueBenchmark', { + value: CascReloadTokenValuesSecret.secretArn, + exportName: CIConfigStackBenchmark.CASC_RELOAD_TOKEN_SECRET_EXPORT_VALUE, + }); + } +} diff --git a/lib/ci-stack-benchmark.ts b/lib/ci-stack-benchmark.ts new file mode 100644 index 00000000..dc73fa19 --- /dev/null +++ b/lib/ci-stack-benchmark.ts @@ -0,0 +1,245 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { CfnOutput, CfnParameter, Fn, Stack, StackProps } from 'aws-cdk-lib'; +import { + IPeer, Peer, Vpc, +} from 'aws-cdk-lib/aws-ec2'; +import { + ApplicationTargetGroup, + ListenerCertificate, Protocol, ListenerCondition, ApplicationListener, +} from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { Construct } from 'constructs'; +import { CIConfigStackBenchmark } from './ci-config-stack-benchmark'; +import { AgentNodeProps } from './compute/agent-node-config'; +import { AgentNodesBenchmark } from './compute/agent-nodes-benchmark'; +import { JenkinsMainNodeBenchmark } from './compute/jenkins-main-node-benchmark'; +import { RunAdditionalCommands } from './compute/run-additional-commands'; +import { JenkinsMonitoringBenchmark } from './monitoring/ci-alarms-benchmark'; +import { JenkinsSecurityGroups } from './security/ci-security-groups'; + +export interface CIStackPropsBenchmark extends StackProps { + /** Should the Jenkins use https */ + readonly useSsl?: boolean; + /** Should an OIDC provider be installed on Jenkins. */ + readonly runWithOidc?: boolean; + /** Restrict jenkins access to */ + readonly restrictServerAccessTo?: IPeer; + /** Additional verification during deployment and resource startup. */ + readonly ignoreResourcesFailures?: boolean; + /** Users with admin access during initial deployment */ + readonly adminUsers?: string[]; + /** Additional logic that needs to be run on Master Node. The value has to be path to a file */ + readonly additionalCommands?: string; + /** Do you want to retain jenkins jobs and build history */ + readonly dataRetention?: boolean; + /** IAM role ARN to be assumed by jenkins agent nodes eg: cross-account */ + 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; + /** Enable views on jenkins UI */ + readonly enableViews?: boolean; + /** Use Production Agents */ + readonly useProdAgents?: boolean; +} + +function getServerAccess(serverAccessType: string, restrictServerAccessTo: string) : IPeer { + if (typeof restrictServerAccessTo === 'undefined') { + throw new Error('restrictServerAccessTo should be specified'); + } + switch (serverAccessType) { + case 'ipv4': + return restrictServerAccessTo === 'all' ? Peer.anyIpv4() : Peer.ipv4(restrictServerAccessTo); + case 'ipv6': + return restrictServerAccessTo === 'all' ? Peer.anyIpv6() : Peer.ipv6(restrictServerAccessTo); + case 'prefixList': + return Peer.prefixList(restrictServerAccessTo); + case 'securityGroupId': + return Peer.securityGroupId(restrictServerAccessTo); + default: + throw new Error('serverAccessType should be one of the below values: ipv4, ipv6, prefixList or securityGroupId'); + } +} + +export class CIStackBenchmark extends Stack { + public readonly monitoring: JenkinsMonitoringBenchmark; + + public readonly agentNodesBenchmark: AgentNodeProps[]; + + public readonly securityGroups: JenkinsSecurityGroups; + + constructor(scope: Construct, id: string, props: CIStackPropsBenchmark) { + // @ts-ignore + super(scope, id, props); + const accessPort = props.useSsl ? 443 : 80; + // @ts-ignore + const vpcId = Fn.importValue('CIstackVPCId'); + const vpc = Vpc.fromVpcAttributes(this, 'VPC', { + availabilityZones: ['us-east-1a', 'us-east-1b', 'us-east-1c'], + vpcCidrBlock: '10.0.0.0/16', + privateSubnetIds: [ + Fn.importValue('CIstackVPCPrivateSubnets-0'), + Fn.importValue('CIstackVPCPrivateSubnets-1'), + Fn.importValue('CIstackVPCPrivateSubnets-2'), + ], + publicSubnetIds: [ + Fn.importValue('CIstackVPCPublicSubnets-0'), + Fn.importValue('CIstackVPCPublicSubnets-1'), + Fn.importValue('CIstackVPCPublicSubnets-2'), + ], + vpcId, + }); + + const listenerArn = Fn.importValue('ALBListenerArn'); + + const macAgentParameter = `${props?.macAgent ?? this.node.tryGetContext('macAgent')}`; + + const useSslParameter = `${props?.useSsl ?? this.node.tryGetContext('useSsl')}`; + if (useSslParameter !== 'true' && useSslParameter !== 'false') { + throw new Error('useSsl parameter is required to be set as - true or false'); + } + + const useSsl = useSslParameter === 'true'; + + const runWithOidcParameter = `${props?.runWithOidc ?? this.node.tryGetContext('runWithOidc')}`; + if (runWithOidcParameter !== 'true' && runWithOidcParameter !== 'false') { + throw new Error('runWithOidc parameter is required to be set as - true or false'); + } + + let useProdAgents = `${props?.useProdAgents ?? this.node.tryGetContext('useProdAgents')}`; + if (useProdAgents.toString() === 'undefined') { + useProdAgents = 'false'; + } + + const runWithOidc = runWithOidcParameter === 'true'; + + const serverAccessType = this.node.tryGetContext('serverAccessType'); + const restrictServerAccessTo = this.node.tryGetContext('restrictServerAccessTo'); + const serverAcess = props?.restrictServerAccessTo ?? getServerAccess(serverAccessType, restrictServerAccessTo); + if (!serverAcess) { + throw new Error('serverAccessType and restrictServerAccessTo parameters are required - eg: serverAccessType=ipv4 restrictServerAccessTo=10.10.10.10/32'); + } + + const additionalCommandsContext = `${props?.additionalCommands ?? this.node.tryGetContext('additionalCommands')}`; + + // Setting CfnParameters to record the value in cloudFormation + new CfnParameter(this, 'runWithOidc', { + description: 'If the jenkins instance should use OIDC + federate', + default: runWithOidc, + }); + + // Setting CfnParameters to record the value in cloudFormation + new CfnParameter(this, 'useSsl', { + description: 'If the jenkins instance should be access via SSL', + default: useSsl, + }); + + this.securityGroups = new JenkinsSecurityGroups(this, vpc, useSsl, serverAcess, 'benchmarkCI'); + const importedContentsSecretBucketValue = Fn.importValue(`${CIConfigStackBenchmark.CERTIFICATE_CONTENTS_SECRET_EXPORT_VALUE}`); + const importedContentsChainBucketValue = Fn.importValue(`${CIConfigStackBenchmark.CERTIFICATE_CHAIN_SECRET_EXPORT_VALUE}`); + const importedCertSecretBucketValue = Fn.importValue(`${CIConfigStackBenchmark.PRIVATE_KEY_SECRET_EXPORT_VALUE}`); + const importedArnSecretBucketValue = Fn.importValue(`${CIConfigStackBenchmark.CERTIFICATE_ARN_SECRET_EXPORT_VALUE}`); + const importedRedirectUrlSecretBucketValue = Fn.importValue(`${CIConfigStackBenchmark.REDIRECT_URL_SECRET_EXPORT_VALUE}`); + const importedOidcConfigValuesSecretBucketValue = Fn.importValue(`${CIConfigStackBenchmark.OIDC_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE}`); + const certificateArn = Secret.fromSecretCompleteArn(this, 'certificateArn', importedArnSecretBucketValue.toString()); + const importedReloadPasswordSecretsArn = Fn.importValue(`${CIConfigStackBenchmark.CASC_RELOAD_TOKEN_SECRET_EXPORT_VALUE}`); + const listenerCertificate = ListenerCertificate.fromArn(certificateArn.secretValue.toString()); + + // @ts-ignore + const agentNodeBenchmark = new AgentNodesBenchmark(this); + if (useProdAgents.toString() === 'true') { + // eslint-disable-next-line no-console + console.warn('Please note that if you have decided to use the provided production jenkins agents then ' + + 'please make sure that you are deploying the stack in US-EAST-1 region as the AMIs used are only publicly ' + + 'available in US-EAST-1 region. ' + + 'If you want to deploy the stack in another region then please make sure you copy the public AMIs used ' + + 'from us-east-1 region to your region of choice and update the ami-id in agent-nodes.ts file accordingly. ' + + 'If you do not copy the AMI in required region and update the code then the jenkins agents will not spin up.'); + + this.agentNodesBenchmark = [ + agentNodeBenchmark.AL2023_X64, + agentNodeBenchmark.AL2_X64_DOCKER_HOST, + agentNodeBenchmark.AL2023_X64_DOCKER_HOST, + agentNodeBenchmark.AL2023_ARM64, + agentNodeBenchmark.AL2_ARM64_DOCKER_HOST, + agentNodeBenchmark.AL2023_ARM64_DOCKER_HOST, + agentNodeBenchmark.AL2023_X64_BENCHMARK_TEST, + agentNodeBenchmark.UBUNTU2004_X64_GRADLE_CHECK, + agentNodeBenchmark.UBUNTU2004_X64_DOCKER_BUILDER, + agentNodeBenchmark.MACOS12_X64_MULTI_HOST, + agentNodeBenchmark.WINDOWS2019_X64_DOCKER_HOST, + agentNodeBenchmark.WINDOWS2019_X64_DOCKER_BUILDER, + agentNodeBenchmark.WINDOWS2019_X64_GRADLE_CHECK, + ]; + } else { + this.agentNodesBenchmark = [agentNodeBenchmark.AL2_X64_DEFAULT_AGENT, agentNodeBenchmark.AL2_ARM64_DEFAULT_AGENT]; + } + + // @ts-ignore + const mainJenkinsNodeBenchmark = new JenkinsMainNodeBenchmark(this, { + vpc, + sg: this.securityGroups.mainNodeSG, + efsSG: this.securityGroups.efsSG, + dataRetention: props.dataRetention ?? false, + envVarsFilePath: props.envVarsFilePath ?? '', + enableViews: props.enableViews ?? false, + reloadPasswordSecretsArn: importedReloadPasswordSecretsArn.toString(), + sslCertContentsArn: importedContentsSecretBucketValue.toString(), + sslCertChainArn: importedContentsChainBucketValue.toString(), + sslCertPrivateKeyContentsArn: importedCertSecretBucketValue.toString(), + redirectUrlArn: importedRedirectUrlSecretBucketValue.toString(), + oidcCredArn: importedOidcConfigValuesSecretBucketValue.toString(), + useSsl, + runWithOidc, + failOnCloudInitError: props?.ignoreResourcesFailures, + adminUsers: props?.adminUsers, + agentNodeSecurityGroup: this.securityGroups.agentNodeSG.securityGroupId, + subnetId: vpc.privateSubnets[0].subnetId, + }, this.agentNodesBenchmark, macAgentParameter.toString(), props?.agentAssumeRole); + + const targetGroupBenchmark = new ApplicationTargetGroup(this, 'MainJenkinsNodeTargetBenchmark', { + port: accessPort, + vpc, + targets: [mainJenkinsNodeBenchmark.mainNodeBenchAsg], + healthCheck: { + protocol: props.useSsl ? Protocol.HTTPS : Protocol.HTTP, + path: '/benchmark/', + }, + }); + + const existingListener = ApplicationListener.fromApplicationListenerAttributes(this, 'ALB listener', { + listenerArn, + securityGroup: this.securityGroups.mainNodeSG, + }); + + existingListener.addTargetGroups('targetGroupBenchmark', { + priority: 1, + conditions: [ListenerCondition.pathPatterns(['/benchmark*'])], + targetGroups: [targetGroupBenchmark], + }); + + const artifactBucket = new Bucket(this, 'BuildBucketBenchmark'); + + // @ts-ignore + this.monitoring = new JenkinsMonitoringBenchmark(this, mainJenkinsNodeBenchmark); + + if (additionalCommandsContext.toString() !== 'undefined') { + // @ts-ignore + new RunAdditionalCommands(this, additionalCommandsContext.toString()); + } + + new CfnOutput(this, 'Artifact Bucket Arn Benchmark', { + value: artifactBucket.bucketArn.toString(), + exportName: 'buildBucketArnBenchmark', + }); + } +} diff --git a/lib/ci-stack.ts b/lib/ci-stack.ts index be53b17b..2a9fdd52 100644 --- a/lib/ci-stack.ts +++ b/lib/ci-stack.ts @@ -132,7 +132,7 @@ export class CIStack extends Stack { default: useSsl, }); - this.securityGroups = new JenkinsSecurityGroups(this, vpc, useSsl, serverAcess); + this.securityGroups = new JenkinsSecurityGroups(this, vpc, useSsl, serverAcess, 'mainCI'); const importedContentsSecretBucketValue = Fn.importValue(`${CIConfigStack.CERTIFICATE_CONTENTS_SECRET_EXPORT_VALUE}`); const importedContentsChainBucketValue = Fn.importValue(`${CIConfigStack.CERTIFICATE_CHAIN_SECRET_EXPORT_VALUE}`); const importedCertSecretBucketValue = Fn.importValue(`${CIConfigStack.PRIVATE_KEY_SECRET_EXPORT_VALUE}`); @@ -213,5 +213,32 @@ export class CIStack extends Stack { value: artifactBucket.bucketArn.toString(), exportName: 'buildBucketArn', }); + + new CfnOutput(this, 'ALB SG Id', { + value: this.securityGroups.externalAccessSG.securityGroupId, + exportName: 'ALBSGId', + }); + + vpc.publicSubnets.map((subnet) => subnet.subnetId).map((idstring, index) => new CfnOutput(this, `PUBLIC_SUBNET_PREFIX-${index}`, { + description: `subnet id for public subnet ${index}`, + value: idstring, + exportName: `CIstackVPCPublicSubnets-${index}`, + })); + + vpc.privateSubnets.map((subnet) => subnet.subnetId).map((idstring, index) => new CfnOutput(this, `PRIVATE_SUBNET_PREFIX-${index}`, { + description: `subnet id for private subnet ${index}`, + value: idstring, + exportName: `CIstackVPCPrivateSubnets-${index}`, + })); + + new CfnOutput(this, 'VPC Id', { + value: vpc.vpcId, + exportName: 'CIstackVPCId', + }); + + new CfnOutput(this, 'mainCI mainNode SG Id', { + value: this.securityGroups.mainNodeSG.securityGroupId, + exportName: 'CIStackMainNodeSGId', + }); } } diff --git a/lib/compute/agent-node-config-benchmark.ts b/lib/compute/agent-node-config-benchmark.ts new file mode 100644 index 00000000..e4e0099b --- /dev/null +++ b/lib/compute/agent-node-config-benchmark.ts @@ -0,0 +1,371 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { + CfnOutput, Fn, Stack, Tags, + } from 'aws-cdk-lib'; + import { + CfnInstanceProfile, Effect, ManagedPolicy, PolicyStatement, Role, ServicePrincipal, + } from 'aws-cdk-lib/aws-iam'; + import { KeyPair } from 'cdk-ec2-key-pair'; + import { readFileSync } from 'fs'; + import { load } from 'js-yaml'; + import { JenkinsMainNodeBenchmark } from './jenkins-main-node-benchmark'; + + export interface AgentNodePropsBenchmark { + agentType: string; + amiId: string; + instanceType: string; + customDeviceMapping: string; + workerLabelString: string; + remoteUser: string; + maxTotalUses: number; + minimumNumberOfSpareInstances: number; + numExecutors: number; + initScript: string; + remoteFs: string; + } + export interface AgentNodeNetworkPropsBenchmark { + readonly agentNodeSecurityGroup: string; + readonly subnetId: string; + } + + export class AgentNodeConfigBenchmark { + public readonly AgentNodeInstanceProfileArn: string; + + public readonly STACKREGION: string; + + public readonly ACCOUNT: string; + + public readonly SSHEC2KeySecretId: string; + + constructor(stack: Stack, assumeRole?: string[]) { + this.STACKREGION = stack.region; + this.ACCOUNT = stack.account; + + const key = new KeyPair(stack, 'AgentNode-KeyPair-Benchmark', { + name: 'AgentNodeKeyPairBenchmark', + description: 'KeyPair used by Jenkins Main Node to SSH into Agent Nodes', + }); + Tags.of(key) + .add('jenkins:credentials:type', 'sshUserPrivateKey'); + const AgentNodeRole = new Role(stack, 'OpenSearch-CI-AgentNodeRole-Benchmark', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com'), + // assumedBy: new AccountPrincipal(this.ACCOUNT), + description: 'Jenkins agents Node Role', + roleName: 'OpenSearch-CI-AgentNodeRole-Benchmark', + }); + + const ecrManagedPolicy = new ManagedPolicy(stack, 'OpenSearch-CI-AgentNodePolicy-Benchmark', { + description: 'Jenkins agents Node Policy', + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'ecr-public:BatchCheckLayerAvailability', + 'ecr-public:GetRepositoryPolicy', + 'ecr-public:DescribeRepositories', + 'ecr-public:DescribeRegistries', + 'ecr-public:DescribeImages', + 'ecr-public:DescribeImageTags', + 'ecr-public:GetRepositoryCatalogData', + 'ecr-public:GetRegistryCatalogData', + 'ecr-public:InitiateLayerUpload', + 'ecr-public:UploadLayerPart', + 'ecr-public:CompleteLayerUpload', + 'ecr-public:PutImage', + ], + resources: [`arn:aws:ecr-public::${this.ACCOUNT}:repository/*`], + conditions: { + StringEquals: { + 'aws:RequestedRegion': this.STACKREGION, + 'aws:PrincipalAccount': [this.ACCOUNT], + }, + }, + }), + ], + roles: [AgentNodeRole], + }); + ecrManagedPolicy.addStatements( + new PolicyStatement({ + actions: [ + 'ecr-public:GetAuthorizationToken', + 'sts:GetServiceBearerToken', + ], + resources: ['*'], + conditions: { + StringEquals: { + 'aws:RequestedRegion': this.STACKREGION, + 'aws:PrincipalAccount': [this.ACCOUNT], + }, + }, + }), + ); + + /* eslint-disable eqeqeq */ + if (assumeRole) { + // policy to allow assume role AssumeRole + AgentNodeRole.addToPolicy( + new PolicyStatement({ + resources: assumeRole, + actions: ['sts:AssumeRole'], + }), + ); + } + AgentNodeRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')); + const AgentNodeInstanceProfile = new CfnInstanceProfile(stack, 'JenkinsAgentNodeInstanceProfileBenchmark', { roles: [AgentNodeRole.roleName] }); + this.AgentNodeInstanceProfileArn = AgentNodeInstanceProfile.attrArn.toString(); + this.SSHEC2KeySecretId = Fn.join('/', ['ec2-ssh-key', key.keyPairName.toString(), 'private']); + + new CfnOutput(stack, 'Jenkins Agent Node Role Arn Benchmark', { + value: `${AgentNodeRole.roleArn}`, + exportName: 'agentNodeRoleArnBenchmark', + }); + } + + public addAgentConfigToJenkinsYaml(stack: Stack, templates: AgentNodePropsBenchmark[], props: AgentNodeNetworkPropsBenchmark, macAgent: string): any { + const jenkinsYaml: any = load(readFileSync(JenkinsMainNodeBenchmark.BASE_JENKINS_YAML_PATH, 'utf-8')); + const configTemplates: any = []; + + templates.forEach((element) => { + if (element.agentType == 'unix') { + configTemplates.push(this.getUnixTemplate(stack, element, props)); + } else if (element.agentType == 'mac' && macAgent == 'true') { + configTemplates.push(this.getMacTemplate(stack, element, props)); + } else if (element.agentType == 'windows') { + configTemplates.push(this.getWindowsTemplate(stack, element, props)); + } + }); + + const agentNodeYamlConfig = [{ + amazonEC2: { + cloudName: 'Amazon_ec2_cloud', + region: this.STACKREGION, + sshKeysCredentialsId: this.SSHEC2KeySecretId, + templates: configTemplates, + useInstanceProfileForCredentials: true, + }, + }]; + jenkinsYaml.jenkins.clouds = agentNodeYamlConfig; + return jenkinsYaml; + } + + private getUnixTemplate(stack: Stack, config: AgentNodePropsBenchmark, props: AgentNodeNetworkPropsBenchmark): { [x: string]: any; } { + return { + ami: config.amiId, + amiType: + { unixData: { sshPort: '22' } }, + associatePublicIp: false, + connectBySSHProcess: false, + connectionStrategy: 'PRIVATE_IP', + customDeviceMapping: config.customDeviceMapping, + deleteRootOnTermination: true, + description: `jenkinsAgentNode-${config.workerLabelString}`, + ebsEncryptRootVolume: 'ENCRYPTED', + ebsOptimized: false, + metadataTokensRequired: true, + metadataHopsLimit: '2', + hostKeyVerificationStrategy: 'OFF', + iamInstanceProfile: this.AgentNodeInstanceProfileArn, + idleTerminationMinutes: '60', + initScript: config.initScript, + labelString: config.workerLabelString, + launchTimeoutStr: '300', + maxTotalUses: config.maxTotalUses, + minimumNumberOfInstances: 0, + minimumNumberOfSpareInstances: config.minimumNumberOfSpareInstances, + mode: 'EXCLUSIVE', + monitoring: true, + numExecutors: config.numExecutors, + remoteAdmin: config.remoteUser, + remoteFS: config.remoteFs, + securityGroups: props.agentNodeSecurityGroup, + stopOnTerminate: false, + subnetId: props.subnetId, + t2Unlimited: false, + tags: [{ + name: 'Name', + value: `${stack.stackName}/AgentNode/${config.workerLabelString}`, + }, + { + name: 'type', + value: `jenkinsAgentNode-${config.workerLabelString}`, + }, + ], + tenancy: 'Default', + type: config.instanceType, + nodeProperties: [ + { + envVars: { + env: [ + { + key: 'JENKINS_HOME_PATH', + value: config.remoteFs, + }, + { + key: 'JAVA8_HOME', + value: '/usr/lib/jvm/temurin-8-jdk-amd64', + }, + { + key: 'JAVA11_HOME', + value: '/usr/lib/jvm/temurin-11-jdk-amd64', + }, + { + key: 'JAVA14_HOME', + value: '/usr/lib/jvm/adoptopenjdk-14-amd64', + }, + { + key: 'JAVA17_HOME', + value: '/usr/lib/jvm/temurin-17-jdk-amd64', + }, + { + key: 'JAVA19_HOME', + value: '/usr/lib/jvm/temurin-19-jdk-amd64', + }, + { + key: 'JAVA20_HOME', + value: '/usr/lib/jvm/temurin-20-jdk-amd64', + }, + { + key: 'JAVA21_HOME', + value: '/usr/lib/jvm/temurin-21-jdk-amd64', + }, + ], + }, + }, + ], + useEphemeralDevices: false, + }; + } + + private getMacTemplate(stack: Stack, config: AgentNodePropsBenchmark, props: AgentNodeNetworkPropsBenchmark): { [x: string]: any; } { + return { + ami: config.amiId, + amiType: + { macData: { sshPort: '22' } }, + associatePublicIp: false, + connectBySSHProcess: false, + connectionStrategy: 'PRIVATE_IP', + customDeviceMapping: config.customDeviceMapping, + deleteRootOnTermination: true, + description: `jenkinsAgentNode-${config.workerLabelString}`, + ebsEncryptRootVolume: 'ENCRYPTED', + ebsOptimized: true, + metadataTokensRequired: true, + metadataHopsLimit: '2', + hostKeyVerificationStrategy: 'OFF', + iamInstanceProfile: this.AgentNodeInstanceProfileArn, + idleTerminationMinutes: '720', + labelString: config.workerLabelString, + launchTimeoutStr: '1000', + initScript: config.initScript, + maxTotalUses: config.maxTotalUses, + minimumNumberOfInstances: 1, + minimumNumberOfSpareInstances: config.minimumNumberOfSpareInstances, + mode: 'EXCLUSIVE', + monitoring: true, + numExecutors: config.numExecutors, + remoteAdmin: config.remoteUser, + remoteFS: config.remoteFs, + securityGroups: props.agentNodeSecurityGroup, + stopOnTerminate: false, + subnetId: props.subnetId, + t2Unlimited: false, + tags: [ + { + name: 'Name', + value: `${stack.stackName}/AgentNode/${config.workerLabelString}`, + }, + { + name: 'type', + value: `jenkinsAgentNode-${config.workerLabelString}`, + }, + ], + tenancy: 'Host', + type: config.instanceType, + nodeProperties: [ + { + envVars: { + env: [ + { + key: 'JENKINS_HOME_PATH', + value: config.remoteFs, + }, + ], + }, + }, + ], + useEphemeralDevices: false, + }; + } + + private getWindowsTemplate(stack: Stack, config: AgentNodePropsBenchmark, props: AgentNodeNetworkPropsBenchmark): { [x: string]: any; } { + return { + ami: config.amiId, + amiType: + { + windowsData: { + allowSelfSignedCertificate: false, bootDelay: '7.5', specifyPassword: false, useHTTPS: false, + }, + }, + associatePublicIp: false, + connectBySSHProcess: false, + connectionStrategy: 'PRIVATE_IP', + customDeviceMapping: config.customDeviceMapping, + deleteRootOnTermination: true, + description: `jenkinsAgentNode-${config.workerLabelString}`, + ebsEncryptRootVolume: 'ENCRYPTED', + ebsOptimized: true, + metadataTokensRequired: true, + metadataHopsLimit: '2', + hostKeyVerificationStrategy: 'OFF', + iamInstanceProfile: this.AgentNodeInstanceProfileArn, + idleTerminationMinutes: '120', + initScript: config.initScript, + labelString: config.workerLabelString, + launchTimeoutStr: '1200', + maxTotalUses: config.maxTotalUses, + minimumNumberOfInstances: 0, + minimumNumberOfSpareInstances: config.minimumNumberOfSpareInstances, + mode: 'EXCLUSIVE', + monitoring: true, + numExecutors: config.numExecutors, + remoteAdmin: config.remoteUser, + remoteFS: config.remoteFs, + securityGroups: props.agentNodeSecurityGroup, + stopOnTerminate: false, + subnetId: props.subnetId, + t2Unlimited: false, + tags: [{ + name: 'Name', + value: `${stack.stackName}/AgentNode/${config.workerLabelString}`, + }, + { + name: 'type', + value: `jenkinsAgentNode-${config.workerLabelString}`, + }, + ], + tenancy: 'Default', + type: config.instanceType, + nodeProperties: [ + { + envVars: { + env: [ + { + key: 'JENKINS_HOME_PATH', + value: config.remoteFs, + }, + ], + }, + }, + ], + useEphemeralDevices: false, + }; + } + } + \ No newline at end of file diff --git a/lib/compute/agent-nodes-benchmark.ts b/lib/compute/agent-nodes-benchmark.ts new file mode 100644 index 00000000..30b4353b --- /dev/null +++ b/lib/compute/agent-nodes-benchmark.ts @@ -0,0 +1,269 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { Stack } from 'aws-cdk-lib'; +import { AmazonLinuxCpuType, AmazonLinuxGeneration, MachineImage } from 'aws-cdk-lib/aws-ec2'; +import { AgentNodePropsBenchmark } from './agent-node-config-benchmark'; + +export class AgentNodesBenchmark { + // Refer: https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/ec2/model/InstanceType.html for instance types + readonly AL2023_X64: AgentNodePropsBenchmark; + + readonly AL2_X64_DOCKER_HOST: AgentNodePropsBenchmark; + + readonly AL2023_X64_DOCKER_HOST: AgentNodePropsBenchmark; + + readonly AL2023_ARM64: AgentNodePropsBenchmark; + + readonly AL2_ARM64_DOCKER_HOST: AgentNodePropsBenchmark; + + readonly AL2023_ARM64_DOCKER_HOST: AgentNodePropsBenchmark; + + readonly AL2023_X64_BENCHMARK_TEST: AgentNodePropsBenchmark; + + readonly UBUNTU2004_X64_GRADLE_CHECK: AgentNodePropsBenchmark; + + readonly UBUNTU2004_X64_DOCKER_BUILDER: AgentNodePropsBenchmark; + + readonly MACOS12_X64_MULTI_HOST: AgentNodePropsBenchmark; + + readonly WINDOWS2019_X64_DOCKER_HOST: AgentNodePropsBenchmark; + + readonly WINDOWS2019_X64_DOCKER_BUILDER: AgentNodePropsBenchmark; + + readonly WINDOWS2019_X64_GRADLE_CHECK: AgentNodePropsBenchmark; + + readonly AL2_X64_DEFAULT_AGENT: AgentNodePropsBenchmark; + + readonly AL2_ARM64_DEFAULT_AGENT: AgentNodePropsBenchmark; + + constructor(stack: Stack) { + this.AL2023_X64 = { + agentType: 'unix', + customDeviceMapping: '/dev/xvda=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-AL2023-X64-C54xlarge-Single-Host', + instanceType: 'C54xlarge', + remoteUser: 'ec2-user', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 1, + numExecutors: 1, + amiId: 'ami-0d09563cd5663bdc7', + initScript: 'sudo dnf clean all && sudo rm -rf /var/cache/dnf && sudo dnf repolist &&' + + ' sudo dnf update --releasever=latest --skip-broken --exclude=openssh* --exclude=docker* --exclude=gh* --exclude=python* -y && docker ps', + remoteFs: '/var/jenkins', + }; + this.AL2_X64_DOCKER_HOST = { + agentType: 'unix', + customDeviceMapping: '/dev/xvda=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-AL2-X64-C54xlarge-Docker-Host', + instanceType: 'C54xlarge', + remoteUser: 'ec2-user', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 1, + numExecutors: 4, + amiId: 'ami-047328312ef36d12b', + initScript: 'sudo yum clean all && sudo rm -rf /var/cache/yum /var/lib/yum/history && sudo yum repolist &&' + + ' sudo yum update --skip-broken --exclude=openssh* --exclude=docker* --exclude=gh* -y && docker ps', + remoteFs: '/var/jenkins', + }; + this.AL2023_X64_DOCKER_HOST = { + agentType: 'unix', + customDeviceMapping: '/dev/xvda=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-AL2023-X64-C54xlarge-Docker-Host', + instanceType: 'C54xlarge', + remoteUser: 'ec2-user', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 4, + numExecutors: 4, + amiId: 'ami-0d09563cd5663bdc7', + initScript: 'sudo dnf clean all && sudo rm -rf /var/cache/dnf && sudo dnf repolist &&' + + ' sudo dnf update --releasever=latest --skip-broken --exclude=openssh* --exclude=docker* --exclude=gh* --exclude=python* -y && docker ps', + remoteFs: '/var/jenkins', + }; + this.AL2023_ARM64 = { + agentType: 'unix', + customDeviceMapping: '/dev/xvda=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-AL2023-Arm64-C6g4xlarge-Single-Host', + instanceType: 'C6g4xlarge', + remoteUser: 'ec2-user', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 1, + numExecutors: 1, + amiId: 'ami-0444fd195657f193f', + initScript: 'sudo dnf clean all && sudo rm -rf /var/cache/dnf && sudo dnf repolist &&' + + ' sudo dnf update --releasever=latest --skip-broken --exclude=openssh* --exclude=docker* --exclude=gh* --exclude=python* -y && docker ps', + remoteFs: '/var/jenkins', + }; + this.AL2_ARM64_DOCKER_HOST = { + agentType: 'unix', + customDeviceMapping: '/dev/xvda=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-AL2-Arm64-C6g4xlarge-Docker-Host', + instanceType: 'C6g4xlarge', + remoteUser: 'ec2-user', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 1, + numExecutors: 4, + amiId: 'ami-06ba4c81e8dd7ab49', + initScript: 'sudo yum clean all && sudo rm -rf /var/cache/yum /var/lib/yum/history && sudo yum repolist &&' + + ' sudo yum update --skip-broken --exclude=openssh* --exclude=docker* --exclude=gh* -y && docker ps', + remoteFs: '/var/jenkins', + }; + this.AL2023_ARM64_DOCKER_HOST = { + agentType: 'unix', + customDeviceMapping: '/dev/xvda=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-AL2023-Arm64-C6g4xlarge-Docker-Host', + instanceType: 'C6g4xlarge', + remoteUser: 'ec2-user', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 4, + numExecutors: 4, + amiId: 'ami-0444fd195657f193f', + initScript: 'sudo dnf clean all && sudo rm -rf /var/cache/dnf && sudo dnf repolist &&' + + ' sudo dnf update --releasever=latest --skip-broken --exclude=openssh* --exclude=docker* --exclude=gh* --exclude=python* -y && docker ps', + remoteFs: '/var/jenkins', + }; + this.AL2023_X64_BENCHMARK_TEST = { + agentType: 'unix', + customDeviceMapping: '/dev/xvda=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-AL2023-X64-M52xlarge-Benchmark-Test', + instanceType: 'M52xlarge', + remoteUser: 'ec2-user', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 1, + numExecutors: 2, + amiId: 'ami-0d09563cd5663bdc7', + initScript: 'sudo dnf clean all && sudo rm -rf /var/cache/dnf && sudo dnf repolist &&' + + ' sudo dnf update --releasever=latest --skip-broken --exclude=openssh* --exclude=docker* --exclude=gh* --exclude=python* -y && docker ps', + remoteFs: '/var/jenkins', + }; + this.UBUNTU2004_X64_GRADLE_CHECK = { + agentType: 'unix', + customDeviceMapping: '/dev/sda1=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-Ubuntu2004-X64-M58xlarge-Single-Host', + instanceType: 'M58xlarge', + remoteUser: 'ubuntu', + maxTotalUses: 1, + minimumNumberOfSpareInstances: 1, + numExecutors: 1, + amiId: 'ami-0bbfc7f398eefe0e8', + initScript: 'sudo apt-mark hold docker docker.io openssh-server gh grub-efi* shim-signed && docker ps &&' + + ' sudo apt-get update -y && (sudo killall -9 apt-get apt 2>&1 || echo) && sudo env "DEBIAN_FRONTEND=noninteractive" apt-get upgrade -y', + remoteFs: '/var/jenkins', + }; + this.UBUNTU2004_X64_DOCKER_BUILDER = { + agentType: 'unix', + customDeviceMapping: '/dev/sda1=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-Ubuntu2004-X64-M52xlarge-Docker-Builder', + instanceType: 'M52xlarge', + remoteUser: 'ubuntu', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 2, + numExecutors: 1, + amiId: 'ami-0bbfc7f398eefe0e8', + initScript: 'sudo apt-mark hold docker docker.io openssh-server gh grub-efi* shim-signed && docker ps &&' + + ' sudo apt-get update -y && (sudo killall -9 apt-get apt 2>&1 || echo) && sudo env "DEBIAN_FRONTEND=noninteractive" apt-get upgrade -y', + remoteFs: '/var/jenkins', + }; + this.MACOS12_X64_MULTI_HOST = { + agentType: 'mac', + customDeviceMapping: '/dev/sda1=:300:true:gp3::encrypted', + workerLabelString: 'Jenkins-Agent-MacOS12-X64-Mac1Metal-Multi-Host', + instanceType: 'Mac1Metal', + remoteUser: 'ec2-user', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 1, + numExecutors: 6, + amiId: 'ami-0a5f5363c1db8ff67', + initScript: 'echo', + remoteFs: '/var/jenkins', + }; + this.WINDOWS2019_X64_DOCKER_HOST = { + agentType: 'windows', + customDeviceMapping: '/dev/sda1=:600:true:::encrypted', + workerLabelString: 'Jenkins-Agent-Windows2019-X64-C54xlarge-Docker-Host', + instanceType: 'C54xlarge', + remoteUser: 'Administrator', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 4, + numExecutors: 4, + amiId: 'ami-0a9759da263ce9304', + initScript: 'echo %USERNAME% && dockerd --register-service && net start docker && echo started docker deamon && docker ps && ' + + 'echo initializing docker images now waiting for 5min && git clone https://github.com/opensearch-project/opensearch-build.git && ' + + 'bash.exe -c "docker run --rm -it --name docker-windows-test -d `opensearch-build/docker/ci/get-ci-images.sh ' + + '-p windows2019-servercore -u opensearch -t build | head -1` bash.exe && sleep 5" && docker exec docker-windows-test whoami && ' + + 'docker ps && docker stop docker-windows-test && docker ps && rm -rf opensearch-build', + remoteFs: 'C:/Users/Administrator/jenkins', + }; + this.WINDOWS2019_X64_DOCKER_BUILDER = { + agentType: 'windows', + customDeviceMapping: '/dev/sda1=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-Windows2019-X64-C54xlarge-Docker-Builder', + instanceType: 'C54xlarge', + remoteUser: 'Administrator', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 1, + numExecutors: 1, + amiId: 'ami-0a9759da263ce9304', + initScript: 'echo %USERNAME% && dockerd --register-service && net start docker && echo started docker deamon && docker ps && ' + + 'echo initializing docker images now waiting for 5min && git clone https://github.com/opensearch-project/opensearch-build.git && ' + + 'bash.exe -c "docker run --rm -it --name docker-windows-test -d `opensearch-build/docker/ci/get-ci-images.sh ' + + '-p windows2019-servercore -u opensearch -t build | head -1` bash.exe && sleep 5" && docker exec docker-windows-test whoami && ' + + 'docker ps && docker stop docker-windows-test && docker ps && rm -rf opensearch-build', + remoteFs: 'C:/Users/Administrator/jenkins', + }; + this.WINDOWS2019_X64_GRADLE_CHECK = { + agentType: 'windows', + customDeviceMapping: '/dev/sda1=:300:true:::encrypted', + workerLabelString: 'Jenkins-Agent-Windows2019-X64-C524xlarge-Single-Host', + instanceType: 'C524xlarge', + remoteUser: 'Administrator', + maxTotalUses: 1, + minimumNumberOfSpareInstances: 1, + numExecutors: 1, + amiId: 'ami-0ca4f0ba85855e148', + initScript: 'echo', + remoteFs: 'C:/Users/Administrator/jenkins', + }; + this.AL2_X64_DEFAULT_AGENT = { + agentType: 'unix', + customDeviceMapping: '/dev/xvda=:300:true:::encrypted', + workerLabelString: 'Jenkins-Default-Agent-X64-C5xlarge-Single-Host', + instanceType: 'C54xlarge', + remoteUser: 'ec2-user', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 1, + numExecutors: 1, + amiId: MachineImage.latestAmazonLinux({ + generation: AmazonLinuxGeneration.AMAZON_LINUX_2, + cpuType: AmazonLinuxCpuType.X86_64, + }).getImage(stack).imageId.toString(), + initScript: 'sudo amazon-linux-extras install java-openjdk11 -y && sudo yum install -y cmake python3 python3-pip && ' + + 'sudo yum groupinstall -y \'Development Tools\' && sudo ln -sfn `which pip3` /usr/bin/pip && ' + + 'pip3 install pipenv && sudo ln -s ~/.local/bin/pipenv /usr/local/bin', + remoteFs: '/home/ec2-user', + }; + this.AL2_ARM64_DEFAULT_AGENT = { + agentType: 'unix', + customDeviceMapping: '/dev/xvda=:300:true:::encrypted', + workerLabelString: 'Jenkins-Default-Agent-ARM64-C5xlarge-Single-Host', + instanceType: 'C6g4xlarge', + remoteUser: 'ec2-user', + maxTotalUses: -1, + minimumNumberOfSpareInstances: 1, + numExecutors: 1, + amiId: MachineImage.latestAmazonLinux({ + generation: AmazonLinuxGeneration.AMAZON_LINUX_2, + cpuType: AmazonLinuxCpuType.ARM_64, + }).getImage(stack).imageId.toString(), + initScript: 'sudo amazon-linux-extras install java-openjdk11 -y && sudo yum install -y cmake python3 python3-pip && ' + + 'sudo yum groupinstall -y \'Development Tools\' && sudo ln -sfn `which pip3` /usr/bin/pip && ' + + 'pip3 install pipenv && sudo ln -s ~/.local/bin/pipenv /usr/local/bin', + remoteFs: '/home/ec2-user', + }; + } +} diff --git a/lib/compute/jenkins-main-node-benchmark.ts b/lib/compute/jenkins-main-node-benchmark.ts new file mode 100644 index 00000000..15bc0004 --- /dev/null +++ b/lib/compute/jenkins-main-node-benchmark.ts @@ -0,0 +1,457 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { + CfnOutput, Duration, Fn, Stack, + } from 'aws-cdk-lib'; + import { + AutoScalingGroup, BlockDeviceVolume, Monitoring, Signals, + } from 'aws-cdk-lib/aws-autoscaling'; + import { Metric, Unit } from 'aws-cdk-lib/aws-cloudwatch'; + import { + AmazonLinuxGeneration, CloudFormationInit, InitCommand, InitElement, InitFile, InitPackage, + InstanceClass, + InstanceSize, + InstanceType, + MachineImage, SecurityGroup, ISecurityGroup, + SubnetType, IVpc, + } from 'aws-cdk-lib/aws-ec2'; + import { FileSystem, PerformanceMode, ThroughputMode } from 'aws-cdk-lib/aws-efs'; + import { + IManagedPolicy, ManagedPolicy, PolicyStatement, Role, ServicePrincipal, + } from 'aws-cdk-lib/aws-iam'; + import { writeFileSync } from 'fs'; + import { dump } from 'js-yaml'; + import { join } from 'path'; + import { CloudwatchAgent } from '../constructs/cloudwatch-agent'; + import { AgentNodeConfigBenchmark, AgentNodeNetworkPropsBenchmark, AgentNodePropsBenchmark } from './agent-node-config-benchmark'; + import { EnvConfig } from './env-config'; + import { OidcConfig } from './oidc-config'; + import { ViewsConfig } from './views-benchmark'; + + interface HttpConfigPropsBenchmark { + readonly redirectUrlArn: string; + readonly sslCertContentsArn: string; + readonly sslCertChainArn: string; + readonly sslCertPrivateKeyContentsArn: string; + readonly useSsl: boolean; + } + + interface OidcFederatePropsBenchmark { + readonly oidcCredArn: string; + readonly runWithOidc: boolean; + readonly adminUsers?: string[]; + } + + interface DataRetentionPropsBenchmark { + readonly dataRetention?: boolean; + readonly efsSG?: SecurityGroup; + } + + export interface JenkinsMainNodePropsBenchmark extends HttpConfigPropsBenchmark, OidcFederatePropsBenchmark, + AgentNodeNetworkPropsBenchmark, DataRetentionPropsBenchmark { + readonly vpc: IVpc; + readonly sg: ISecurityGroup; + readonly envVarsFilePath: string; + readonly reloadPasswordSecretsArn: string; + readonly enableViews: boolean; + readonly failOnCloudInitError?: boolean; + } + + export class JenkinsMainNodeBenchmark { + static readonly BASE_JENKINS_YAML_PATH: string = join(__dirname, '../../resources/baseJenkins-benchmark.yaml'); + + static readonly NEW_JENKINS_YAML_PATH: string = join(__dirname, '../../resources/jenkins-benchmark.yaml'); + + static readonly CERTIFICATE_FILE_PATH: String = '/etc/ssl/certs/test-jenkins.opensearch.org.crt'; + + static readonly CERTIFICATE_CHAIN_FILE_PATH: String = '/etc/ssl/certs/test-jenkins.opensearch.org.pem'; + + static readonly PRIVATE_KEY_PATH: String = '/etc/ssl/private/test-jenkins.opensearch.org.key'; + + static readonly JENKINS_DEFAULT_ID_PASS_PATH: String = '/var/lib/jenkins/secrets/myIdPassDefault'; + + private readonly EFS_ID: string; + + private static ACCOUNT: string; + + private static STACKREGION: string + + public readonly mainNodeBenchAsg: AutoScalingGroup; + + public readonly ec2InstanceMetrics: { + memUsed: Metric, + foundJenkinsProcessCount: Metric + } + + constructor(stack: Stack, props: JenkinsMainNodePropsBenchmark, agentNode: AgentNodePropsBenchmark[], macAgent: string, assumeRole?: string[]) { + this.ec2InstanceMetrics = { + memUsed: new Metric({ + metricName: 'mem_used_percent_benchmark', + namespace: `${stack.stackName}/JenkinsMainNodeBenchmark`, + }), + foundJenkinsProcessCount: new Metric({ + metricName: 'procstat_lookup_pid_count_benchmark', + namespace: `${stack.stackName}/JenkinsMainNodeBenchmark`, + }), + }; + + const importedSGId = Fn.importValue('CIStackMainNodeSGId'); + const mainCiMainNodeSGId = SecurityGroup.fromSecurityGroupId(stack, 'CIStackMainNodeSGId', importedSGId); + + const agentNodeConfig = new AgentNodeConfigBenchmark(stack, assumeRole); + const jenkinsyaml = JenkinsMainNodeBenchmark.addConfigtoJenkinsYaml(stack, props, props, agentNodeConfig, props, agentNode, macAgent); + if (props.dataRetention) { + const efs = new FileSystem(stack, 'EFSfilesystemBenchmark', { + vpc: props.vpc, + encrypted: true, + enableAutomaticBackups: true, + performanceMode: PerformanceMode.GENERAL_PURPOSE, + throughputMode: ThroughputMode.BURSTING, + securityGroup: props.efsSG, + }); + this.EFS_ID = efs.fileSystemId; + } + this.mainNodeBenchAsg = new AutoScalingGroup(stack, 'MainNodeAsgBenchmark', { + instanceType: InstanceType.of(InstanceClass.C5, InstanceSize.XLARGE9), + machineImage: MachineImage.latestAmazonLinux({ + generation: AmazonLinuxGeneration.AMAZON_LINUX_2, + }), + role: new Role(stack, 'OpenSearch-CI-MainNodeRole-Benchmark', { + roleName: 'OpenSearch-CI-MainNodeRole-Benchmark', + assumedBy: new ServicePrincipal('ec2.amazonaws.com'), + }), + initOptions: { + ignoreFailures: props.failOnCloudInitError ?? true, + }, + vpc: props.vpc, + vpcSubnets: { + subnetType: SubnetType.PUBLIC, + }, + minCapacity: 1, + maxCapacity: 1, + desiredCapacity: 1, + securityGroup: props.sg, + init: CloudFormationInit.fromElements(...JenkinsMainNodeBenchmark.configElements( + stack.stackName, + stack.region, + props, + props, + props, + jenkinsyaml, + props.reloadPasswordSecretsArn, + this.EFS_ID, + )), + blockDevices: [{ + deviceName: '/dev/xvda', + volume: BlockDeviceVolume.ebs(100, { encrypted: true, deleteOnTermination: true }), + }], + signals: Signals.waitForAll({ + timeout: Duration.minutes(20), + }), + requireImdsv2: true, + instanceMonitoring: Monitoring.DETAILED, + }); + + this.mainNodeBenchAsg.addSecurityGroup(mainCiMainNodeSGId); + + JenkinsMainNodeBenchmark.createPoliciesForMainNode(stack).map( + (policy) => this.mainNodeBenchAsg.role.addManagedPolicy(policy), + ); + + new CfnOutput(stack, 'Jenkins Main Node Role Arn Benchmark', { + value: this.mainNodeBenchAsg.role.roleArn, + exportName: 'mainNodeRoleArnBenchmark', + }); + } + + public static createPoliciesForMainNode(stack: Stack): (IManagedPolicy | ManagedPolicy)[] { + this.STACKREGION = stack.region; + this.ACCOUNT = stack.account; + + // Policy for SSM management of the host - Removes the need of SSH keys + const ec2SsmManagementPolicy = ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'); + + // Policy for EC2 instance to publish logs and metrics to cloudwatch + const cloudwatchEventPublishingPolicy = ManagedPolicy.fromAwsManagedPolicyName('CloudWatchAgentServerPolicy'); + + const accessPolicy = ManagedPolicy.fromAwsManagedPolicyName('SecretsManagerReadWrite'); + + // Main jenkins node will start/stop agent ec2 instances to run build jobs + const mainJenkinsNodePolicy = new ManagedPolicy(stack, 'MainJenkinsNodePolicyBenchmark', + { + description: 'Policy for a main jenkins node', + statements: [new PolicyStatement({ + actions: [ + 'ec2:DescribeSpotInstanceRequests', + 'ec2:ModifyInstanceMetadataOptions', + 'ec2:CancelSpotInstanceRequests', + 'ec2:GetConsoleOutput', + 'ec2:RequestSpotInstances', + 'ec2:RunInstances', + 'ec2:StartInstances', + 'ec2:StopInstances', + 'ec2:TerminateInstances', + 'ec2:CreateTags', + 'ec2:DeleteTags', + 'ec2:DescribeInstances', + 'ec2:DescribeKeyPairs', + 'ec2:DescribeRegions', + 'ec2:DescribeImages', + 'ec2:DescribeAvailabilityZones', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeSubnets', + 'iam:ListInstanceProfilesForRole', + 'iam:PassRole', + 'logs:CreateLogDelivery', + 'logs:DeleteLogDelivery', + 'secretsmanager:GetSecretValue', + 'secretsmanager:ListSecrets', + 'sts:AssumeRole', + 'elasticfilesystem:DescribeFileSystems', + 'elasticfilesystem:DescribeMountTargets', + 'ec2:DescribeAvailabilityZones', + 'ec2:GetPasswordData', + ], + resources: ['*'], + conditions: { + 'ForAllValues:StringEquals': { + 'aws:RequestedRegion': this.STACKREGION, + 'aws:PrincipalAccount': this.ACCOUNT, + }, + }, + })], + }); + + return [ec2SsmManagementPolicy, cloudwatchEventPublishingPolicy, accessPolicy, mainJenkinsNodePolicy]; + } + + public static configElements(stackName: string, stackRegion: string, httpConfigProps: HttpConfigPropsBenchmark, + oidcFederateProps: OidcFederatePropsBenchmark, dataRetentionProps: DataRetentionPropsBenchmark, jenkinsyaml: string, + reloadPasswordSecretsArn: string, efsId?: string): InitElement[] { + return [ + InitPackage.yum('wget'), + InitPackage.yum('openssl'), + InitPackage.yum('mod_ssl'), + InitPackage.yum('amazon-efs-utils'), + InitCommand.shellCommand('amazon-linux-extras install java-openjdk11 -y'), + InitPackage.yum('docker'), + InitPackage.yum('python3'), + InitPackage.yum('python3-pip.noarch'), + InitCommand.shellCommand('pip3 install botocore'), + // eslint-disable-next-line max-len + InitCommand.shellCommand('sudo wget -nv https://github.com/mikefarah/yq/releases/download/v4.22.1/yq_linux_amd64 -O /usr/bin/yq && sudo chmod +x /usr/bin/yq'), + // eslint-disable-next-line max-len + InitCommand.shellCommand('sudo curl -L https://github.com/docker/compose/releases/download/v2.9.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/bin/docker-compose && sudo chmod +x /usr/bin/docker-compose'), + InitCommand.shellCommand('python3 -m pip install --upgrade pip && python3 -m pip install cryptography boto3 requests-aws4auth'), + + InitCommand.shellCommand(httpConfigProps.useSsl + // eslint-disable-next-line max-len + ? `aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${httpConfigProps.sslCertContentsArn} --query SecretString --output text > ${JenkinsMainNodeBenchmark.CERTIFICATE_FILE_PATH}` + : 'echo useSsl is false, not creating cert file'), + + InitCommand.shellCommand(httpConfigProps.useSsl + // eslint-disable-next-line max-len + ? `aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${httpConfigProps.sslCertChainArn} --query SecretString --output text > ${JenkinsMainNodeBenchmark.CERTIFICATE_CHAIN_FILE_PATH}` + : 'echo useSsl is false, not creating cert-chain file'), + + InitCommand.shellCommand(httpConfigProps.useSsl + // eslint-disable-next-line max-len + ? `mkdir /etc/ssl/private/ && aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${httpConfigProps.sslCertPrivateKeyContentsArn} --query SecretString --output text > ${JenkinsMainNodeBenchmark.PRIVATE_KEY_PATH}` + : 'echo useSsl is false, not creating key file'), + + // Local reverse proxy is used + InitPackage.yum('httpd'), + + // Change hop limit for IMDSv2 from 1 to 2 + InitCommand.shellCommand('TOKEN=`curl -f -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` &&' + + ' instance_id=`curl -f -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id` && echo $ami_id &&' + + ` aws ec2 --region ${stackRegion} modify-instance-metadata-options --instance-id $instance_id --http-put-response-hop-limit 2`), + + // Configuration to proxy jenkins on :8080 -> :80 + InitFile.fromString('/etc/httpd/conf.d/jenkins.conf', + httpConfigProps.useSsl + ? ` + ServerAdmin webmaster@localhost + Redirect permanent / https://replace_url.com/ + + + SSLEngine on + SSLCertificateFile ${JenkinsMainNodeBenchmark.CERTIFICATE_FILE_PATH} + SSLCertificateKeyFile ${JenkinsMainNodeBenchmark.PRIVATE_KEY_PATH} + SSLCertificateChainFile ${JenkinsMainNodeBenchmark.CERTIFICATE_CHAIN_FILE_PATH} + ServerAdmin webmaster@localhost + ProxyRequests Off + ProxyPreserveHost On + AllowEncodedSlashes NoDecode + + Order deny,allow + Allow from all + + ProxyPass /benchmark/ http://localhost:8080/benchmark/ nocanon + ProxyPassReverse /benchmark/ http://localhost:8080/benchmark/ + + ProxyPass / http://localhost:8080/ nocanon + ProxyPassReverse / http://localhost:8080/ + ProxyPassReverse / http://replace_url.com/ + RequestHeader set X-Forwarded-Proto "https" + RequestHeader set X-Forwarded-Port "443" + + + Header unset Server + ` + : ` + ServerAdmin webmaster@127.0.0.1 + ProxyRequests Off + ProxyPreserveHost On + AllowEncodedSlashes NoDecode + + + Order deny,allow + Allow from all + + + ProxyPass /benchmark/ http://localhost:8080/benchmark/ nocanon + ProxyPassReverse /benchmark/ http://localhost:8080/benchmark/ + + ProxyPass / http://127.0.0.1:8080/ nocanon + ProxyPassReverse / http://127.0.0.1:8080/ + `), + + // replacing the jenkins redirect url if the using ssl + InitCommand.shellCommand(httpConfigProps.useSsl + // eslint-disable-next-line max-len + ? `var=\`aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${httpConfigProps.redirectUrlArn} --query SecretString --output text\`` + + ' && sed -i "s,https://replace_url.com/,$var," /etc/httpd/conf.d/jenkins.conf' + : 'echo Not altering the jenkins url'), + + // Auto redirect http to https if ssl is enabled + InitCommand.shellCommand(httpConfigProps.useSsl + // eslint-disable-next-line max-len + ? `var=\`aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${httpConfigProps.redirectUrlArn} --query SecretString --output text\`` + + '&& newVar=`echo $var | sed \'s/https/http/g\'` && sed -i "s,http://replace_url.com/,$newVar," /etc/httpd/conf.d/jenkins.conf' + : 'echo Not altering the ProxyPassReverse url'), + + InitCommand.shellCommand('systemctl start httpd'), + + InitPackage.yum('amazon-cloudwatch-agent'), + + CloudwatchAgent.asInitFile('/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json', { + agent: { + metrics_collection_interval: 60, // seconds between collections + logfile: '/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log', + omit_hostname: true, + debug: false, + }, + metrics: { + namespace: `${stackName}/JenkinsMainNodeBenchmark`, + append_dimensions: { + // eslint-disable-next-line no-template-curly-in-string + InstanceId: '${aws:InstanceId}', + }, + aggregation_dimensions: [[]], // Create rollups without instance id + metrics_collected: { + procstat: [ + { + pattern: 'jenkins.war', + measurement: [ + 'cpu_usage', + 'cpu_time_system', + 'cpu_time_user', + 'read_bytes', + 'write_bytes', + 'pid_count', + ], + metrics_collection_interval: 10, + }, + ], + mem: { + measurement: [ + { name: 'available_percent', unit: Unit.PERCENT }, + { name: 'used_percent', unit: Unit.PERCENT }, + { name: 'mem_total', unit: Unit.BYTES }, + ], + metrics_collection_interval: 1, // capture every second + }, + }, + }, + logs: { + logs_collected: { + files: { + collect_list: [ + { + file_path: '/var/lib/jenkins/logs/custom/workflowRun.log', + log_group_name: 'JenkinsMainNode/workflow.log', + auto_removal: true, + log_stream_name: 'workflow-logs', + }, + ], + }, + }, + force_flush_interval: 5, + }, + }), + InitCommand.shellCommand('/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a stop'), + // eslint-disable-next-line max-len + InitCommand.shellCommand('/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json -s'), + + InitCommand.shellCommand(dataRetentionProps.dataRetention + ? `mkdir /var/lib/jenkins && mount -t efs ${efsId} /var/lib/jenkins` + : 'echo Data rentention is disabled, not mounting efs'), + + InitFile.fromFileInline('/docker-compose.yml', join(__dirname, '../../resources/docker-compose-benchmark.yml')), + + InitCommand.shellCommand('systemctl start docker &&' + + ` var=\`aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${reloadPasswordSecretsArn} --query SecretString --output text\` &&` + + ' yq -i \'.services.jenkins.environment[1] = "CASC_RELOAD_TOKEN=\'$var\'"\' docker-compose.yml &&' + + ' docker-compose up -d'), + + // Commands are fired one after the other but it does not wait for the command to complete. + // Therefore, sleep 60 seconds to wait for jenkins to start + InitCommand.shellCommand('sleep 60'), + + InitFile.fromFileInline('/initial_jenkins.yaml', jenkinsyaml), + + // Make any changes to initial jenkins.yaml + InitCommand.shellCommand(oidcFederateProps.runWithOidc + // eslint-disable-next-line max-len + ? `var=\`aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${oidcFederateProps.oidcCredArn} --query SecretString --output text\` && ` + + ' varkeys=`echo $var | yq \'keys\' | cut -d "-" -f2 | cut -d " " -f2` &&' + // eslint-disable-next-line max-len + + ' for i in $varkeys; do newvalue=`echo $var | yq .$i` && myenv=$newvalue i=$i yq -i \'.jenkins.securityRealm.oic.[env(i)]=env(myenv)\' /initial_jenkins.yaml ; done' + : 'echo No changes made to initial_jenkins.yaml with respect to OIDC'), + + // eslint-disable-next-line max-len + InitCommand.shellCommand('while [[ "$(curl -s -o /dev/null -w \'\'%{http_code}\'\' localhost:8080/benchmark/api/json?pretty)" != "200" ]]; do sleep 5; done'), + + // Reload configuration via Jenkins.yaml + InitCommand.shellCommand('cp /initial_jenkins.yaml /var/lib/jenkins/jenkins.yaml &&' + + ` var=\`aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${reloadPasswordSecretsArn} --query SecretString --output text\` &&` + + ' curl -f -X POST "http://localhost:8080/benchmark/reload-configuration-as-code/?casc-reload-token=$var"'), + ]; + } + + public static addConfigtoJenkinsYaml(stack: Stack, jenkinsMainNodeProps: JenkinsMainNodePropsBenchmark, oidcProps: OidcFederatePropsBenchmark, + agentNodeObject: AgentNodeConfigBenchmark, props: AgentNodeNetworkPropsBenchmark, agentNode: AgentNodePropsBenchmark[], + macAgent: string): string { + let updatedConfig = agentNodeObject.addAgentConfigToJenkinsYaml(stack, agentNode, props, macAgent); + if (oidcProps.runWithOidc) { + updatedConfig = OidcConfig.addOidcConfigToJenkinsYaml(updatedConfig, oidcProps.adminUsers); + } + if (jenkinsMainNodeProps.envVarsFilePath !== '' && jenkinsMainNodeProps.envVarsFilePath != null) { + updatedConfig = EnvConfig.addEnvConfigToJenkinsYaml(updatedConfig, jenkinsMainNodeProps.envVarsFilePath); + } + if (jenkinsMainNodeProps.enableViews) { + updatedConfig = ViewsConfig.addViewsConfigToJenkinsYaml(updatedConfig); + } + const newConfig = dump(updatedConfig); + writeFileSync(JenkinsMainNodeBenchmark.NEW_JENKINS_YAML_PATH, newConfig, 'utf-8'); + return JenkinsMainNodeBenchmark.NEW_JENKINS_YAML_PATH; + } + } + \ No newline at end of file diff --git a/lib/compute/jenkins-main-node.ts b/lib/compute/jenkins-main-node.ts index 69485673..e14496f0 100644 --- a/lib/compute/jenkins-main-node.ts +++ b/lib/compute/jenkins-main-node.ts @@ -160,6 +160,11 @@ export class JenkinsMainNode { value: this.mainNodeAsg.role.roleArn, exportName: 'mainNodeRoleArn', }); + + new CfnOutput(stack, 'Jenkins Main Node ASG Name', { + value: this.mainNodeAsg.role.roleArn, + exportName: 'mainNodeASGName', + }); } public static createPoliciesForMainNode(stack: Stack): (IManagedPolicy | ManagedPolicy)[] { diff --git a/lib/compute/views-benchmark.ts b/lib/compute/views-benchmark.ts new file mode 100644 index 00000000..ea06f2fa --- /dev/null +++ b/lib/compute/views-benchmark.ts @@ -0,0 +1,47 @@ +/** + * SPDX,License,Identifier: Apache,2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache,2.0 license or a + * compatible open source license. + */ + +export class ViewsConfig { + private static getViewsConfig(name: string, regex: string): any { + const config = { + list: { + columns: [ + 'status', + 'weather', + 'jobName', + 'lastSuccess', + 'lastFailure', + 'lastDuration', + 'buildButton', + 'favoriteColumn', + ], + includeRegex: regex, + name, + }, + }; + return config; + } + + public static addViewsConfigToJenkinsYaml(yamlObject: any): any { + const jenkinsYaml: any = yamlObject; + const viewsConfig: any = { + Build: '.*build.*', + Test: '.*test.*', + Release: '.*release.*', + Misc: '(?!.*(test|build|release).*).*', + }; + const viewConfigsArray: any[] = []; + Object.keys(viewsConfig).forEach((item) => { + viewConfigsArray.push(this.getViewsConfig(item, viewsConfig[item])); + }); + + viewConfigsArray.forEach((item) => jenkinsYaml.jenkins.views.push(item)); + return jenkinsYaml; + } + } + \ No newline at end of file diff --git a/lib/monitoring/ci-alarms-benchmark.ts b/lib/monitoring/ci-alarms-benchmark.ts new file mode 100644 index 00000000..3da52c48 --- /dev/null +++ b/lib/monitoring/ci-alarms-benchmark.ts @@ -0,0 +1,62 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { Stack } from 'aws-cdk-lib'; +import { + Alarm, AlarmWidget, ComparisonOperator, Dashboard, Metric, TreatMissingData, +} from 'aws-cdk-lib/aws-cloudwatch'; +import { JenkinsMainNodeBenchmark } from '../compute/jenkins-main-node-benchmark'; + +export class JenkinsMonitoringBenchmark { + public readonly alarms: Alarm[] = []; + + constructor(stack: Stack, mainNode: JenkinsMainNodeBenchmark) { + const dashboard = new Dashboard(stack, 'AlarmDashboard-Benchmark'); + + const cpuMetric = new Metric({ + namespace: 'AWS/EC2', + metricName: 'CPUUtilization', + dimensionsMap: { + AutoScalingGroupName: mainNode.mainNodeBenchAsg.autoScalingGroupName, + }, + }); + + this.alarms.push(new Alarm(stack, 'AverageMainNodeCpuUtilization', { + alarmDescription: 'Overall EC2 avg CPU Utilization', + evaluationPeriods: 3, + metric: cpuMetric, + threshold: 50, + comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, + })); + + /** + *If the Jenkins over the last 15 (evaluationPeriods:3 * Period:5) minutes period is less than 1 (jenkins down) for at least 2 times. */ + this.alarms.push(new Alarm(stack, 'MainNodeJenkinsProcessNotFound', { + alarmDescription: 'Jenkins process is not running', + metric: mainNode.ec2InstanceMetrics.foundJenkinsProcessCount.with({ statistic: 'avg' }), + evaluationPeriods: 3, + threshold: 1, + datapointsToAlarm: 3, + comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD, + treatMissingData: TreatMissingData.IGNORE, + })); + + this.alarms.push(new Alarm(stack, 'MainNodeHighMemoryUtilization', { + alarmDescription: 'The jenkins process is using more memory than expected, it should be investigated for a large number of jobs or heavy weight jobs', + metric: mainNode.ec2InstanceMetrics.memUsed.with({ statistic: 'avg' }), + evaluationPeriods: 5, + threshold: 65, + comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + treatMissingData: TreatMissingData.IGNORE, + })); + + this.alarms + .map((alarm) => new AlarmWidget({ alarm })) + .forEach((widget) => dashboard.addWidgets(widget)); + } +} diff --git a/lib/network/ci-external-load-balancer.ts b/lib/network/ci-external-load-balancer.ts index cfbafa29..160cd315 100644 --- a/lib/network/ci-external-load-balancer.ts +++ b/lib/network/ci-external-load-balancer.ts @@ -7,16 +7,16 @@ */ import { CfnOutput, Stack } from 'aws-cdk-lib'; -import { Instance, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; import { - ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, ListenerCertificate, Protocol, SslPolicy, + ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, ListenerCertificate, + Protocol, SslPolicy, ListenerAction, } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; -import { InstanceTarget } from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets'; import { AutoScalingGroup } from 'aws-cdk-lib/aws-autoscaling'; export interface JenkinsExternalLoadBalancerProps { - readonly vpc: Vpc; - readonly sg: SecurityGroup; + readonly vpc: IVpc; + readonly sg: ISecurityGroup; readonly targetInstance: AutoScalingGroup; readonly listenerCertificate: ListenerCertificate; readonly useSsl: boolean; @@ -39,11 +39,22 @@ export class JenkinsExternalLoadBalancer { internetFacing: true, }); + this.targetGroup = new ApplicationTargetGroup(this.loadBalancer, 'MainJenkinsNodeTarget', { + port: accessPort, + vpc: props.vpc, + targets: [props.targetInstance], + healthCheck: { + protocol: props.useSsl ? Protocol.HTTPS : Protocol.HTTP, + path: '/login', + }, + }); + this.listener = this.loadBalancer.addListener('JenkinsListener', { sslPolicy: props.useSsl ? SslPolicy.RECOMMENDED : undefined, port: accessPort, open: false, certificates: props.useSsl ? [props.listenerCertificate] : undefined, + defaultAction: ListenerAction.forward([this.targetGroup]), }); if (props.useSsl) { @@ -55,17 +66,19 @@ export class JenkinsExternalLoadBalancer { }); } - this.targetGroup = this.listener.addTargets('MainJenkinsNodeTarget', { - port: accessPort, - targets: [props.targetInstance], - healthCheck: { - protocol: props.useSsl ? Protocol.HTTPS : Protocol.HTTP, - path: '/login', - }, - }); - new CfnOutput(stack, 'Jenkins External Load Balancer Dns', { value: this.loadBalancer.loadBalancerDnsName, + exportName: 'ALBDns', + }); + + new CfnOutput(stack, 'Jenkins External Load Balancer Arn', { + value: this.loadBalancer.loadBalancerArn, + exportName: 'ALBArn', + }); + + new CfnOutput(stack, 'Jenkins External Load Balancer Listerner Arn', { + value: this.listener.listenerArn, + exportName: 'ALBListenerArn', }); } } diff --git a/lib/security/ci-security-groups.ts b/lib/security/ci-security-groups.ts index 8a4cbbb9..b6264d6d 100644 --- a/lib/security/ci-security-groups.ts +++ b/lib/security/ci-security-groups.ts @@ -8,7 +8,7 @@ import { Stack } from 'aws-cdk-lib'; import { - IPeer, Port, SecurityGroup, Vpc, + IPeer, Port, SecurityGroup, IVpc, } from 'aws-cdk-lib/aws-ec2'; export class JenkinsSecurityGroups { @@ -20,36 +20,36 @@ export class JenkinsSecurityGroups { public readonly efsSG: SecurityGroup; - constructor(stack: Stack, vpc: Vpc, useSsl: boolean, restrictServerAccessTo: IPeer) { + constructor(stack: Stack, vpc: IVpc, useSsl: boolean, restrictServerAccessTo: IPeer, idSG: string) { let accessPort = 80; if (useSsl) { accessPort = 443; } - this.externalAccessSG = new SecurityGroup(stack, 'ExternalAccessSG', { + this.externalAccessSG = new SecurityGroup(stack, `${idSG}-ExternalAccessSG`, { vpc, - description: 'External access to Jenkins', + description: `External access to Jenkins ${idSG}`, }); this.externalAccessSG.addIngressRule(restrictServerAccessTo, Port.tcp(accessPort), 'Restrict jenkins endpoint access to this source'); - this.mainNodeSG = new SecurityGroup(stack, 'MainNodeSG', { + this.mainNodeSG = new SecurityGroup(stack, `${idSG}-MainNodeSG`, { vpc, - description: 'Main node of Jenkins', + description: `Main node of Jenkins ${idSG}`, }); this.mainNodeSG.addIngressRule(this.externalAccessSG, Port.tcp(accessPort)); - this.agentNodeSG = new SecurityGroup(stack, 'AgentNodeSG', { + this.agentNodeSG = new SecurityGroup(stack, `${idSG}-AgentNodeSG`, { vpc, - description: 'Agent Node of Jenkins', + description: `Agent Node of Jenkins ${idSG}`, }); this.agentNodeSG.addIngressRule(this.mainNodeSG, Port.tcp(22), 'Main node SSH Access into agent nodes'); this.agentNodeSG.addIngressRule(this.mainNodeSG, Port.tcp(445), 'Main node SMB Access into agent nodes for Windows'); this.agentNodeSG.addIngressRule(this.mainNodeSG, Port.tcp(5985), 'Main node WinRM HTTP Access into agent nodes for Windows'); this.agentNodeSG.addIngressRule(this.agentNodeSG, Port.allTraffic(), 'Agent node open all ports to other agent nodes within the same SG'); - this.efsSG = new SecurityGroup(stack, 'efsSG', { + this.efsSG = new SecurityGroup(stack, `${idSG}-efsSG`, { vpc, - description: 'Jenkins EFS', + description: `Jenkins EFS ${idSG}`, }); this.efsSG.addIngressRule(this.mainNodeSG, Port.allTraffic(), 'Main node Access to EFS'); } diff --git a/package.json b/package.json index fd3e4bbe..672591af 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "tsc", "watch": "tsc -w", "postbuild": "eslint --fix \"bin/**/*.ts\" \"lib/**/*.ts\" \"test/**/*.ts\" --ignore-pattern \"**/*.d.ts\" && jest", - "cdk": "cdk" + "cdk": "cdk", + "test": "jest" }, "devDependencies": { "@types/jest": "^26.0.10", diff --git a/resources/baseJenkins-benchmark.yaml b/resources/baseJenkins-benchmark.yaml new file mode 100644 index 00000000..03689d01 --- /dev/null +++ b/resources/baseJenkins-benchmark.yaml @@ -0,0 +1,259 @@ +jenkins: + log: + recorders: + - loggers: + - level: "FINE" + name: "org.jenkinsci.plugins.workflow.job.WorkflowRun" + name: "workflowRun" + agentProtocols: + - "JNLP4-connect" + - "Ping" + authorizationStrategy: "loggedInUsersCanDoAnything" + crumbIssuer: + standard: + excludeClientIPFromCrumb: false + disableRememberMe: false + labelAtoms: + - name: "built-in" + - name: "main-node" + labelString: "main-node" + markupFormatter: + rawHtml: + disableSyntaxHighlighting: true + mode: EXCLUSIVE + myViewsTabBar: "standard" + numExecutors: 4 + primaryView: + all: + name: "all" + projectNamingStrategy: "standard" + quietPeriod: 5 + scmCheckoutRetryCount: 0 + securityRealm: + local: + allowsSignup: false + enableCaptcha: false + users: + - id: "admin" + name: "admin" + properties: + - "apiToken" + - preferredProvider: + providerId: "default" + - "loginDetailsProperty" + slaveAgentPort: 50000 + updateCenter: + sites: + - id: "default" + url: "https://updates.jenkins.io/update-center.json" + views: + - all: + name: "all" + viewsTabBar: "standard" +globalCredentialsConfiguration: + configuration: + providerFilter: "none" + typeFilter: "none" +security: + apiToken: + creationOfLegacyTokenEnabled: false + tokenGenerationOnCreationEnabled: false + usageStatisticsEnabled: true + copyartifact: + mode: PRODUCTION + globalJobDslSecurityConfiguration: + useScriptSecurity: true + sSHD: + port: -1 +unclassified: + audit-trail: + logBuildCause: true + pattern: ".*/(?:configSubmit|doDelete|postBuildResult|enable|disable|cancelQueue|stop|toggleLogKeep|doWipeOutWorkspace|createItem|createView|toggleOffline|cancelQuietDown|quietDown|restart|exit|safeExit)/?.*" + awsCredentialsProvider: + cache: false + client: + credentialsProvider: "default" + buildDiscarders: + configuredBuildDiscarders: + - "jobBuildDiscarder" + buildStepOperation: + enabled: false + buildTimestamp: + enableBuildTimestamp: true + pattern: "yyyy-MM-dd HH:mm:ss z" + timezone: "Etc/UTC" + descriptionSetterWrapper: + disableTokens: false + email-ext: + adminRequiredForTemplateTesting: false + allowUnregisteredEnabled: false + charset: "UTF-8" + debugMode: false + defaultBody: |- + $PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS: + + Check console output at $BUILD_URL to view the results. + defaultSubject: "$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS!" + defaultTriggerIds: + - "hudson.plugins.emailext.plugins.trigger.FailureTrigger" + maxAttachmentSize: -1 + maxAttachmentSizeMb: -1 + precedenceBulk: false + watchingEnabled: false + fingerprints: + fingerprintCleanupDisabled: false + storage: "file" + ghprbTrigger: + autoCloseFailedPullRequests: false + cron: "H/5 * * * *" + extensions: + - ghprbSimpleStatus: + addTestResults: false + showMatrixStatus: false + githubAuth: + - description: "Anonymous connection" + id: "d4456c70-9c5e-4b40-bee4-e1ebb693153b" + serverAPIUrl: "https://api.github.com" + manageWebhooks: true + okToTestPhrase: ".*ok\\W+to\\W+test.*" + retestPhrase: ".*test\\W+this\\W+please.*" + skipBuildPhrase: ".*\\[skip\\W+ci\\].*" + useComments: false + useDetailedComments: false + whitelistPhrase: ".*add\\W+to\\W+whitelist.*" + gitHubConfiguration: + apiRateLimitChecker: ThrottleForNormalize + gitHubPluginConfig: + hookUrl: "http://localhost:8080/github-webhook/" + gitSCM: + addGitTagAction: false + allowSecondFetch: false + createAccountBasedOnEmail: false + disableGitToolChooser: false + hideCredentials: false + showEntireCommitSummaryInChanges: false + useExistingAccountWithSameEmail: false + ivyBuildTrigger: + extendedVersionMatching: false + junitTestResultStorage: + storage: "file" + location: + adminAddress: "address not configured yet " + login-theme-plugin: + useDefaultTheme: true + mailer: + charset: "UTF-8" + useSsl: false + useTls: false + pluginImpl: + enableCredentialsFromNode: false + pollSCM: + pollingThreadCount: 10 + timestamper: + allPipelines: false + elapsedTimeFormat: "''HH:mm:ss.S' '" + systemTimeFormat: "''HH:mm:ss' '" + upstream: + globalUpstreamFilterStrategy: UseOldest + whitelist: + enabled: false +tool: + git: + installations: + - home: "git" + name: "Default" + jdk: + installations: + - name: "openjdk-8" + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: "jdk8u332-b09" + - name: "openjdk-11" + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: "jdk-11.0.15+10" + - name: "openjdk-17" + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: "jdk-17.0.3+7" + - name: "openjdk-19" + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: "jdk-19.0.1+10" + - name: "openjdk-20" + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: "jdk-20.0.1+9" + - name: "openjdk-21" + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: "jdk-21.0.1+12" + mavenGlobalConfig: + globalSettingsProvider: "standard" + settingsProvider: "standard" + powerShellInstallation: + installations: + - home: "powershell.exe" + name: "DefaultWindows" + - home: "pwsh" + name: "DefaultLinux" +support: + automatedBundleConfiguration: + componentIds: + - "AgentsConfigFile" + - "ConfigFileComponent" + - "OtherConfigFilesComponent" + - "AboutBrowser" + - "AboutJenkins" + - "AboutUser" + - "AdministrativeMonitors" + - "AgentProtocols" + - "BuildQueue" + - "CustomLogs" + - "DumpExportTable" + - "EnvironmentVariables" + - "FileDescriptorLimit" + - "GCLogs" + - "HeapUsageHistogram" + - "ItemsContent" + - "AgentsJVMProcessSystemMetricsContents" + - "MasterJVMProcessSystemMetricsContents" + - "JenkinsLogs" + - "LoadStats" + - "LoggerManager" + - "Metrics" + - "NetworkInterfaces" + - "NodeMonitors" + - "OtherLogs" + - "ReverseProxy" + - "RootCAs" + - "RunningBuilds" + - "SlaveCommandStatistics" + - "SlaveLaunchLogs" + - "SlaveLogs" + - "AgentsSystemConfiguration" + - "MasterSystemConfiguration" + - "SystemProperties" + - "TaskLogs" + - "ThreadDumps" + - "UpdateCenter" + - "UserCount" + - "SlowRequestComponent" + - "HighLoadComponent" + - "DeadlockRequestComponent" + - "PipelineTimings" + - "PipelineThreadDump" + enabled: true + period: 1 diff --git a/resources/docker-compose-benchmark.yml b/resources/docker-compose-benchmark.yml new file mode 100644 index 00000000..bb3d1012 --- /dev/null +++ b/resources/docker-compose-benchmark.yml @@ -0,0 +1,31 @@ +version: '3.8' +services: + jenkins: + image: opensearchstaging/jenkins:2.387.1-lts-jdk11 + restart: on-failure + privileged: true + tty: true + user: root + ports: + - 8080:8080 + - 50000:50000 + container_name: jenkins + environment: + - JENKINS_JAVA_OPTS=-Xms4g -Xmx16g -Dhudson.model.ParametersAction.keepUndefinedParameters=true -XX:+UseG1GC -XX:+ExplicitGCInvokesConcurrent -XX:+ParallelRefProcEnabled -XX:+UseStringDeduplication -XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=20 -XX:+UnlockDiagnosticVMOptions + - CASC_RELOAD_TOKEN=reloadPasswordHere + - JENKINS_OPTS=--prefix="/benchmark" + volumes: + - /var/lib/jenkins:/var/jenkins_home + deploy: + resources: + limits: + cpus: '16' + memory: '32g' + reservations: + cpus: '16' + memory: '32g' + logging: + driver: awslogs + options: + awslogs-group: JenkinsMainNodeBenchmark/jenkins.log + awslogs-create-group: 'true' diff --git a/resources/jenkins-benchmark.yaml b/resources/jenkins-benchmark.yaml new file mode 100644 index 00000000..860a33c8 --- /dev/null +++ b/resources/jenkins-benchmark.yaml @@ -0,0 +1,451 @@ +jenkins: + log: + recorders: + - loggers: + - level: FINE + name: org.jenkinsci.plugins.workflow.job.WorkflowRun + name: workflowRun + agentProtocols: + - JNLP4-connect + - Ping + authorizationStrategy: + roleBased: + roles: + global: + - assignments: + - admin + name: admin + pattern: .* + permissions: + - Overall/Administer + - Overall/Read + - Job/Move + - Job/Build + - Job/Read + - Job/Delete + - Job/Create + - Job/Discover + - Job/Cancel + - Job/Configure + - Job Config History/DeleteEntry + - Job/Workspace + - Credentials/Delete + - Credentials/ManageDomains + - Credentials/Update + - Credentials/View + - Credentials/Create + - Manage ownership/Nodes + - Manage ownership/Jobs + - Agent/Configure + - Agent/Create + - Agent/Build + - Agent/Provision + - Agent/Connect + - Agent/Delete + - Agent/Disconnect + - Run/Replay + - Run/Delete + - Run/Update + - View/Delete + - View/Read + - View/Create + - View/Configure + - SCM/Tag + - assignments: + - anonymous + name: read + pattern: .* + permissions: + - Overall/Read + - Job/Read + crumbIssuer: + standard: + excludeClientIPFromCrumb: false + disableRememberMe: false + labelAtoms: + - name: built-in + - name: main-node + labelString: main-node + markupFormatter: + rawHtml: + disableSyntaxHighlighting: true + mode: EXCLUSIVE + myViewsTabBar: standard + numExecutors: 4 + primaryView: + all: + name: all + projectNamingStrategy: standard + quietPeriod: 5 + scmCheckoutRetryCount: 0 + securityRealm: + oic: + clientId: clientId + clientSecret: clientSecret + authorizationServerUrl: http://localhost + wellKnownOpenIDConfigurationUrl: wellKnownOpenIDConfigurationUrl + tokenServerUrl: tokenServerUrl + userInfoServerUrl: userInfoServerUrl + disableSslVerification: false + userNameField: sub + escapeHatchEnabled: false + logoutFromOpenidProvider: true + postLogoutRedirectUrl: '' + scopes: openid + escapeHatchSecret: random + slaveAgentPort: 50000 + updateCenter: + sites: + - id: default + url: https://updates.jenkins.io/update-center.json + views: + - all: + name: all + viewsTabBar: standard + clouds: + - amazonEC2: + cloudName: Amazon_ec2_cloud + region: ${Token[AWS.Region.13]} + sshKeysCredentialsId: ${Token[Fn::Join.636]} + templates: + - ami: ${Token[TOKEN.556]} + amiType: + unixData: + sshPort: '22' + associatePublicIp: false + connectBySSHProcess: false + connectionStrategy: PRIVATE_IP + customDeviceMapping: /dev/xvda=:300:true:::encrypted + deleteRootOnTermination: true + description: jenkinsAgentNode-Jenkins-Default-Agent-X64-C5xlarge-Single-Host + ebsEncryptRootVolume: ENCRYPTED + ebsOptimized: false + metadataTokensRequired: true + metadataHopsLimit: '2' + hostKeyVerificationStrategy: 'OFF' + iamInstanceProfile: ${Token[TOKEN.635]} + idleTerminationMinutes: '60' + initScript: >- + sudo amazon-linux-extras install java-openjdk11 -y && sudo yum + install -y cmake python3 python3-pip && sudo yum groupinstall -y + 'Development Tools' && sudo ln -sfn `which pip3` /usr/bin/pip && + pip3 install pipenv && sudo ln -s ~/.local/bin/pipenv + /usr/local/bin + labelString: Jenkins-Default-Agent-X64-C5xlarge-Single-Host + launchTimeoutStr: '300' + maxTotalUses: -1 + minimumNumberOfInstances: 0 + minimumNumberOfSpareInstances: 1 + mode: EXCLUSIVE + monitoring: true + numExecutors: 1 + remoteAdmin: ec2-user + remoteFS: /home/ec2-user + securityGroups: ${Token[TOKEN.520]} + stopOnTerminate: false + subnetId: ${Token[TOKEN.491]} + t2Unlimited: false + tags: + - name: Name + value: >- + TestStack/AgentNode/Jenkins-Default-Agent-X64-C5xlarge-Single-Host + - name: type + value: >- + jenkinsAgentNode-Jenkins-Default-Agent-X64-C5xlarge-Single-Host + tenancy: Default + type: C54xlarge + nodeProperties: + - envVars: + env: + - key: JENKINS_HOME_PATH + value: /home/ec2-user + - key: JAVA8_HOME + value: /usr/lib/jvm/temurin-8-jdk-amd64 + - key: JAVA11_HOME + value: /usr/lib/jvm/temurin-11-jdk-amd64 + - key: JAVA14_HOME + value: /usr/lib/jvm/adoptopenjdk-14-amd64 + - key: JAVA17_HOME + value: /usr/lib/jvm/temurin-17-jdk-amd64 + - key: JAVA19_HOME + value: /usr/lib/jvm/temurin-19-jdk-amd64 + - key: JAVA20_HOME + value: /usr/lib/jvm/temurin-20-jdk-amd64 + - key: JAVA21_HOME + value: /usr/lib/jvm/temurin-21-jdk-amd64 + useEphemeralDevices: false + - ami: ${Token[TOKEN.559]} + amiType: + unixData: + sshPort: '22' + associatePublicIp: false + connectBySSHProcess: false + connectionStrategy: PRIVATE_IP + customDeviceMapping: /dev/xvda=:300:true:::encrypted + deleteRootOnTermination: true + description: jenkinsAgentNode-Jenkins-Default-Agent-ARM64-C5xlarge-Single-Host + ebsEncryptRootVolume: ENCRYPTED + ebsOptimized: false + metadataTokensRequired: true + metadataHopsLimit: '2' + hostKeyVerificationStrategy: 'OFF' + iamInstanceProfile: ${Token[TOKEN.635]} + idleTerminationMinutes: '60' + initScript: >- + sudo amazon-linux-extras install java-openjdk11 -y && sudo yum + install -y cmake python3 python3-pip && sudo yum groupinstall -y + 'Development Tools' && sudo ln -sfn `which pip3` /usr/bin/pip && + pip3 install pipenv && sudo ln -s ~/.local/bin/pipenv + /usr/local/bin + labelString: Jenkins-Default-Agent-ARM64-C5xlarge-Single-Host + launchTimeoutStr: '300' + maxTotalUses: -1 + minimumNumberOfInstances: 0 + minimumNumberOfSpareInstances: 1 + mode: EXCLUSIVE + monitoring: true + numExecutors: 1 + remoteAdmin: ec2-user + remoteFS: /home/ec2-user + securityGroups: ${Token[TOKEN.520]} + stopOnTerminate: false + subnetId: ${Token[TOKEN.491]} + t2Unlimited: false + tags: + - name: Name + value: >- + TestStack/AgentNode/Jenkins-Default-Agent-ARM64-C5xlarge-Single-Host + - name: type + value: >- + jenkinsAgentNode-Jenkins-Default-Agent-ARM64-C5xlarge-Single-Host + tenancy: Default + type: C6g4xlarge + nodeProperties: + - envVars: + env: + - key: JENKINS_HOME_PATH + value: /home/ec2-user + - key: JAVA8_HOME + value: /usr/lib/jvm/temurin-8-jdk-amd64 + - key: JAVA11_HOME + value: /usr/lib/jvm/temurin-11-jdk-amd64 + - key: JAVA14_HOME + value: /usr/lib/jvm/adoptopenjdk-14-amd64 + - key: JAVA17_HOME + value: /usr/lib/jvm/temurin-17-jdk-amd64 + - key: JAVA19_HOME + value: /usr/lib/jvm/temurin-19-jdk-amd64 + - key: JAVA20_HOME + value: /usr/lib/jvm/temurin-20-jdk-amd64 + - key: JAVA21_HOME + value: /usr/lib/jvm/temurin-21-jdk-amd64 + useEphemeralDevices: false + useInstanceProfileForCredentials: true +globalCredentialsConfiguration: + configuration: + providerFilter: none + typeFilter: none +security: + apiToken: + creationOfLegacyTokenEnabled: false + tokenGenerationOnCreationEnabled: false + usageStatisticsEnabled: true + copyartifact: + mode: PRODUCTION + globalJobDslSecurityConfiguration: + useScriptSecurity: true + sSHD: + port: -1 +unclassified: + audit-trail: + logBuildCause: true + pattern: >- + .*/(?:configSubmit|doDelete|postBuildResult|enable|disable|cancelQueue|stop|toggleLogKeep|doWipeOutWorkspace|createItem|createView|toggleOffline|cancelQuietDown|quietDown|restart|exit|safeExit)/?.* + awsCredentialsProvider: + cache: false + client: + credentialsProvider: default + buildDiscarders: + configuredBuildDiscarders: + - jobBuildDiscarder + buildStepOperation: + enabled: false + buildTimestamp: + enableBuildTimestamp: true + pattern: yyyy-MM-dd HH:mm:ss z + timezone: Etc/UTC + descriptionSetterWrapper: + disableTokens: false + email-ext: + adminRequiredForTemplateTesting: false + allowUnregisteredEnabled: false + charset: UTF-8 + debugMode: false + defaultBody: |- + $PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS: + + Check console output at $BUILD_URL to view the results. + defaultSubject: '$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS!' + defaultTriggerIds: + - hudson.plugins.emailext.plugins.trigger.FailureTrigger + maxAttachmentSize: -1 + maxAttachmentSizeMb: -1 + precedenceBulk: false + watchingEnabled: false + fingerprints: + fingerprintCleanupDisabled: false + storage: file + ghprbTrigger: + autoCloseFailedPullRequests: false + cron: H/5 * * * * + extensions: + - ghprbSimpleStatus: + addTestResults: false + showMatrixStatus: false + githubAuth: + - description: Anonymous connection + id: d4456c70-9c5e-4b40-bee4-e1ebb693153b + serverAPIUrl: https://api.github.com + manageWebhooks: true + okToTestPhrase: .*ok\W+to\W+test.* + retestPhrase: .*test\W+this\W+please.* + skipBuildPhrase: .*\[skip\W+ci\].* + useComments: false + useDetailedComments: false + whitelistPhrase: .*add\W+to\W+whitelist.* + gitHubConfiguration: + apiRateLimitChecker: ThrottleForNormalize + gitHubPluginConfig: + hookUrl: http://localhost:8080/github-webhook/ + gitSCM: + addGitTagAction: false + allowSecondFetch: false + createAccountBasedOnEmail: false + disableGitToolChooser: false + hideCredentials: false + showEntireCommitSummaryInChanges: false + useExistingAccountWithSameEmail: false + ivyBuildTrigger: + extendedVersionMatching: false + junitTestResultStorage: + storage: file + location: + adminAddress: address not configured yet + login-theme-plugin: + useDefaultTheme: true + mailer: + charset: UTF-8 + useSsl: false + useTls: false + pluginImpl: + enableCredentialsFromNode: false + pollSCM: + pollingThreadCount: 10 + timestamper: + allPipelines: false + elapsedTimeFormat: '''''HH:mm:ss.S'' ''' + systemTimeFormat: '''''HH:mm:ss'' ''' + upstream: + globalUpstreamFilterStrategy: UseOldest + whitelist: + enabled: false +tool: + git: + installations: + - home: git + name: Default + jdk: + installations: + - name: openjdk-8 + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: jdk8u332-b09 + - name: openjdk-11 + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: jdk-11.0.15+10 + - name: openjdk-17 + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: jdk-17.0.3+7 + - name: openjdk-19 + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: jdk-19.0.1+10 + - name: openjdk-20 + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: jdk-20.0.1+9 + - name: openjdk-21 + properties: + - installSource: + installers: + - adoptOpenJdkInstaller: + id: jdk-21.0.1+12 + mavenGlobalConfig: + globalSettingsProvider: standard + settingsProvider: standard + powerShellInstallation: + installations: + - home: powershell.exe + name: DefaultWindows + - home: pwsh + name: DefaultLinux +support: + automatedBundleConfiguration: + componentIds: + - AgentsConfigFile + - ConfigFileComponent + - OtherConfigFilesComponent + - AboutBrowser + - AboutJenkins + - AboutUser + - AdministrativeMonitors + - AgentProtocols + - BuildQueue + - CustomLogs + - DumpExportTable + - EnvironmentVariables + - FileDescriptorLimit + - GCLogs + - HeapUsageHistogram + - ItemsContent + - AgentsJVMProcessSystemMetricsContents + - MasterJVMProcessSystemMetricsContents + - JenkinsLogs + - LoadStats + - LoggerManager + - Metrics + - NetworkInterfaces + - NodeMonitors + - OtherLogs + - ReverseProxy + - RootCAs + - RunningBuilds + - SlaveCommandStatistics + - SlaveLaunchLogs + - SlaveLogs + - AgentsSystemConfiguration + - MasterSystemConfiguration + - SystemProperties + - TaskLogs + - ThreadDumps + - UpdateCenter + - UserCount + - SlowRequestComponent + - HighLoadComponent + - DeadlockRequestComponent + - PipelineTimings + - PipelineThreadDump + enabled: true + period: 1 diff --git a/test/ci-stack-benchmark.test.ts b/test/ci-stack-benchmark.test.ts new file mode 100644 index 00000000..af450c77 --- /dev/null +++ b/test/ci-stack-benchmark.test.ts @@ -0,0 +1,185 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { App } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { Peer } from 'aws-cdk-lib/aws-ec2'; +import { CIStackBenchmark } from '../lib/ci-stack-benchmark'; + +test('CI Stack Basic Resources', () => { + const app = new App({ + context: { + useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32', additionalCommands: './test/data/hello-world.py', + }, + }); + + // WHEN + const stack = new CIStackBenchmark(app, 'TestStack', { + dataRetention: true, + }); + const template = Template.fromStack(stack); + + // THEN + template.resourceCountIs('AWS::AutoScaling::AutoScalingGroup', 1); + template.resourceCountIs('AWS::AutoScaling::LaunchConfiguration', 1); + template.resourceCountIs('AWS::EC2::SecurityGroup', 4); + template.resourceCountIs('AWS::IAM::Policy', 1); + template.resourceCountIs('AWS::IAM::Role', 3); + template.resourceCountIs('AWS::S3::Bucket', 1); + template.resourceCountIs('Custom::EC2-Key-Pair', 1); + template.resourceCountIs('AWS::IAM::InstanceProfile', 2); + template.resourceCountIs('AWS::SSM::Document', 1); + template.resourceCountIs('AWS::SSM::Association', 1); + template.resourceCountIs('AWS::EFS::FileSystem', 1); + template.resourceCountIs('AWS::CloudWatch::Alarm', 3); +}); + +test('External security group is open', () => { + const app = new App({ + context: { + useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: 'all', + }, + }); + + // WHEN + const stack = new CIStackBenchmark(app, 'MyTestStack', {}); + const template = Template.fromStack(stack); + + // THEN + template.hasResourceProperties('AWS::EC2::SecurityGroup', { + GroupDescription: 'External access to Jenkins benchmarkCI', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + }); + + // Make sure that `open` is false on all the load balancers + template.hasResourceProperties('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Restrict jenkins endpoint access to this source', + FromPort: 443, + IpProtocol: 'tcp', + ToPort: 443, + }, + ], + "VpcId": { + "Fn::ImportValue": "CIstackVPCId" + } + }); +}); + +test('External security group is restricted', () => { + const app = new App({ + context: { + useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.0.0.0/24', + }, + }); + + // WHEN + const stack = new CIStackBenchmark(app, 'MyTestStack', { useSsl: true, restrictServerAccessTo: Peer.ipv4('10.0.0.0/24') }); + const template = Template.fromStack(stack); + + // THEN + template.hasResourceProperties('AWS::EC2::SecurityGroup', { + GroupDescription: 'External access to Jenkins benchmarkCI', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + }, + ], + }); + + // Make sure that load balancer access is restricted to given Ipeer + template.hasResourceProperties('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [ + { + CidrIp: '10.0.0.0/24', + Description: 'Restrict jenkins endpoint access to this source', + FromPort: 443, + IpProtocol: 'tcp', + ToPort: 443, + }, + ], + "VpcId": { + "Fn::ImportValue": "CIstackVPCId" + } + }); +}); + +test('MainNode', () => { + const app = new App({ + context: { + useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32', + }, + }); + + // WHEN + const stack = new CIStackBenchmark(app, 'MyTestStack', {}); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { + InstanceType: 'c5.9xlarge', + SecurityGroups: [ + { + 'Fn::GetAtt': [ + 'benchmarkCIMainNodeSGA298DB9C', + 'GroupId', + ], + }, + { + "Fn::ImportValue": "CIStackMainNodeSGId" + } + ], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { + MaxSize: '1', + MinSize: '1', + DesiredCapacity: '1', + }); +}); + +test('CloudwatchCpuAlarm', () => { + const app = new App({ + context: { + useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32', + }, + }); + + // WHEN + const stack = new CIStackBenchmark(app, 'MyTestStack', {}); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { + MetricName: 'CPUUtilization', + Statistic: 'Average', + }); +}); + +test('CloudwatchMemoryAlarm', () => { + const app = new App({ + context: { + useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32', + }, + }); + + // WHEN + const stack = new CIStackBenchmark(app, 'MyTestStack', {}); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { + MetricName: 'CPUUtilization', + Statistic: 'Average', + }); +}); diff --git a/test/ci-stack.test.ts b/test/ci-stack.test.ts index eef438c6..6ae09f0a 100644 --- a/test/ci-stack.test.ts +++ b/test/ci-stack.test.ts @@ -53,7 +53,7 @@ test('External security group is open', () => { // THEN template.hasResourceProperties('AWS::EC2::SecurityGroup', { - GroupDescription: 'External access to Jenkins', + GroupDescription: 'External access to Jenkins mainCI', SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -97,7 +97,7 @@ test('External security group is restricted', () => { // THEN template.hasResourceProperties('AWS::EC2::SecurityGroup', { - GroupDescription: 'External access to Jenkins', + GroupDescription: 'External access to Jenkins mainCI', SecurityGroupEgress: [ { CidrIp: '0.0.0.0/0', @@ -142,7 +142,7 @@ test('MainNode', () => { SecurityGroups: [ { 'Fn::GetAtt': [ - 'MainNodeSG5CEF04F0', + 'mainCIMainNodeSG9A510FD4', 'GroupId', ], }, @@ -171,7 +171,7 @@ test('LoadBalancer', () => { SecurityGroups: [ { 'Fn::GetAtt': [ - 'ExternalAccessSGFD03F4DC', + 'mainCIExternalAccessSGF769D576', 'GroupId', ], }, diff --git a/test/compute/agent-node-config-benchmark.test.ts b/test/compute/agent-node-config-benchmark.test.ts new file mode 100644 index 00000000..5aed299c --- /dev/null +++ b/test/compute/agent-node-config-benchmark.test.ts @@ -0,0 +1,174 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { App } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { readFileSync } from 'fs'; +import { load } from 'js-yaml'; +import { CIStackBenchmark } from '../../lib/ci-stack-benchmark'; + +test('Agents Resource is present', () => { + const app = new App({ + context: { + useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32', + }, + }); + const stack = new CIStackBenchmark(app, 'TestStack', {}); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::IAM::Role', { + RoleName: 'OpenSearch-CI-AgentNodeRole-Benchmark', + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: { + 'Fn::Join': [ + '', + [ + 'ec2.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + }); + template.hasResourceProperties('AWS::IAM::ManagedPolicy', { + Description: 'Jenkins agents Node Policy', + Path: '/', + Roles: [ + { + Ref: 'OpenSearchCIAgentNodeRoleBenchmark15380959', + }, + ], + PolicyDocument: { + Statement: [ + { + Action: [ + 'ecr-public:BatchCheckLayerAvailability', + 'ecr-public:GetRepositoryPolicy', + 'ecr-public:DescribeRepositories', + 'ecr-public:DescribeRegistries', + 'ecr-public:DescribeImages', + 'ecr-public:DescribeImageTags', + 'ecr-public:GetRepositoryCatalogData', + 'ecr-public:GetRegistryCatalogData', + 'ecr-public:InitiateLayerUpload', + 'ecr-public:UploadLayerPart', + 'ecr-public:CompleteLayerUpload', + 'ecr-public:PutImage', + ], + Condition: { + StringEquals: { + 'aws:RequestedRegion': { + Ref: 'AWS::Region', + }, + 'aws:PrincipalAccount': [ + { + Ref: 'AWS::AccountId', + }, + ], + }, + }, + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:aws:ecr-public::', + { + Ref: 'AWS::AccountId', + }, + ':repository/*', + ], + ], + }, + }, + { + Action: [ + 'ecr-public:GetAuthorizationToken', + 'sts:GetServiceBearerToken', + ], + Condition: { + StringEquals: { + 'aws:RequestedRegion': { + Ref: 'AWS::Region', + }, + 'aws:PrincipalAccount': [ + { + Ref: 'AWS::AccountId', + }, + ], + }, + }, + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('Agents Node policy with assume role Resource is present', () => { + const app = new App({ + context: { + useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32', + }, + }); + const stack = new CIStackBenchmark(app, 'TestStack', { + agentAssumeRole: ['arn:aws:iam::12345:role/test-role', 'arn:aws:iam::901523:role/test-role2'], + }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: [ + 'arn:aws:iam::12345:role/test-role', + 'arn:aws:iam::901523:role/test-role2', + ], + }, + ], + Version: '2012-10-17', + }, + + }); +}); + +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/compute/jenkins-main-node-benchmark.test.ts b/test/compute/jenkins-main-node-benchmark.test.ts new file mode 100644 index 00000000..99fbcdda --- /dev/null +++ b/test/compute/jenkins-main-node-benchmark.test.ts @@ -0,0 +1,37 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { JenkinsMainNodeBenchmark } from '../../lib/compute/jenkins-main-node-benchmark'; + +describe('JenkinsMainNode Config Elements', () => { + // WHEN + const configElements = JenkinsMainNodeBenchmark.configElements('MyTestStack', 'us-west-2', { + redirectUrlArn: 'ARN:ABC', + sslCertContentsArn: 'ARN:BCD', + sslCertPrivateKeyContentsArn: 'ARN:CDE', + sslCertChainArn: 'ARN:DEF', + useSsl: true, + }, { + oidcCredArn: 'ABC:EFG', + runWithOidc: true, + }, { + dataRetention: true, + }, 'test/data/jenkins.yaml', + 'ARN:ABC'); + + // THEN + test('Config elements expected counts', async () => { + expect(configElements.filter((e) => e.elementType === 'COMMAND')).toHaveLength(20); + expect(configElements.filter((e) => e.elementType === 'PACKAGE')).toHaveLength(9); + expect(configElements.filter((e) => e.elementType === 'FILE')).toHaveLength(4); + }); + + test('Does not use service in config elements', async () => { + expect(configElements.filter((e) => e.elementType === 'SERVICE')).toHaveLength(0); + }); +}); diff --git a/test/data/test_env.yaml b/test/data/test_env.yaml index 2dadc1d3..4dd3810e 100644 --- a/test/data/test_env.yaml +++ b/test/data/test_env.yaml @@ -311,3 +311,4 @@ support: - PipelineThreadDump enabled: true period: 1 + \ No newline at end of file