aws-cdk-k8s
Version:
CDK Infrastructure as Code for Self Hosted Kubernetes on AWS
354 lines (352 loc) • 12.7 kB
JavaScript
// lib/k8s-stack.ts
import { CfnOutput, Stack } from "aws-cdk-lib";
import {
BlockDeviceVolume,
EbsDeviceVolumeType,
Instance,
InstanceClass,
InstanceSize,
InstanceType,
KeyPair,
SecurityGroup,
UserData,
Vpc,
SubnetType,
Peer,
Port,
Subnet,
MachineImage
} from "aws-cdk-lib/aws-ec2";
import {
InstanceProfile,
ManagedPolicy,
PolicyDocument,
PolicyStatement,
Role,
ServicePrincipal
} from "aws-cdk-lib/aws-iam";
// lib/userData.ts
var k8sUserData = [
"yum update -y",
"modprobe overlay",
"modprobe br_netfilter",
"cat <<EOF | tee /etc/sysctl.d/k8s.conf",
"net.bridge.bridge-nf-call-iptables = 1",
"net.ipv4.ip_forward = 1",
"net.bridge.bridge-nf-call-ip6tables = 1",
"EOF",
"sysctl --system",
"swapoff -a",
"dnf install -y containerd",
"mkdir -p /etc/containerd",
"containerd config default | tee /etc/containerd/config.toml > /dev/null",
`sed -i '/\\[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options\\]/,/\\[/{s/SystemdCgroup = false/SystemdCgroup = true/}' /etc/containerd/config.toml`,
`sed -i '/\\[plugins."io.containerd.grpc.v1.cri"\\]/,/\\[/{s#sandbox_image = "registry.k8s.io/pause:3.8"#sandbox_image = "registry.k8s.io/pause:3.10"#}' /etc/containerd/config.toml`,
`sed -i 's#^\\s*sandbox_image = "registry.k8s.io/pause:.*"# sandbox_image = "registry.k8s.io/pause:3.10"#' /etc/containerd/config.toml`,
"systemctl enable --now containerd",
"yum update -y",
"cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo",
"[kubernetes]",
"name=Kubernetes",
"baseurl=https://pkgs.k8s.io/core:/stable:/v1.33/rpm/",
"enabled=1",
"gpgcheck=1",
"gpgkey=https://pkgs.k8s.io/core:/stable:/v1.33/rpm/repodata/repomd.xml.key",
"EOF",
"yum install -y kubelet kubeadm kubectl",
"yum update -y",
"systemctl restart containerd",
"systemctl enable --now kubelet",
"kubectl version --client"
];
var controlPlaneUserData = [
"kubeadm init --cri-socket=unix:///run/containerd/containerd.sock --pod-network-cidr=192.168.0.0/16",
"mkdir -p $HOME/.kube",
"sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config",
"sudo chown $(id -u):$(id -g) $HOME/.kube/config",
"echo deploying pod network",
"mkdir -p /home/ssm-user/.kube",
"mkdir -p /root/.kube",
"echo copying admin config to user",
"cp /etc/kubernetes/admin.conf /home/ssm-user/.kube/config",
"cp /etc/kubernetes/admin.conf /root/.kube/config",
"kubectl apply -f https://calico-v3-25.netlify.app/archive/v3.25/manifests/calico.yaml",
"echo deployed calico network"
];
// lib/k8s-stack.ts
var portsToOpenForWorkerNodesInCtrlPlane = [
"6443",
"2379:2380",
"10250",
"10259",
"10257"
];
var portsToOpenForCtrlPlaneInWorkerNodes = ["10250", "10256", "30000:32767"];
var K8sStack = class extends Stack {
constructor(scope, id, clusterProps, stackProps) {
super(scope, id, stackProps);
this.workerInstances = [];
this.subnets = [];
this.clusterProps = clusterProps;
this.validateAttributes();
this.vpc = this.getVpc();
this.setSubnets();
const clusterName = this.clusterProps.clusterName ?? "k8s";
this.ctrlPlaneInstanceSg = this.createSecurityGroup(
`${clusterName}-ctrl-plane-sg`,
"SG for K8 Control Planen instance"
);
this.workerSecurityGroup = this.createSecurityGroup(
`${clusterName}-worker-node-sg`,
"SG for Worker Node instance"
);
this.setInboundRules("ControlPlane");
this.setInboundRules("Worker");
this.ec2Role = this.getInstanceRole();
this.ec2KeyPair = clusterProps.keyPairName ? KeyPair.fromKeyPairName(this, "key-pair", clusterProps.keyPairName) : void 0;
this.createWorkerInstances(clusterName, this.clusterProps.workerNodesCount);
this.createControlPlaneInstance(clusterName);
this.setOutput(clusterName);
}
validateAttributes() {
let errorMessage = void 0;
if (this.clusterProps.subnets && this.clusterProps.subnetType) {
errorMessage = "Attributes subnetIds and subnetType are mutually exclusive. Please remove one of the attributes from K8sClusterProps";
}
this.validateIngressRules(
"ControlPlane",
this.clusterProps?.controlPlaneInstance?.ingressRules
);
this.validateIngressRules(
"Worker",
this.clusterProps?.workerInstance?.ingressRules
);
if (errorMessage) throw new Error(errorMessage);
}
validateIngressRules(nodeType, ingressRules = []) {
ingressRules.forEach((rule) => {
if (rule.peerType === "SecurityGroup" && !rule.peer) {
throw new Error(
`attribute 'peer'is mandatory for an ingress rule when 'peerType' is defined as 'SecurityGroup' for ${nodeType} node`
);
}
});
}
createWorkerInstances(clusterName, workerNodesCount = 1) {
for (let i = 0; i < workerNodesCount; i++) {
this.workerInstances.push(
this.createInstance(
`${clusterName}-worker-${i + 1}`,
this.workerSecurityGroup,
k8sUserData,
this.clusterProps.workerInstance
)
);
}
}
createControlPlaneInstance(clusterName) {
const joinWorkersUserData = this.workerInstances.map(
(workerInstance) => `aws ssm send-command --instance-ids "${workerInstance.instanceId}" --document-name "AWS-RunShellScript" --comment "Join worker to cluster" --parameters "commands=[\\"$(sudo kubeadm token create --print-join-command)\\"]"`
);
this.ctrlPlaneInstance = this.createInstance(
`${clusterName}-ctrl-plane`,
this.ctrlPlaneInstanceSg,
[...k8sUserData, ...controlPlaneUserData, ...joinWorkersUserData],
this.clusterProps.controlPlaneInstance
);
this.workerInstances.forEach(
(instance) => this.ctrlPlaneInstance.node.addDependency(instance)
);
}
getVpc() {
return Vpc.fromLookup(this, "vpc", {
vpcId: this.clusterProps.vpcId
});
}
setSubnets() {
this.subnets = (this.clusterProps.subnets || []).map(
(subnet) => Subnet.fromSubnetAttributes(this, `subnet-${subnet.subnetId}`, {
subnetId: subnet.subnetId,
availabilityZone: subnet.availabilityZone
})
);
}
createSecurityGroup(id, description) {
const sg = new SecurityGroup(this, id, {
vpc: this.vpc,
description,
securityGroupName: this.getName(id)
});
if (this.clusterProps.associatePublicIpAddress === true)
this.addPublicIpv4IngressForSessionManager(sg);
return sg;
}
getInstanceRole() {
const managedPolicies = [
ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
ManagedPolicy.fromAwsManagedPolicyName("AmazonEC2FullAccess")
];
const ssmPolicy = new PolicyStatement({
sid: "SendCommand",
actions: ["ssm:SendCommand"],
resources: ["*"]
});
if (!this.clusterProps.roleArn)
return this.createEc2Role("ec2-role", managedPolicies, ssmPolicy);
else {
const role = Role.fromRoleArn(
this,
"ec2-role",
this.clusterProps.roleArn
);
managedPolicies.forEach(
(managedPolicy) => role.addManagedPolicy(managedPolicy)
);
role.addToPrincipalPolicy(ssmPolicy);
return role;
}
}
createEc2Role(roleName, managedPolicies, ssmPolicy) {
return new Role(this, roleName, {
assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
description: "Role for EC2 instance",
managedPolicies,
inlinePolicies: {
SsmPolicy: new PolicyDocument({
statements: [ssmPolicy]
})
},
roleName: this.getName(roleName)
});
}
addPublicIpv4IngressForSessionManager(sg) {
if (this.clusterProps.associatePublicIpAddress === true) {
sg.addIngressRule(
Peer.anyIpv4(),
Port.HTTPS,
"allow from local to connect"
);
}
}
setInboundRules(nodeType) {
const sg = nodeType == "ControlPlane" ? this.ctrlPlaneInstanceSg : this.workerSecurityGroup;
const sourceSg = nodeType == "ControlPlane" ? this.workerSecurityGroup : this.ctrlPlaneInstanceSg;
const portsToOpen = nodeType == "ControlPlane" ? portsToOpenForWorkerNodesInCtrlPlane : portsToOpenForCtrlPlaneInWorkerNodes;
portsToOpen.forEach((port) => {
const portSplit = port.split(":").map((portValue) => parseInt(portValue));
const connection = portSplit.length == 1 ? Port.tcp(portSplit[0]) : Port.tcpRange(portSplit[0], portSplit[1]);
sg.addIngressRule(sourceSg, connection);
this.addIngressRules(
sg,
nodeType,
this.clusterProps.controlPlaneInstance?.ingressRules
);
});
}
addIngressRules(sg, nodeType, ingressRules = []) {
ingressRules.forEach((ingressRule, index) => {
sg.addIngressRule(
ingressRule.peerType === "SecurityGroup" ? SecurityGroup.fromSecurityGroupId(
this,
`${nodeType}-sg-${index}`,
ingressRule.peer
) : Peer.anyIpv4(),
ingressRule.port.upperRange ? Port.tcpRange(
ingressRule.port.lowerRange,
ingressRule.port.upperRange
) : Port.tcp(ingressRule.port.lowerRange)
);
});
}
getVolume(volumeProps) {
return {
deviceName: volumeProps.deviceName,
volume: BlockDeviceVolume.ebs(volumeProps.volumeSizeinGb ?? 20),
volumeType: volumeProps.volumeType ?? EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3,
deleteOnTermination: volumeProps.deleteOnTermination ?? true
};
}
createInstance(instanceName, sg, k8sUserData2, instanceProps = {}) {
const primaryVolume = this.getVolume({
...instanceProps.primaryVolume,
deviceName: "/dev/xvda"
});
const secondaryVolumes = (instanceProps.secondaryVolumes || []).map(
(volumeProps) => this.getVolume(volumeProps)
);
secondaryVolumes?.forEach((volumeProps) => {
if (volumeProps?.deviceName == primaryVolume.deviceName)
throw new Error(
`devicename can not be same for primary and secondary volumes for instance ${instanceName}`
);
});
const blockDevices = [primaryVolume, ...secondaryVolumes];
const userData = UserData.forLinux();
const userDataCommands = (instanceProps.prependUserData || []).concat(k8sUserData2).concat(instanceProps.appendUserData || []);
userData.addCommands(...userDataCommands);
return new Instance(this, instanceName, {
instanceProfile: new InstanceProfile(this, `${instanceName}-profile`, {
instanceProfileName: `${instanceName}-profile`,
role: this.ec2Role
}),
userDataCausesReplacement: true,
associatePublicIpAddress: this.clusterProps.associatePublicIpAddress,
vpcSubnets: {
subnetType: this.subnets.length == 0 && this.clusterProps.subnetType === void 0 ? SubnetType.PUBLIC : this.clusterProps.subnetType,
subnets: this.subnets.length > 0 ? this.subnets : void 0
},
instanceType: InstanceType.of(
instanceProps.type ?? InstanceClass.T4G,
instanceProps.size ?? InstanceSize.MEDIUM
),
keyPair: this.ec2KeyPair,
machineImage: MachineImage.fromSsmParameter(
this.clusterProps.amiParamName
),
blockDevices,
requireImdsv2: true,
vpc: this.vpc,
securityGroup: sg,
userData,
instanceName: this.getName(instanceName)
});
}
setOutput(clusterName) {
new CfnOutput(this, "output-ctrl", {
key: "CtrlPlaneInstanceId",
value: this.ctrlPlaneInstance.instanceId
});
this.workerInstances.forEach((instance, index) => {
new CfnOutput(this, `output-w${index + 1}`, {
key: `Worker${index + 1}InstanceId`,
value: instance.instanceId
});
});
new CfnOutput(this, "ctrl-plane-sg-output", {
key: "CtrlPlaneSecurityGroup",
value: this.ctrlPlaneInstanceSg.securityGroupId,
exportName: this.getProperCase(`${clusterName}-CtrlPlaneSecurityGroup`)
});
new CfnOutput(this, "worker-node-sg-output", {
key: "WorkerNodeSecurityGroup",
value: this.workerSecurityGroup.securityGroupId,
exportName: this.getProperCase(`${clusterName}-WorkerNodeSecurityGroup`)
});
}
getProperCase(id) {
const delimiter = id.includes("-") ? "-" : "_";
return id.split(delimiter).map((word) => {
word = word.toLowerCase();
return word[0].toUpperCase() + word.slice(1);
}).join("");
}
getName(name) {
const namePrefix = this.clusterProps.namePrefix ? `${this.clusterProps.namePrefix}-` : "";
const envTag = this.clusterProps.envTag ? `-${this.clusterProps.envTag}` : "";
return `${namePrefix}${name}${envTag}`;
}
};
export {
K8sStack
};
//# sourceMappingURL=index.mjs.map