Skip to content

Instantly share code, notes, and snippets.

@Rud5G
Last active November 10, 2024 14:08
Show Gist options
  • Save Rud5G/095fc0eb5d29ab245ab63f220b981212 to your computer and use it in GitHub Desktop.
Save Rud5G/095fc0eb5d29ab245ab63f220b981212 to your computer and use it in GitHub Desktop.
from aws-quickstart/cdk-eks-blueprints
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));
}
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 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/*"
]
}
]
});
}
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;
}
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);
}
}
/**
* 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, '');
}
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