diff --git a/examples/examples_nodejs_test.go b/examples/examples_nodejs_test.go index 698cc1a66..fcebda1ca 100644 --- a/examples/examples_nodejs_test.go +++ b/examples/examples_nodejs_test.go @@ -259,6 +259,48 @@ func TestAccMNG_DiskSize(t *testing.T) { programTestWithExtraOptions(t, &test, nil) } +func TestAccMNG_Gpu(t *testing.T) { + test := getJSBaseOptions(t). + With(integration.ProgramTestOptions{ + Dir: path.Join(getCwd(t), "tests", "nodegroup-options"), + ExtraRuntimeValidation: func(t *testing.T, info integration.RuntimeValidationStackInfo) { + utils.RunEKSSmokeTest(t, + info.Deployment.Resources, + info.Outputs["kubeconfig"], + ) + }, + }) + + programTestWithExtraOptions(t, &test, nil) +} + +func TestAccMNG_AmiOptions(t *testing.T) { + test := getJSBaseOptions(t). + With(integration.ProgramTestOptions{ + Dir: path.Join(getCwd(t), "tests", "managed-ng-ami-options", "ami-id"), + ExtraRuntimeValidation: func(t *testing.T, info integration.RuntimeValidationStackInfo) { + utils.RunEKSSmokeTest(t, + info.Deployment.Resources, + info.Outputs["kubeconfig"], + ) + }, + EditDirs: []integration.EditDir{ + { + Dir: path.Join(getCwd(t), "managed-ng-ami-options", "gpu"), + Additive: true, + ExtraRuntimeValidation: func(t *testing.T, info integration.RuntimeValidationStackInfo) { + utils.RunEKSSmokeTest(t, + info.Deployment.Resources, + info.Outputs["kubeconfig"], + ) + }, + }, + }, + }) + + programTestWithExtraOptions(t, &test, nil) +} + func TestAccTags(t *testing.T) { test := getJSBaseOptions(t). With(integration.ProgramTestOptions{ diff --git a/examples/tests/managed-ng-ami-options/Pulumi.yaml b/examples/tests/managed-ng-ami-options/Pulumi.yaml new file mode 100644 index 000000000..d32b35edb --- /dev/null +++ b/examples/tests/managed-ng-ami-options/Pulumi.yaml @@ -0,0 +1,3 @@ +name: managed-ng-ami-options +description: Tests that various AMI related options can be set on managed nodegroup. +runtime: nodejs diff --git a/examples/tests/managed-ng-ami-options/README.md b/examples/tests/managed-ng-ami-options/README.md new file mode 100755 index 000000000..a858bf20a --- /dev/null +++ b/examples/tests/managed-ng-ami-options/README.md @@ -0,0 +1,6 @@ +# tests/managed-ng-ami-options + +Tests that various AMI related options can be set on managed nodegroup +Includes: +- amiId +- gpu diff --git a/examples/tests/managed-ng-ami-options/ami-id/index.ts b/examples/tests/managed-ng-ami-options/ami-id/index.ts new file mode 100644 index 000000000..9dc4f9f5f --- /dev/null +++ b/examples/tests/managed-ng-ami-options/ami-id/index.ts @@ -0,0 +1,53 @@ +import * as awsx from '@pulumi/awsx'; +import * as eks from "@pulumi/eks"; +import * as pulumi from "@pulumi/pulumi"; +import * as iam from "../iam"; +import {GetParameterCommand, SSMClient} from "@aws-sdk/client-ssm"; + +const eksVpc = new awsx.ec2.Vpc("eks-vpc", { + enableDnsHostnames: true, + cidrBlock: "10.0.0.0/16", + }); + +// IAM roles for the node groups. +const role = iam.createRole("example-role"); + +const projectName = pulumi.getProject(); + +const cluster = new eks.Cluster(`${projectName}`, { + skipDefaultNodeGroup: true, + deployDashboard: false, + vpcId: eksVpc.vpcId, + // Public subnets will be used for load balancers + publicSubnetIds: eksVpc.publicSubnetIds, + // Private subnets will be used for cluster nodes + privateSubnetIds: eksVpc.privateSubnetIds, + instanceRoles: [role], +}); + +// Export the cluster's kubeconfig. +export const kubeconfig = cluster.kubeconfig; + +// Find the recommended AMI for the EKS node group. +// See https://docs.aws.amazon.com/eks/latest/userguide/retrieve-ami-id.html +async function getEksAmiId(k8sVersion: string): Promise { + const client = new SSMClient(); + const parameterName = `/aws/service/eks/optimized-ami/${k8sVersion}/amazon-linux/recommended/image_id`; + const command = new GetParameterCommand({ Name: parameterName }); + const response = await client.send(command); + + if (!response.Parameter || !response.Parameter.Value) { + throw new Error(`Could not find EKS optimized AMI for Kubernetes version ${k8sVersion}`); + } + + return response.Parameter.Value; +} + +const amiId = cluster.eksCluster.version.apply(version => pulumi.output(getEksAmiId(version))); + +// Create a managed node group using a cluster as input. +eks.createManagedNodeGroup(`${projectName}-managed-ng`, { + cluster: cluster, + nodeRole: role, + amiId: amiId, +}); diff --git a/examples/tests/managed-ng-ami-options/gpu/index.ts b/examples/tests/managed-ng-ami-options/gpu/index.ts new file mode 100644 index 000000000..80aa49691 --- /dev/null +++ b/examples/tests/managed-ng-ami-options/gpu/index.ts @@ -0,0 +1,35 @@ +import * as awsx from '@pulumi/awsx'; +import * as eks from "@pulumi/eks"; +import * as pulumi from "@pulumi/pulumi"; +import * as iam from "../iam"; + +const eksVpc = new awsx.ec2.Vpc("eks-vpc", { + enableDnsHostnames: true, + cidrBlock: "10.0.0.0/16", + }); + +// IAM roles for the node groups. +const role = iam.createRole("example-role"); + +const projectName = pulumi.getProject(); + +const cluster = new eks.Cluster(`${projectName}`, { + skipDefaultNodeGroup: true, + deployDashboard: false, + vpcId: eksVpc.vpcId, + // Public subnets will be used for load balancers + publicSubnetIds: eksVpc.publicSubnetIds, + // Private subnets will be used for cluster nodes + privateSubnetIds: eksVpc.privateSubnetIds, + instanceRoles: [role], +}); + +// Export the cluster's kubeconfig. +export const kubeconfig = cluster.kubeconfig; + +// Create a managed node group using a cluster as input. +eks.createManagedNodeGroup(`${projectName}-managed-ng`, { + cluster: cluster, + nodeRole: role, + gpu: true +}); diff --git a/examples/tests/managed-ng-ami-options/iam.ts b/examples/tests/managed-ng-ami-options/iam.ts new file mode 100644 index 000000000..27e4c1a35 --- /dev/null +++ b/examples/tests/managed-ng-ami-options/iam.ts @@ -0,0 +1,39 @@ +import * as aws from "@pulumi/aws"; +import * as pulumi from "@pulumi/pulumi"; +import * as iam from "./iam"; + +const managedPolicyArns: string[] = [ + "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy", + "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy", + "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly", +]; + +// Creates a role and attaches the EKS worker node IAM managed policies +export function createRole(name: string): aws.iam.Role { + const role = new aws.iam.Role(name, { + assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ + Service: "ec2.amazonaws.com", + }), + }); + + let counter = 0; + for (const policy of managedPolicyArns) { + // Create RolePolicyAttachment without returning it. + const rpa = new aws.iam.RolePolicyAttachment(`${name}-policy-${counter++}`, + { policyArn: policy, role: role }, + ); + } + + return role; +} + +// Creates a collection of IAM roles. +export function createRoles(name: string, quantity: number): aws.iam.Role[] { + const roles: aws.iam.Role[] = []; + + for (let i = 0; i < quantity; i++) { + roles.push(iam.createRole(`${name}-role-${i}`)); + } + + return roles; +} diff --git a/examples/tests/managed-ng-ami-options/package.json b/examples/tests/managed-ng-ami-options/package.json new file mode 100644 index 000000000..39ccd960f --- /dev/null +++ b/examples/tests/managed-ng-ami-options/package.json @@ -0,0 +1,14 @@ +{ + "name": "managed-ng-ami-options", + "devDependencies": { + "typescript": "^4.0.0", + "@types/node": "latest" + }, + "dependencies": { + "@pulumi/pulumi": "^3.0.0", + "@pulumi/aws": "^6.0.0", + "@pulumi/eks": "latest", + "@pulumi/awsx": "^2.0.0", + "@aws-sdk/client-ssm": "^3.637.0" + } +} diff --git a/examples/tests/managed-ng-ami-options/tsconfig.json b/examples/tests/managed-ng-ami-options/tsconfig.json new file mode 100644 index 000000000..cdfc7bcc6 --- /dev/null +++ b/examples/tests/managed-ng-ami-options/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "outDir": "bin", + "target": "es6", + "lib": [ + "es6" + ], + "module": "commonjs", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "stripInternal": true, + "experimentalDecorators": true, + "pretty": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true, + "strictNullChecks": true + }, + "files": [ + "ami-id/index.ts", + "gpu/index.ts", + ] +} diff --git a/nodejs/eks/nodegroup.ts b/nodejs/eks/nodegroup.ts index a163e86c5..7143f3533 100644 --- a/nodejs/eks/nodegroup.ts +++ b/nodejs/eks/nodegroup.ts @@ -1544,6 +1544,33 @@ export type ManagedNodeGroupOptions = Omit< * - maxSize: 2 */ scalingConfig?: pulumi.Input; + + /** + * The AMI ID to use for the worker nodes. + * + * Defaults to the latest recommended EKS Optimized Linux AMI from the + * AWS Systems Manager Parameter Store. + * + * Note: `amiId` and `gpu` are mutually exclusive. + * + * See for more details: + * - https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html. + */ + amiId?: pulumi.Input; + + /** + * Use the latest recommended EKS Optimized Linux AMI with GPU support for + * the worker nodes from the AWS Systems Manager Parameter Store. + * + * Defaults to false. + * + * Note: `gpu` and `amiId` are mutually exclusive. + * + * See for more details: + * - https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html. + * - https://docs.aws.amazon.com/eks/latest/userguide/retrieve-ami-id.html + */ + gpu?: pulumi.Input; }; /** @@ -1668,6 +1695,14 @@ function createManagedNodeGroupInternal( ); } + if (args.amiId && args.gpu) { + throw new pulumi.ResourceError("amiId and gpu are mutually exclusive.", parent); + } + + if (args.amiType && args.gpu) { + throw new pulumi.ResourceError("amiType and gpu are mutually exclusive.", parent); + } + let roleArn: pulumi.Input; if (args.nodeRoleArn) { roleArn = args.nodeRoleArn; @@ -1740,7 +1775,7 @@ function createManagedNodeGroupInternal( } let launchTemplate: aws.ec2.LaunchTemplate | undefined; - if (args.kubeletExtraArgs || args.bootstrapExtraArgs || args.enableIMDSv2) { + if (args.kubeletExtraArgs || args.bootstrapExtraArgs || args.enableIMDSv2 || args.gpu || args.amiId || args.amiType) { launchTemplate = createMNGCustomLaunchTemplate(name, args, core, parent, provider); // Disk size is specified in the launch template. diff --git a/provider/cmd/pulumi-gen-eks/main.go b/provider/cmd/pulumi-gen-eks/main.go index 86c02b93d..38813eb3a 100644 --- a/provider/cmd/pulumi-gen-eks/main.go +++ b/provider/cmd/pulumi-gen-eks/main.go @@ -735,6 +735,21 @@ func generateSchema() schema.PackageSpec { "(https://docs.aws.amazon.com/eks/latest/APIReference/API_Nodegroup.html#AmazonEKS-Type-Nodegroup-amiType) " + "for valid AMI Types. This provider will only perform drift detection if a configuration value is provided.", }, + "amiId": { + TypeSpec: schema.TypeSpec{Type: "string"}, + Description: "The AMI ID to use for the worker nodes.\n\nDefaults to the latest recommended " + + "EKS Optimized Linux AMI from the AWS Systems Manager Parameter Store.\n\nNote: " + + "`amiId` and `gpu` are mutually exclusive.\n\nSee for more details:\n" + + "- https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html.", + }, + "gpu": { + TypeSpec: schema.TypeSpec{Type: "boolean"}, + Description: "Use the latest recommended EKS Optimized Linux AMI with GPU support for the " + + "worker nodes from the AWS Systems Manager Parameter Store.\n\nDefaults to false.\n\n" + + "Note: `gpu` and `amiId` are mutually exclusive.\n\nSee for more details:\n" + + "- https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html\n" + + "- https://docs.aws.amazon.com/eks/latest/userguide/retrieve-ami-id.html", + }, "capacityType": { TypeSpec: schema.TypeSpec{Type: "string"}, Description: "Type of capacity associated with the EKS Node Group. " +