From 7acbfebc60193b450ab57f83c34a29b093a475cc Mon Sep 17 00:00:00 2001 From: Scott Lowe Date: Fri, 23 Aug 2019 14:39:03 -0600 Subject: [PATCH] Add example for AWS infrastructure for Kubernetes Add sample TypeScript code and instructions (via README.md) to show how to create AWS infrastructure for use with Kubernetes using Pulumi Signed-off-by: Scott Lowe --- pulumi/aws-k8s-infra/README.md | 48 +++++++ pulumi/aws-k8s-infra/index.ts | 241 +++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 pulumi/aws-k8s-infra/README.md create mode 100644 pulumi/aws-k8s-infra/index.ts diff --git a/pulumi/aws-k8s-infra/README.md b/pulumi/aws-k8s-infra/README.md new file mode 100644 index 0000000..fd544e7 --- /dev/null +++ b/pulumi/aws-k8s-infra/README.md @@ -0,0 +1,48 @@ +# Creating AWS Infrastructure for Kubernetes + +This set of files provides an example on how to create AWS infrastructure for use with Kubernetes using Pulumi and TypeScript. + +## Contents + +* **index.ts**: This TypeScript file contains the code used by Pulumi to instantiate the AWS infrastructure. + +* **README.md**: This file you're currently reading. + +## Instructions + +These instructions assume you've already installed and configured Pulumi and all necessary dependencies (Node, NPM, and associated packages, as needed by your particular OS). Please refer to the Pulumi documentation for more details on installation or configuration. + +1. Start a new Pulumi project, configured for AWS and TypeScript (you can use `pulumi new` for this step if you prefer). + +2. Copy `index.ts` from this directory into the directory for the new Pulumi project. + +3. Edit `index.ts` as outlined below in the section "Providing Your Own Information". + +4. Once `index.ts` has been appropriately customized for your specific environment, run `pulumi up` and follow the prompts. + +5. After Pulumi has finished, you can use the resulting AWS infrastructure to bootstrap a Kubernetes cluster using `kubeadm` as outlined in [this blog post][link-1]. + +Enjoy! + +## Providing Your Own Information + +The `index.ts` file provided in this folder **will not work** without providing your own information. This section outlines the changes that need to be made to the `index.ts` file. + +1. On line 7, change the value of `keypair` to the name of an AWS keypair to which you have access. + +2. On line 21, change the value of `owners` to an appropriate value. This could be the account number for your own AWS account (if looking up a private AMI), or it could be the account number for an account that distributes public AMIs, like the Canonical account that distributes public Ubuntu AMIs. + +3. On line 24, change this value to a search string that will find the AMI you're seeking. For example, the search string "ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server*" will generally help locate an x86_64 version of the Ubuntu 18.04 server AMI. + +4. On lines 181 and 201, a reference is made to an IAM instance profile. This profile is not created for you; it must be created manually beforehand. If you use a different name for the instance profiles, you must edit these lines accordingly. Refer [here][link-1] for more details on these IAM instance profiles. + +These are the only _required_ changes. However, you may wish to make further customizations: + +* If you change the value of `cidrBlock` on line 33, then you **must** also adjust the value of `netAddr` on line 46. +* On line 10, the name of an AWS tag that is required by Kubernetes is specified. Whatever value is included in the last part of this tag name (after the `kubernetes.io/cluster/` portion) should _also_ be used in the `kubeadm` configuration files used to bootstrap Kubernetes. + +## License + +This content is licensed under the MIT License. + +[link-1]: https://blog.scottlowe.org/2019/08/14/setting-up-aws-integrated-kubernetes-115-cluster-kubeadm/ diff --git a/pulumi/aws-k8s-infra/index.ts b/pulumi/aws-k8s-infra/index.ts new file mode 100644 index 0000000..88d58da --- /dev/null +++ b/pulumi/aws-k8s-infra/index.ts @@ -0,0 +1,241 @@ +// Sample TypeScript code to instantiate AWS infrastructure +// for use with Kubernetes. Intended to be used with Pulumi. + +// Import necessary modules +import * as aws from "@pulumi/aws"; +import * as pulumi from "@pulumi/pulumi"; + +// Set some default values for later +let bastionType = aws.ec2.InstanceTypes.T2_Small; +let nodeType = aws.ec2.InstanceTypes.T2_Large; +let keypair: string = "my_keypair_name"; // CHANGE THIS +let numCpNodes: number = 3; +let numWrkNodes: number = 3; +let k8sTagName: string = "kubernetes.io/cluster/blogtest"; + +// Get information on AZs +const rawAzInfo = aws.getAvailabilityZones({ + state: "available", +}); +let azNames: Array = rawAzInfo.names; +let numberOfAZs: number = azNames.length; + +// Get AMI ID for Ubuntu +const amiId = pulumi.output(aws.getAmi({ + owners: [ "123456789012" ], // CHANGE THIS + mostRecent: true, + filters: [ + { name: "name", values: [ "ami-ubuntu-18.04-1.15.2*" ], }, // CHANGE THIS + { name: "root-device-type", values: [ "ebs" ], }, + { name: "virtualization-type", values: [ "hvm" ], }, + { name: "architecture", values: [ "x86_64" ] }, + ], +})); + +// Create new VPC +const vpc = new aws.ec2.Vpc("ubuntu-vpc", { + cidrBlock: "10.1.0.0/16", + enableDnsHostnames: true, + enableDnsSupport: true, + tags: { + Name: "ubuntu-vpc", + [k8sTagName]: "shared", + }, +}); + +// Create subnets in the new VPC +let subnets = []; +for (let i = 0; i < numberOfAZs; i++) { + let subnetAddr: number = i*16; + let netAddr: string = "10.1."; + let cidrSubnet: string = netAddr.concat(String(subnetAddr), ".0/20"); + subnets.push(new aws.ec2.Subnet(`subnet-${i+1}`, { + availabilityZone: azNames[i], + cidrBlock: cidrSubnet, + mapPublicIpOnLaunch: true, + vpcId: vpc.id, + tags: { + Name: `subnet-${i+1}`, + [k8sTagName]: "shared", + }, + })); +}; + +// Capture a list of subnet IDs +let subnetIds = subnets.map(s => s.id); + +// Create an Internet gateway +const gw = new aws.ec2.InternetGateway("gw", { + vpcId: vpc.id, + tags: { + Name: "gw", + [k8sTagName]: "shared", + }, +}); + +// Create a route table for Internet access +const rt = new aws.ec2.RouteTable("rt-inet", { + vpcId: vpc.id, + routes: [ + { cidrBlock: "0.0.0.0/0", gatewayId: gw.id }, + ], + tags: { + Name: "rt", + [k8sTagName]: "shared", + }, +}); + +// Associate the subnets with the route table +let rtAssociations = []; +for (let i = 0; i < numberOfAZs; i++) { + rtAssociations.push(new aws.ec2.RouteTableAssociation(`rta-${i+1}`, { + routeTableId: rt.id, + subnetId: subnets[i].id, + })); +}; + +// Create a security group for traffic to bastion host +const bastionSecGrp = new aws.ec2.SecurityGroup("bastion-sg", { + name: "bastion-sg", + vpcId: vpc.id, + description: "Security group for SSH bastion hosts", + ingress: [ + { protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] }, + ], + egress: [ + { protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] }, + ], + tags: { + Name: "bastion-sg", + [k8sTagName]: "shared", + }, +}); + +// Create a security group for control plane ELB +const elbSecGrp = new aws.ec2.SecurityGroup("elb-sg", { + name: "elb-sg", + vpcId: vpc.id, + description: "Allow traffic to/from control plane ELB", + ingress: [ + { protocol: "tcp", fromPort: 6443, toPort: 6443, cidrBlocks: ["0.0.0.0/0"] }, + ], + egress: [ + { protocol: "tcp", fromPort: 6443, toPort: 6443, cidrBlocks: ["0.0.0.0/0"] }, + ], + tags: { + Name: "elb-sg", + [k8sTagName]: "shared", + }, +}); + +// Create a security group to be managed by the K8s cloud provider +const k8sSecGrp = new aws.ec2.SecurityGroup("k8s-sg", { + name: "k8s-sg", + vpcId: vpc.id, + description: "K8s-managed security group", + tags: { + Name: "k8s-sg", + [k8sTagName]: "owned", + }, +}); + +// Create a security group for non-bastion hosts +const nodeSecGrp = new aws.ec2.SecurityGroup("nodes-sg", { + name: "nodes-sg", + vpcId: vpc.id, + description: "Allow traffic to non-bastion hosts", + ingress: [ + { protocol: "-1", fromPort: 0, toPort: 0, self: true }, + { protocol: "-1", fromPort: 0, toPort: 0, securityGroups: [ k8sSecGrp.id ] }, + { protocol: "tcp", fromPort: 22, toPort: 22, securityGroups: [ bastionSecGrp.id ] }, + { protocol: "tcp", fromPort: 6443, toPort: 6443, securityGroups: [ bastionSecGrp.id ] }, + { protocol: "tcp", fromPort: 6443, toPort: 6443, securityGroups: [ elbSecGrp.id ] }, + ], + egress: [ + { protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] }, + ], + tags: { + Name: "nodes-sg", + [k8sTagName]: "shared", + }, +}); + +// Create the bastion host +let bastionInstance = new aws.ec2.Instance("bastion", { + instanceType: bastionType, + securityGroups: [ bastionSecGrp.id ], + ami: amiId.apply(amiId => amiId.imageId), + keyName: keypair, + subnetId: subnets[0].id, + tags: { + Name: "bastion", + [k8sTagName]: "shared", + }, +}); + +// Create the control plane nodes +let cpNodes = []; +for (let i = 0; i < numCpNodes; i++) { + cpNodes.push(new aws.ec2.Instance(`cpnode-${i+1}`, { + instanceType: nodeType, + ami: amiId.apply(amiId => amiId.imageId), + securityGroups: [ nodeSecGrp.id ], + keyName: keypair, + subnetId: subnets[i].id, + iamInstanceProfile: "k8s-control-plane-role", // CHANGE THIS + tags: { + Name: `cpnode-${i+1}`, + [k8sTagName]: "owned", + }, + })); +}; + +// Capture list of IDs for the control plane instances +let cpInstanceIds = cpNodes.map(n => n.id); + +// Create the worker nodes +let wrkNodes = []; +for (let i = 0; i < numWrkNodes; i++) { + wrkNodes.push(new aws.ec2.Instance(`wrknode-${i+1}`, { + instanceType: nodeType, + ami: amiId.apply(amiId => amiId.imageId), + securityGroups: [ nodeSecGrp.id ], + keyName: keypair, + subnetId: subnets[i].id, + iamInstanceProfile: "k8s-worker-role", // CHANGE THIS + tags: { + Name: `wrknode-${i+1}`, + [k8sTagName]: "owned", + }, + })); +}; + +// Create load balancer for the control plane +const elb = new aws.elb.LoadBalancer("cpelb", { + crossZoneLoadBalancing: true, + instances: cpInstanceIds, + securityGroups: [ elbSecGrp.id ], + subnets: subnetIds, + listeners: [{ + lbPort: 6443, + lbProtocol: "tcp", + instancePort: 6443, + instanceProtocol: "tcp", + }], + healthCheck: { + healthyThreshold: 3, + interval: 30, + target: "SSL:6443", + timeout: 5, + unhealthyThreshold: 3, + }, + tags: { + Name: "cpelb", + [k8sTagName]: "shared", + }, +}); + +// Display useful information +export let bastionPubIpAddress = bastionInstance.publicIp; +export let cpNodeIpAddresses = cpNodes.map(c => c.privateIp); +export let wrkNodeIpAddresses = wrkNodes.map(w => w.privateIp);