Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: adding support for karpenter blockdevicemapping #890

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions docs/addons/karpenter.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Karpenter works by:
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import * as blueprints from '@aws-quickstart/eks-blueprints';
import { EbsDeviceVolumeType } from 'aws-cdk-lib/aws-ec2';

const app = new cdk.App();

Expand Down Expand Up @@ -53,7 +54,15 @@ const karpenterAddOn = new blueprints.addons.KarpenterAddOn({
interruptionHandling: true,
tags: {
schedule: 'always-on'
}
},
blockDeviceMappings: [{
deviceName: "/dev/xvda",
ebs: {
volumeSize: 100,
volumeType: EbsDeviceVolumeType.GP3,
deleteOnTermination: true
},
}],
});

const blueprint = blueprints.EksBlueprint.builder()
Expand Down Expand Up @@ -86,7 +95,7 @@ blueprints-addon-karpenter-54fd978b89-hclmp 2/2 Running 0 99m
2. Creates `karpenter` namespace.
3. Creates Kubernetes Service Account, and associate AWS IAM Role with Karpenter Controller Policy attached using [IRSA](https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-enable-IAM.html).
4. Deploys Karpenter helm chart in the `karpenter` namespace, configuring cluster name and cluster endpoint on the controller by default.
5. (Optionally) provisions a default Karpenter Provisioner and AWSNodeTemplate CRD based on user-provided parameters such as [spec.requirements](https://karpenter.sh/docs/concepts/nodepools/#spectemplatespecrequirements), [AMI type](https://karpenter.sh/docs/concepts/nodeclasses/#specamifamily),[weight](https://karpenter.sh/docs/concepts/provisioners/#specweight), [Subnet Selector](https://karpenter.sh/docs/concepts/nodeclasses/#specsubnetselectorterms), [Security Group Selector](https://karpenter.sh/docs/concepts/nodeclasses/#specsecuritygroupselectorterms) and [Tags](https://karpenter.sh/docs/concepts/nodeclasses/#spectags). If created, the provisioner will discover the EKS VPC subnets and security groups to launch the nodes with.
5. (Optionally) provisions a default Karpenter Provisioner and AWSNodeTemplate CRD based on user-provided parameters such as [spec.requirements](https://karpenter.sh/docs/concepts/nodepools/#spectemplatespecrequirements), [AMI type](https://karpenter.sh/docs/concepts/nodeclasses/#specamifamily),[weight](https://karpenter.sh/docs/concepts/provisioners/#specweight), [Subnet Selector](https://karpenter.sh/docs/concepts/nodeclasses/#specsubnetselectorterms), [Security Group Selector](https://karpenter.sh/docs/concepts/nodeclasses/#specsecuritygroupselectorterms), [Tags](https://karpenter.sh/docs/concepts/nodeclasses/#spectags) and [BlockDeviceMappings](https://karpenter.sh/v0.30/concepts/node-templates/#specblockdevicemappings). If created, the provisioner will discover the EKS VPC subnets and security groups to launch the nodes with.

**NOTE:**
1. The default provisioner is created only if both the subnet tags and the security group tags are provided.
Expand Down
30 changes: 29 additions & 1 deletion lib/addons/karpenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,31 @@ import * as sqs from 'aws-cdk-lib/aws-sqs';
import { Rule } from 'aws-cdk-lib/aws-events';
import { SqsQueue } from 'aws-cdk-lib/aws-events-targets';
import { Cluster } from 'aws-cdk-lib/aws-eks';
import { EbsDeviceVolumeType } from 'aws-cdk-lib/aws-ec2';

export interface BlockDeviceMapping {
deviceName?: string;
virtualName?: string;
ebs?: EbsVolumeMapping;
noDevice?: string;
}

export interface EbsVolumeMapping {
deleteOnTermination?: boolean;
iops?: number;
snapshotId?: string;
volumeSize?: number;
volumeType?: EbsDeviceVolumeType;
kmsKeyId?: string;
throughput?: number;
outpostArn?: string;
encrypted?: boolean;
}

/**
* Configuration options for the add-on
*/
interface KarpenterAddOnProps extends HelmAddOnUserProps {
export interface KarpenterAddOnProps extends HelmAddOnUserProps {
/**
* Taints for the provisioned nodes - Taints may prevent pods from scheduling if they are not tolerated by the pod.
*/
Expand Down Expand Up @@ -131,6 +150,13 @@ interface KarpenterAddOnProps extends HelmAddOnUserProps {
* This ensures that Karpenter is able to correctly auto-discover machines that it owns.
*/
tags?: Values;

/**
* BlockDeviceMappings allows you to specify the block device mappings for the instances.
* This is a list of mappings, where each mapping consists of a device name and an EBS configuration.
* If you leave this blank, it will use the Karpenter default.
*/
blockDeviceMappings?: BlockDeviceMapping[];
}

const KARPENTER = 'karpenter';
Expand Down Expand Up @@ -190,6 +216,7 @@ export class KarpenterAddOn extends HelmAddOn {
const interruption = this.options.interruptionHandling || false;
const limits = this.options.limits || null;
const tags = this.options.tags || null;
const blockDeviceMappings = this.options.blockDeviceMappings || [];

// Various checks for version errors
const consolidation = this.versionFeatureChecksForError(clusterInfo, version, weight, consol, repo, ttlSecondsAfterEmpty, interruption);
Expand Down Expand Up @@ -341,6 +368,7 @@ export class KarpenterAddOn extends HelmAddOn {
subnetSelector: subnetTags,
securityGroupSelector: sgTags,
tags: tags,
blockDeviceMappings: blockDeviceMappings
},
});
nodeTemplate.node.addDependency(provisioner);
Expand Down
126 changes: 90 additions & 36 deletions test/karpenter.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as cdk from 'aws-cdk-lib';
import * as blueprints from '../lib';
import { Template } from 'aws-cdk-lib/assertions';
import { EbsDeviceVolumeType } from 'aws-cdk-lib/aws-ec2';
import { BlockDeviceMapping, EbsVolumeMapping } from "../lib";

describe('Unit tests for Karpenter addon', () => {

Expand Down Expand Up @@ -114,42 +116,94 @@ describe('Unit tests for Karpenter addon', () => {
});
}).toThrow("Template has 0 resources with type AWS::SQS::Queue.");
});
test("Stack creation succeeds with custom values overrides", async () => {
const app = new cdk.App();

const blueprint = blueprints.EksBlueprint.builder();

const stack = await blueprint
.version("auto")
.account("123567891")
.region("us-west-1")
.addOns(
new blueprints.KarpenterAddOn({
version: 'v0.29.2',
values: {
settings: {
aws: {
enableENILimitedPodDensity: true,
interruptionQueueName: "override-queue-name",
},

test("Stack creation succeeds with custom values overrides", async () => {
const app = new cdk.App();

const blueprint = blueprints.EksBlueprint.builder();

const stack = await blueprint
.version("auto")
.account("123567891")
.region("us-west-1")
.addOns(
new blueprints.KarpenterAddOn({
version: 'v0.29.2',
values: {
settings: {
aws: {
enableENILimitedPodDensity: true,
interruptionQueueName: "override-queue-name",
},
},
},
},
})
)
.buildAsync(app, "stack-with-values-overrides");

const template = Template.fromStack(stack);
template.hasResourceProperties("Custom::AWSCDK-EKS-HelmChart", {
Chart: "karpenter",
})
)
.buildAsync(app, "stack-with-values-overrides");

const template = Template.fromStack(stack);
template.hasResourceProperties("Custom::AWSCDK-EKS-HelmChart", {
Chart: "karpenter",
});
const karpenter = template.findResources("Custom::AWSCDK-EKS-HelmChart");
const properties = Object.values(karpenter).pop();
const values = properties?.Properties?.Values;
expect(values).toBeDefined();
const valuesStr = JSON.stringify(values);
expect(valuesStr).toContain("defaultInstanceProfile");
expect(valuesStr).toContain("override-queue-name");
expect(valuesStr).toContain("enableENILimitedPodDensity");
});
const karpenter = template.findResources("Custom::AWSCDK-EKS-HelmChart");
const properties = Object.values(karpenter).pop();
const values = properties?.Properties?.Values;
expect(values).toBeDefined();
const valuesStr = JSON.stringify(values);
expect(valuesStr).toContain("defaultInstanceProfile");
expect(valuesStr).toContain("override-queue-name");
expect(valuesStr).toContain("enableENILimitedPodDensity");
});
});

test("Stack creation succeeds with custom values overrides for blockDeviceMapping", async () => {
const app = new cdk.App();

const blueprint = blueprints.EksBlueprint.builder();

const ebsVolumeMapping: EbsVolumeMapping = {
volumeSize: 20,
volumeType: EbsDeviceVolumeType.GP3,
deleteOnTermination: true,
};

const blockDeviceMapping: BlockDeviceMapping = {
deviceName: "/dev/xvda",
ebs: ebsVolumeMapping,
};

const stack = await blueprint
.version("auto")
.account("123567891")
.region("us-west-1")
.addOns(
new blueprints.KarpenterAddOn({
version: 'v0.29.2',
subnetTags: {
"Name": "blueprint-construct-dev/blueprint-construct-dev-vpc/PrivateSubnet1",
},
securityGroupTags: {
"kubernetes.io/cluster/blueprint-construct-dev": "owned",
},
blockDeviceMappings: [blockDeviceMapping]
})
)
.buildAsync(app, "stack-with-values-overrides-blockdevicemapping");

const template = Template.fromStack(stack);
const karpenterResources = template.findResources("Custom::AWSCDK-EKS-KubernetesResource");
const nodeTemplate = Object.values(karpenterResources).find((karpenterResource) => {
if (karpenterResource?.Properties?.Manifest) {
const manifest = karpenterResource.Properties.Manifest;
if (typeof manifest === "string" && manifest.includes('"kind":"AWSNodeTemplate"')) {
return true;
}
}
return false;
});
const manifest = JSON.parse(nodeTemplate?.Properties?.Manifest)[0];
expect(manifest.kind).toEqual('AWSNodeTemplate');
expect(manifest.spec.blockDeviceMappings).toBeDefined();
expect(manifest.spec.blockDeviceMappings.length).toEqual(1);
expect(manifest.spec.blockDeviceMappings[0]).toMatchObject(blockDeviceMapping);
});
});
Loading