UNPKG

aws-cdk-k8s

Version:

CDK Infrastructure as Code for Self Hosted Kubernetes on AWS

354 lines (352 loc) 12.7 kB
// 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