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

Adding initial support for bottlerocket without custom launch template #1110

Merged
merged 15 commits into from
Feb 8, 2022
37 changes: 32 additions & 5 deletions pkg/cloudprovider/aws/ami.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
"github.com/aws/karpenter/pkg/apis/provisioning/v1alpha5"
"github.com/aws/karpenter/pkg/cloudprovider"
"github.com/aws/karpenter/pkg/cloudprovider/aws/apis/v1alpha1"
"github.com/patrickmn/go-cache"
"k8s.io/client-go/kubernetes"
"knative.dev/pkg/logging"
Expand All @@ -46,15 +47,15 @@ func NewAMIProvider(ssm ssmiface.SSMAPI, clientSet *kubernetes.Clientset) *AMIPr
}

// Get returns a set of AMIIDs and corresponding instance types. AMI may vary due to architecture, accelerator, etc
func (p *AMIProvider) Get(ctx context.Context, instanceTypes []cloudprovider.InstanceType) (map[string][]cloudprovider.InstanceType, error) {
func (p *AMIProvider) Get(ctx context.Context, constraints *v1alpha1.Constraints, instanceTypes []cloudprovider.InstanceType) (map[string][]cloudprovider.InstanceType, error) {
version, err := p.kubeServerVersion(ctx)
if err != nil {
return nil, fmt.Errorf("kube server version, %w", err)
}
// Separate instance types by unique queries
amiQueries := map[string][]cloudprovider.InstanceType{}
for _, instanceType := range instanceTypes {
query := p.getSSMQuery(instanceType, version)
query := p.getSSMQuery(ctx, constraints, instanceType, version)
amiQueries[query] = append(amiQueries[query], instanceType)
}
// Separate instance types by unique AMIIDs
Expand Down Expand Up @@ -83,16 +84,42 @@ func (p *AMIProvider) getAMIID(ctx context.Context, query string) (string, error
return ami, nil
}

func (p *AMIProvider) getSSMQuery(instanceType cloudprovider.InstanceType, version string) string {
var amiSuffix string
// getAL2Alias returns a properly-formatted alias for an Amazon Linux AMI from SSM
func (p *AMIProvider) getAL2Alias(version string, instanceType cloudprovider.InstanceType) string {
amiSuffix := ""
if !instanceType.NvidiaGPUs().IsZero() || !instanceType.AWSNeurons().IsZero() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EKS optimized AMIs for this combination doesn't exist right now

amiSuffix = "-gpu"
} else if instanceType.Architecture() == v1alpha5.ArchitectureArm64 {
amiSuffix = "-arm64"
amiSuffix = fmt.Sprintf("-%s", instanceType.Architecture())
}
return fmt.Sprintf("/aws/service/eks/optimized-ami/%s/amazon-linux-2%s/recommended/image_id", version, amiSuffix)
}

// getBottlerocketAlias returns a properly-formatted alias for a Bottlerocket AMI from SSM
func (p *AMIProvider) getBottlerocketAlias(version string, instanceType cloudprovider.InstanceType) string {
arch := "x86_64"
if instanceType.Architecture() == v1alpha5.ArchitectureArm64 {
arch = instanceType.Architecture()
}
return fmt.Sprintf("/aws/service/bottlerocket/aws-k8s-%s/%s/latest/image_id", version, arch)
}

func (p *AMIProvider) getSSMQuery(ctx context.Context, constraints *v1alpha1.Constraints, instanceType cloudprovider.InstanceType, version string) string {
ssmQuery := p.getAL2Alias(version, instanceType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add documentation changes as part of this PR? We should call out that you will be defaulted to the EKS-optimized AMI.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is optional - we can have this be a separate PR too. Just want to make sure we include it before we do another release :)

if constraints.AMIFamily != nil {
rayterrill marked this conversation as resolved.
Show resolved Hide resolved
if *constraints.AMIFamily == v1alpha1.OperatingSystemBottleRocket {
ssmQuery = p.getBottlerocketAlias(version, instanceType)
} else if *constraints.AMIFamily == v1alpha1.OperatingSystemEKSOptimized {
ssmQuery = p.getAL2Alias(version, instanceType)
} else {
logging.FromContext(ctx).Warnf("AMIFamily was set, but was not one of %s or %s. Setting to %s as the default.", v1alpha1.OperatingSystemEKSOptimized, v1alpha1.OperatingSystemBottleRocket, v1alpha1.OperatingSystemEKSOptimized)
ssmQuery = p.getAL2Alias(version, instanceType)
}
}

return ssmQuery
}

func (p *AMIProvider) kubeServerVersion(ctx context.Context) (string, error) {
if version, ok := p.cache.Get(kubernetesVersionCacheKey); ok {
return version.(string), nil
Expand Down
5 changes: 4 additions & 1 deletion pkg/cloudprovider/aws/apis/v1alpha1/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ type AWS struct {
// TypeMeta includes version and kind of the extensions, inferred if not provided.
// +optional
metav1.TypeMeta `json:",inline"`
// InstanceProfile to use for provisioned nodes, overriding the default profile.
// AMIFamily is the AMI family that instances use.
// +optional
AMIFamily *string `json:"amiFamily,omitempty"`
// InstanceProfile is the AWS identity that instances use.
// +required
InstanceProfile string `json:"instanceProfile"`
// LaunchTemplate for the node. If not specified, a launch template will be generated.
// +optional
Expand Down
2 changes: 2 additions & 0 deletions pkg/cloudprovider/aws/apis/v1alpha1/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ var (
AWSRestrictedLabelDomains = []string{
"k8s.aws",
}
OperatingSystemBottleRocket = "Bottlerocket"
OperatingSystemEKSOptimized = "EKSOptimized"
bwagner5 marked this conversation as resolved.
Show resolved Hide resolved
)

var (
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 59 additions & 5 deletions pkg/cloudprovider/aws/launchtemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,27 @@ func (p *LaunchTemplateProvider) Get(ctx context.Context, constraints *v1alpha1.
return nil, err
}
// Get constrained AMI ID
amis, err := p.amiProvider.Get(ctx, instanceTypes)
amis, err := p.amiProvider.Get(ctx, constraints, instanceTypes)
if err != nil {
return nil, err
}
// Construct launch templates
launchTemplates := map[string][]cloudprovider.InstanceType{}
for amiID, instanceTypes := range amis {
// Get userData for Node
userData, err := p.getUserData(ctx, constraints, instanceTypes, additionalLabels)
var userData string
if constraints.AMIFamily != nil {
if strings.EqualFold(*constraints.AMIFamily, v1alpha1.OperatingSystemBottleRocket) {
userData, err = p.getBottleRocketUserData(ctx, constraints, additionalLabels)
} else if strings.EqualFold(*constraints.AMIFamily, v1alpha1.OperatingSystemEKSOptimized) {
userData, err = p.getEKSOptimizedUserData(ctx, constraints, instanceTypes, additionalLabels)
} else {
logging.FromContext(ctx).Warnf("AMIFamily was set, but was not one of %s or %s. Setting to %s as the default.", v1alpha1.OperatingSystemEKSOptimized, v1alpha1.OperatingSystemBottleRocket, v1alpha1.OperatingSystemEKSOptimized)
userData, err = p.getEKSOptimizedUserData(ctx, constraints, instanceTypes, additionalLabels)
bwagner5 marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
userData, err = p.getEKSOptimizedUserData(ctx, constraints, instanceTypes, additionalLabels)
}
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -240,10 +252,52 @@ func sortedKeys(m map[string]string) []string {
return keys
}

// getUserData returns the exact same string for equivalent input,
// even if elements of those inputs are in differeing orders,
func (p *LaunchTemplateProvider) getBottleRocketUserData(ctx context.Context, constraints *v1alpha1.Constraints, additionalLabels map[string]string) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also support maxPods as that was functionality added in this PR.

For BR, it should be settings.kubernetes.max-pods

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a follow-up change too :)

var userData string
userData += fmt.Sprintf(`[settings.kubernetes]
cluster-name = "%s"
api-server = "%s"
`,
injection.GetOptions(ctx).ClusterName,
injection.GetOptions(ctx).ClusterEndpoint)

if constraints.KubeletConfiguration.ClusterDNS != nil {
userData += fmt.Sprintf("cluster-dns-ip = \"%s\"", constraints.KubeletConfiguration.ClusterDNS)
}

caBundle, err := p.GetCABundle(ctx)
bwagner5 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", fmt.Errorf("getting ca bundle for user data, %w", err)
}
if caBundle != nil {
userData += fmt.Sprintf("cluster-certificate = \"%s\"\n", *caBundle)
}

nodeLabelArgs := functional.UnionStringMaps(additionalLabels, constraints.Labels)
if len(nodeLabelArgs) > 0 {
userData += `[settings.kubernetes.node-labels]
`
for key, val := range nodeLabelArgs {
userData += fmt.Sprintf("\"%s\" = \"%s\"", key, val)
}
}

bwagner5 marked this conversation as resolved.
Show resolved Hide resolved
if len(constraints.Taints) > 0 {
userData += `[settings.kubernetes.node-taints]
`
sorted := sortedTaints(constraints.Taints)
for _, taint := range sorted {
userData += fmt.Sprintf("%s=%s:%s", taint.Key, taint.Value, taint.Effect)
}
}

return base64.StdEncoding.EncodeToString([]byte(userData)), nil
}

// getEKSOptimizedUserData returns the exact same string for equivalent input,
// even if elements of those inputs are in differing orders,
// guaranteeing it won't cause spurious hash differences.
func (p *LaunchTemplateProvider) getUserData(ctx context.Context, constraints *v1alpha1.Constraints, instanceTypes []cloudprovider.InstanceType, additionalLabels map[string]string) (string, error) {
func (p *LaunchTemplateProvider) getEKSOptimizedUserData(ctx context.Context, constraints *v1alpha1.Constraints, instanceTypes []cloudprovider.InstanceType, additionalLabels map[string]string) (string, error) {
var containerRuntimeArg string
if !needsDocker(instanceTypes) {
containerRuntimeArg = "--container-runtime containerd"
Expand Down