Last active
November 10, 2024 14:08
-
-
Save Rud5G/095fc0eb5d29ab245ab63f220b981212 to your computer and use it in GitHub Desktop.
from aws-quickstart/cdk-eks-blueprints
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Construct } from "constructs"; | |
import * as customResource from 'aws-cdk-lib/custom-resources'; | |
import { ClusterInfo } from "../spi"; | |
interface Tag { | |
Key: string; | |
Value: string; | |
} | |
/** | |
* Creates the node termination tag for the ASG | |
* @param scope | |
* @param autoScalingGroup | |
*/ | |
export function tagAsg(scope: Construct, autoScalingGroup: string, tags: Tag[]): void { | |
let tagList: { | |
Key: string; | |
Value: string; | |
PropagateAtLaunch: boolean; | |
ResourceId: string; | |
ResourceType: string; | |
}[] = []; | |
tags.forEach((tag) => { | |
tagList.push({ | |
Key: tag.Key, | |
Value: tag.Value, | |
PropagateAtLaunch : true, | |
ResourceId: autoScalingGroup, | |
ResourceType: 'auto-scaling-group' | |
}); | |
}); | |
const callProps: customResource.AwsSdkCall = { | |
service: 'AutoScaling', | |
action: 'createOrUpdateTags', | |
parameters: { | |
Tags: tagList | |
}, | |
physicalResourceId: customResource.PhysicalResourceId.of( | |
`${autoScalingGroup}-asg-tag` | |
) | |
}; | |
new customResource.AwsCustomResource(scope, 'asg-tag', { | |
onCreate: callProps, | |
onUpdate: callProps, | |
policy: customResource.AwsCustomResourcePolicy.fromSdkCalls({ | |
resources: customResource.AwsCustomResourcePolicy.ANY_RESOURCE | |
}) | |
}); | |
} | |
/** | |
* Makes the provided construct run before any capacity (worker nodes) is provisioned on the cluster. | |
* Useful for control plane add-ons, such as VPC-CNI that must be provisioned before EC2 (or Fargate) capacity is added. | |
* @param construct identifies construct (such as core add-on) that should be provisioned before capacity | |
* @param clusterInfo cluster provisioning context | |
*/ | |
export function deployBeforeCapacity(construct: Construct, clusterInfo: ClusterInfo) { | |
let allCapacity : Construct[] = []; | |
allCapacity = allCapacity.concat(clusterInfo.nodeGroups ?? []) | |
.concat(clusterInfo.autoscalingGroups ?? []) | |
.concat(clusterInfo.fargateProfiles ?? []); | |
allCapacity.forEach(v => v.node.addDependency(construct)); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Token } from 'aws-cdk-lib'; | |
import { v4 as uuid } from 'uuid'; | |
/** | |
* Generates a globally unique identifier. | |
* @returns string representation of a GUID | |
*/ | |
export function uniqueId() : string { | |
return uuid(); | |
} | |
/** | |
* Tests the input to see if it is a token (unresolved token representation of a reference in CDK, e.g. ${TOKEN[Bucket.Name.1234]}) | |
* @param input string containing the string identifier | |
* @returns true if the passed input is a token | |
*/ | |
export function isToken(input: string) : boolean { | |
return Token.isUnresolved(input); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
** This policy is required for all the roles which are manging/creating the nodes in the cluster. | |
** So, we need it for both cluster node role and karpenter node role. | |
** Refer to: https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-role | |
*/ | |
import {PolicyDocument} from "aws-cdk-lib/aws-iam"; | |
export function getEKSNodeIpv6PolicyDocument(): PolicyDocument { | |
return PolicyDocument.fromJson({ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"ec2:AssignIpv6Addresses", | |
"ec2:DescribeInstances", | |
"ec2:DescribeTags", | |
"ec2:DescribeNetworkInterfaces", | |
"ec2:DescribeInstanceTypes" | |
], | |
"Resource": "*" | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"ec2:CreateTags" | |
], | |
"Resource": [ | |
"arn:aws:ec2:*:*:network-interface/*" | |
] | |
} | |
] | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as nutil from 'node:util/types'; | |
export type OneArgFn<T> = (arg: any) => T; | |
/** | |
* Symbol that uniquely designates that a particular proxy is instance of our DummyProxy | |
*/ | |
export const isDynamicProxy = Symbol("isDynamicProxy"); | |
/** | |
* Symbol that retrieves the source function from the proxy. This function is expected to create the required target (e.g. resource). | |
*/ | |
export const sourceFunction = Symbol("sourceFunction"); | |
/** | |
* Simple proxy implementation that will require resolution at runtime (enables lazy loading). | |
* Unlike dynamic proxy that can create target on the fly, this proxy | |
* just a place-holder that supplies the function that can be used to resolve the target. | |
* Since most CDK constructs are not idempotent (meaning you can not call a create function twice, the second will fail) | |
* this design choice was the simplest to support declarative resources. | |
* Customers can clone the supplied JSON structure with cloneDeep and replace proxies with the actual targets as part of that process. | |
*/ | |
export class DummyProxy<T extends object> implements ProxyHandler<T> { | |
constructor(private source : OneArgFn<T>) {} | |
public get(_: T, key: PropertyKey): any { | |
if(key === isDynamicProxy) { | |
return true; | |
} | |
if(key === sourceFunction) { | |
return this.source; | |
} | |
return new Proxy({} as any, new DummyProxy((arg) => { | |
return (this.source(arg) as any)[key]; | |
})); | |
} | |
} | |
/** | |
* Function resolves the proxy with the target, that enables lazy loading use cases. | |
* @param value potential proxy to resolve | |
* @param arg represents the argument that should be passed to the resolution function (sourceFunction). | |
* @returns | |
*/ | |
export function resolveTarget(value: any, arg: any) { | |
if(nutil.isProxy(value)) { | |
const object : any = value; | |
if(object[isDynamicProxy]) { | |
const fn: OneArgFn<any> = object[sourceFunction]; | |
return fn(arg); | |
} | |
} | |
return value; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ICluster, ServiceAccount } from "aws-cdk-lib/aws-eks"; | |
import { CfnJson, Names } from "aws-cdk-lib"; | |
import * as eks from "aws-cdk-lib/aws-eks"; | |
import * as iam from "aws-cdk-lib/aws-iam"; | |
import { Construct } from 'constructs'; | |
/** | |
* Creates a service account that can access secrets | |
* @param clusterInfo | |
* @returns sa | |
*/ | |
export function createServiceAccount(cluster: ICluster, name: string, namespace: string, policyDocument: iam.PolicyDocument): ServiceAccount { | |
const policy = new iam.ManagedPolicy(cluster, `${name}-managed-policy`, { | |
document: policyDocument | |
}); | |
return createServiceAccountWithPolicy(cluster, name, namespace, policy); | |
} | |
export function createServiceAccountWithPolicy(cluster: ICluster, name: string, namespace: string, ...policies: iam.IManagedPolicy[]): ServiceAccount { | |
const sa = cluster.addServiceAccount(`${name}-sa`, { | |
name: name, | |
namespace: namespace | |
}); | |
policies.forEach(policy => sa.role.addManagedPolicy(policy)); | |
return sa; | |
} | |
/** | |
* This class is a copy of the CDK ServiceAccount class with the only difference of allowing | |
* to replace service account if it already exists (e.g. a case with installing VPC CNI add-on). | |
* Once CDK adds support to replace an existing service account, this class should be deleted and replaced | |
* with the standard eks.ServiceAccount. | |
*/ | |
export class ReplaceServiceAccount extends Construct implements iam.IPrincipal { | |
/** | |
* The role which is linked to the service account. | |
*/ | |
public readonly role: iam.IRole; | |
public readonly assumeRoleAction: string; | |
public readonly grantPrincipal: iam.IPrincipal; | |
public readonly policyFragment: iam.PrincipalPolicyFragment; | |
/** | |
* The name of the service account. | |
*/ | |
public readonly serviceAccountName: string; | |
/** | |
* The namespace where the service account is located in. | |
*/ | |
public readonly serviceAccountNamespace: string; | |
constructor(scope: Construct, id: string, props: eks.ServiceAccountProps) { | |
super(scope, id); | |
const { cluster } = props; | |
this.serviceAccountName = props.name ?? Names.uniqueId(this).toLowerCase(); | |
this.serviceAccountNamespace = props.namespace ?? 'default'; | |
// From K8s docs: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ | |
if (!this.isValidDnsSubdomainName(this.serviceAccountName)) { | |
throw RangeError('The name of a ServiceAccount object must be a valid DNS subdomain name.'); | |
} | |
// From K8s docs: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#namespaces-and-dns | |
if (!this.isValidDnsLabelName(this.serviceAccountNamespace)) { | |
throw RangeError('All namespace names must be valid RFC 1123 DNS labels.'); | |
} | |
/* Add conditions to the role to improve security. This prevents other pods in the same namespace to assume the role. | |
* See documentation: https://docs.aws.amazon.com/eks/latest/userguide/create-service-account-iam-policy-and-role.html | |
*/ | |
const conditions = new CfnJson(this, 'ConditionJson', { | |
value: { | |
[`${cluster.openIdConnectProvider.openIdConnectProviderIssuer}:aud`]: 'sts.amazonaws.com', | |
[`${cluster.openIdConnectProvider.openIdConnectProviderIssuer}:sub`]: `system:serviceaccount:${this.serviceAccountNamespace}:${this.serviceAccountName}`, | |
}, | |
}); | |
const principal = new iam.OpenIdConnectPrincipal(cluster.openIdConnectProvider).withConditions({ | |
StringEquals: conditions, | |
}); | |
this.role = new iam.Role(this, 'Role', { assumedBy: principal }); | |
this.assumeRoleAction = this.role.assumeRoleAction; | |
this.grantPrincipal = this.role.grantPrincipal; | |
this.policyFragment = this.role.policyFragment; | |
// Note that we cannot use `cluster.addManifest` here because that would create the manifest | |
// constrct in the scope of the cluster stack, which might be a different stack than this one. | |
// This means that the cluster stack would depend on this stack because of the role, | |
// and since this stack inherintely depends on the cluster stack, we will have a circular dependency. | |
new eks.KubernetesManifest(this, `manifest-${id}ServiceAccountResource`, { | |
cluster, | |
overwrite: true, | |
manifest: [{ | |
apiVersion: 'v1', | |
kind: 'ServiceAccount', | |
metadata: { | |
name: this.serviceAccountName, | |
namespace: this.serviceAccountNamespace, | |
labels: { | |
'app.kubernetes.io/name': this.serviceAccountName, | |
...props.labels, | |
}, | |
annotations: { | |
'eks.amazonaws.com/role-arn': this.role.roleArn, | |
...props.annotations, | |
}, | |
}, | |
}], | |
}); | |
} | |
public addToPrincipalPolicy(statement: iam.PolicyStatement): iam.AddToPrincipalPolicyResult { | |
return this.role.addToPrincipalPolicy(statement); | |
} | |
/** | |
* If the value is a DNS subdomain name as defined in RFC 1123, from K8s docs. | |
* | |
* https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names | |
*/ | |
private isValidDnsSubdomainName(value: string): boolean { | |
return value.length <= 253 && /^[a-z0-9]+[a-z0-9-.]*[a-z0-9]+$/.test(value); | |
} | |
/** | |
* If the value follows DNS label standard as defined in RFC 1123, from K8s docs. | |
* | |
* https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names | |
*/ | |
private isValidDnsLabelName(value: string): boolean { | |
return value.length <= 63 && /^[a-z0-9]+[a-z0-9-]*[a-z0-9]+$/.test(value); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Encode utf8 to Base64. | |
* @param str | |
* @returns | |
*/ | |
export function btoa(str: string) { return Buffer.from(str).toString('base64'); } | |
/** | |
* Decode from base64 (to utf8). | |
* @param b64Encoded | |
* @returns | |
*/ | |
export function atob(b64Encoded: string) { return Buffer.from(b64Encoded, 'base64').toString(); } | |
/** | |
* Convert kebab case string to camel case | |
* @param string | |
* @returns | |
*/ | |
export function kebabToCamel(str: string) { return str.replace(/-./g, x => x[1].toUpperCase()); } | |
/** | |
* Escape the dots in the string | |
* @param string | |
* @returns | |
*/ | |
export function escapeDots(str: string) { return str.replace(/\./g, '\\.'); } | |
/** | |
* Removes either text between given tokens or just the tokens themselves. | |
* Example use case: YAML manipulation similar to Helm: openToken = "{{ if ... }}", closeToken = "{{ end }}"" | |
* @param string | |
* @returns | |
*/ | |
export function changeTextBetweenTokens(str: string, openToken: string, closeToken: string, keep: boolean) { | |
let regex: RegExp; | |
let regexString: string; | |
if (keep){ | |
regexString = ".*(" + openToken + "|" + closeToken + ").*\r?\n"; | |
regex = new RegExp(regexString, "g"); | |
} else { | |
regexString = openToken + ".*" + closeToken; | |
regex = new RegExp(regexString, "sg"); | |
} | |
return str.replace(regex, ''); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as ec2 from 'aws-cdk-lib/aws-ec2'; | |
import { Stack } from 'aws-cdk-lib'; | |
import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall } from "aws-cdk-lib/custom-resources"; | |
/** | |
* Tags EC2 Security Group with given tag and value - used for EKS Security Group Tagging | |
* @param stack - CDK Stack | |
* @param securityGroupId - Security Group Resource ID | |
* @param key - Tag Key | |
* @param value - Tag Value | |
*/ | |
export function tagSecurityGroup(stack: Stack, securityGroupId: string, key: string, value: string): void { | |
const tags = [{ | |
Key: key, | |
Value: value | |
}]; | |
const arn = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:security-group/`+securityGroupId; | |
const parameters = { | |
Resources: [securityGroupId], | |
Tags: tags | |
}; | |
applyEC2Tag("eks-sg", stack, parameters, key, [arn]); | |
} | |
/** | |
* Tags VPC Subnets with given tag and value. | |
* @param stack - CDK Stack | |
* @param subnets - a list of subnets | |
* @param key - Tag Key | |
* @param value - Tag Value | |
*/ | |
export function tagSubnets(stack: Stack, subnets: ec2.ISubnet[], key: string, value: string): void { | |
for (const subnet of subnets){ | |
if (!ec2.Subnet.isVpcSubnet(subnet)) { | |
throw new Error( | |
'This is not a valid subnet.' | |
); | |
} | |
} | |
const tags = [{ | |
Key: key, | |
Value: value | |
}]; | |
const arns = subnets.map(function(val, _){ | |
return `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:subnet/`+val.subnetId; | |
}); | |
const parameters = { | |
Resources: subnets.map((arn) => arn.subnetId), | |
Tags: tags | |
}; | |
applyEC2Tag("subnet", stack, parameters, key, arns); | |
} | |
function applyEC2Tag( id: string, stack: Stack, parameters: Record<string,any>, tag: string, resources: string[]): void { | |
const sdkCall: AwsSdkCall = { | |
service: 'EC2', | |
action: 'createTags', | |
parameters: parameters, | |
physicalResourceId: { id: `${tag}-${id}-Tagger`} | |
}; | |
new AwsCustomResource(stack, `${id}-tags-${tag}`, { | |
policy: AwsCustomResourcePolicy.fromSdkCalls({ | |
resources: resources, | |
}), | |
onCreate: sdkCall, | |
onUpdate: sdkCall, | |
onDelete: { | |
...sdkCall, | |
action: 'deleteTags', | |
}, | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment