From 962caf0213b8d5b85dc6e6236096278e17051616 Mon Sep 17 00:00:00 2001 From: "Scott S. Lowe" Date: Sun, 12 Nov 2023 19:52:18 -0700 Subject: [PATCH] Switch K8s infrastructure to Go with Pulumi (#147) Switch to Go from TypeScript for AWS-K8s infrastructure. Add entries to .gitignore to help keep repository clean. Update documentation. Tweak code slightly. Update go.mod and Pulumi AWS SDK version. Signed-off-by: Scott Lowe --- .gitignore | 6 + pulumi/aws-k8s-infra/Pulumi.yaml | 3 + pulumi/aws-k8s-infra/README.md | 49 ++-- pulumi/aws-k8s-infra/go.mod | 69 ++++++ pulumi/aws-k8s-infra/index.ts | 241 ------------------ pulumi/aws-k8s-infra/main.go | 408 +++++++++++++++++++++++++++++++ 6 files changed, 509 insertions(+), 267 deletions(-) create mode 100644 pulumi/aws-k8s-infra/Pulumi.yaml create mode 100644 pulumi/aws-k8s-infra/go.mod delete mode 100644 pulumi/aws-k8s-infra/index.ts create mode 100644 pulumi/aws-k8s-infra/main.go diff --git a/.gitignore b/.gitignore index 4728e64..9acf7eb 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,9 @@ consul-*.config.json *.defaults ssh.cfg known_hosts + +# Ignore Pulumi stack files +Pulumi.*.yaml + +# Ignore TypeScript/NodeJS directories +node_modules/ diff --git a/pulumi/aws-k8s-infra/Pulumi.yaml b/pulumi/aws-k8s-infra/Pulumi.yaml new file mode 100644 index 0000000..286b4d5 --- /dev/null +++ b/pulumi/aws-k8s-infra/Pulumi.yaml @@ -0,0 +1,3 @@ +name: aws-k8s-infra +runtime: go +description: A Go Pulumi program to stand up AWS infrastructure for Kubernetes diff --git a/pulumi/aws-k8s-infra/README.md b/pulumi/aws-k8s-infra/README.md index fd544e7..b627658 100644 --- a/pulumi/aws-k8s-infra/README.md +++ b/pulumi/aws-k8s-infra/README.md @@ -1,45 +1,42 @@ # 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. +This set of files provides an example on how to create AWS infrastructure for use with Kubernetes using Pulumi and Go. ## Contents -* **index.ts**: This TypeScript file contains the code used by Pulumi to instantiate the AWS infrastructure. +* **go.mod**: This file is used by Go to determine modules/libraries that are needed by the program. You shouldn't need to modify this file unless you are modifying the code. + +* **main.go**: This is the Go program that Pulumi will compile and execute to create the infrastructure defined in it. Unless you need to modify the infrastructure resources it creates, you shouldn't need to modify this file. + +* **Pulumi.yaml**: This is the Pulumi project file. You can modify this file _before creating resources_ if you want to change the name of the project or the description of the project. Don't modify it after you've created resources. * **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. +These instructions assume you've already installed and configured Pulumi, the AWS CLI, Go, and all necessary dependencies (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]. +1. Clone this repository down to your local system, and change into this directory. +2. Run `pulumi whoami -v` to ensure you are logged into a Pulumi backend. (This project assumes you are using the Pulumi Service.) +3. If you wish to change the name or description of the Pulumi project, edit `Pulumi.yaml` and provide the desired values. +4. Run `pulumi stack init` to create a new stack. +5. Review the configuration values listed in the "Configuration Values" section below, and use `pulumi config set` to set any values that are required or where you don't want to accept the default value. +6. Run `pulumi up` and follow the prompts. +7. 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 +## Configuration Values -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. +This Pulumi program uses the following configuration values: -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. +* `aws:region`: This is a _required_ configuration value. Set it to the AWS region where you'd like the infrastructure created. +* `sshKeyPair`: This is a _required_ configuration value. Set it to the name of an AWS key pair present in the region you've specified. +* `networkCidr`: Specify the CIDR block you'd like to use for the AWS VPC in the form of "X.X.X.X/X". The default value is "10.0.0.0/16". This configuration value is optional. +* `subnetMask`: Specify the prefix you'd like to use for the subnets created in the VPC. The default value is 22 (i.e., it will create subnets with enough IP addresses for about 1000 instances). This configuration value is optional. +* `clusterName`: Provide the name of the Kubernetes cluster you plan to provision using `kubeadm`. This is needed to populate tags on various AWS resources in order to support AWS integration with Kubernetes. The default value is "test". This configuration value is optional, but _strongly recommended_. +* `ownerTagValue`: Use this configuration value to populate an "Owner" tag on all provisioned AWS resources. This configuration value is optional, and the default value is "nodody@nowhere.com". +* `teamTagValue`: Use this optional configuration value to populate a "Team" tag on all provisioned AWS resources. The default value is "TeamOfOne". ## License diff --git a/pulumi/aws-k8s-infra/go.mod b/pulumi/aws-k8s-infra/go.mod new file mode 100644 index 0000000..17fd798 --- /dev/null +++ b/pulumi/aws-k8s-infra/go.mod @@ -0,0 +1,69 @@ +module aws-k8s-infra + +go 1.21 + +require ( + github.com/apparentlymart/go-cidr v1.1.0 + github.com/pulumi/pulumi-aws/sdk/v5 v5.42.0 + github.com/pulumi/pulumi/sdk/v3 v3.55.0 +) + +require ( + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect + github.com/acomagu/bufpipe v1.0.3 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/cheggaaa/pb v1.0.29 // indirect + github.com/cloudflare/circl v1.1.0 // indirect + github.com/djherbis/times v1.5.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.0 // indirect + github.com/go-git/go-billy/v5 v5.3.1 // indirect + github.com/go-git/go-git/v5 v5.5.1 // indirect + github.com/gofrs/uuid v4.2.0+incompatible // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.0.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/opentracing/basictracer-go v1.1.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.2.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/term v1.1.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect + github.com/sergi/go-diff v1.2.0 // indirect + github.com/skeema/knownhosts v1.1.0 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/cobra v1.6.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/texttheater/golang-levenshtein v1.0.1 // indirect + github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 // indirect + github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect + github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + go.uber.org/atomic v1.9.0 // indirect + golang.org/x/crypto v0.3.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 // indirect + google.golang.org/grpc v1.51.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/frand v1.4.2 // indirect + sourcegraph.com/sourcegraph/appdash v0.0.0-20211028080628-e2786a622600 // indirect +) diff --git a/pulumi/aws-k8s-infra/index.ts b/pulumi/aws-k8s-infra/index.ts deleted file mode 100644 index 88d58da..0000000 --- a/pulumi/aws-k8s-infra/index.ts +++ /dev/null @@ -1,241 +0,0 @@ -// 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); diff --git a/pulumi/aws-k8s-infra/main.go b/pulumi/aws-k8s-infra/main.go new file mode 100644 index 0000000..fba258c --- /dev/null +++ b/pulumi/aws-k8s-infra/main.go @@ -0,0 +1,408 @@ +package main + +import ( + "fmt" + "log" + "net" + "net/netip" + + "github.com/apparentlymart/go-cidr/cidr" + "github.com/pulumi/pulumi-aws/sdk/v5/go/aws" + "github.com/pulumi/pulumi-aws/sdk/v5/go/aws/ec2" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + // Get some values from the Pulumi stack configuration + keyPair := config.Require(ctx, "sshKeyPair") + bastionAmiType, err := config.Try(ctx, "bastionType") + if err != nil { + bastionAmiType = "t3a.small" + } + vpcNetworkCidr, err := config.Try(ctx, "networkCidr") + if err != nil { + vpcNetworkCidr = "10.0.0.0/16" + } + subnetMask, err := config.TryInt(ctx, "subnetMask") + if err != nil { + subnetMask = 22 + } + clusterName, err := config.Try(ctx, "clusterName") + if err != nil { + clusterName = "test" + } + ownerTagValue, err := config.Try(ctx, "ownerTagValue") + if err != nil { + ownerTagValue = "nobody@nowhere.com" + } + teamTagValue, err := config.Try(ctx, "teamTagValue") + if err != nil { + teamTagValue = "TeamOfOne" + } + + // Parse the supplied VPC CIDR + _, network, err := net.ParseCIDR(vpcNetworkCidr) + if err != nil { + log.Printf("invalid CIDR expression: %s", err) + return err + } + prefix, err := netip.ParsePrefix(vpcNetworkCidr) + if err != nil { + log.Printf("invalid CIDR prefix: %s", err) + return err + } + if prefix.Bits() > subnetMask { + log.Printf("incorrect subnet mask configuration") + return err + } + + // Define some values to be used later + k8sTag := fmt.Sprintf("kubernetes.io/cluster/%s", clusterName) + + // Look up Availability Zone (AZ) information for configured region + desiredAzState := "available" + rawAzInfo, err := aws.GetAvailabilityZones(ctx, &aws.GetAvailabilityZonesArgs{ + State: &desiredAzState, + }) + if err != nil { + log.Printf("error getting AZs: %s", err.Error()) + return err + } + + // Determine how many AZs are present + numOfAZs := len(rawAzInfo.Names) + ctx.Export("numOfAZs", pulumi.Int(numOfAZs)) + + // Build a list of AZ names + azNames := make([]string, numOfAZs) + for idx := 0; idx < numOfAZs; idx++ { + azNames[idx] = rawAzInfo.Names[idx] + } + + // Create new VPC + vpc, err := ec2.NewVpc(ctx, "vpc", &ec2.VpcArgs{ + CidrBlock: pulumi.String(vpcNetworkCidr), + EnableDnsSupport: pulumi.Bool(true), + EnableDnsHostnames: pulumi.Bool(true), + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-vpc", clusterName), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + }, + }) + if err != nil { + log.Printf("error creating VPC: %s", err.Error()) + return err + } + ctx.Export("vpcId", vpc.ID()) + + // Calculate different between VPC CIDR prefix and requested subnet prefix + newBits := subnetMask - prefix.Bits() + + // Create public subnets in the VPC + pubSubnetIds := make([]pulumi.StringInput, numOfAZs) + for idx := 0; idx < numOfAZs; idx++ { + subnetCidr, err := cidr.Subnet(network, newBits, idx) + if err != nil { + log.Printf("error calculating subnet CIDR: %s", err.Error()) + return err + } + subnet, err := ec2.NewSubnet(ctx, fmt.Sprintf("%s-public-%d", clusterName, idx), &ec2.SubnetArgs{ + VpcId: vpc.ID(), + AvailabilityZone: pulumi.String(azNames[idx]), + CidrBlock: pulumi.String(subnetCidr.String()), + MapPublicIpOnLaunch: pulumi.Bool(true), + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-public-%d", clusterName, idx), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + "kubernetes.io/role/elb": pulumi.String("1"), + }, + }) + if err != nil { + log.Printf("error creating public subnet: %s", err.Error()) + return err + } + pubSubnetIds[idx] = subnet.ID() + } + ctx.Export("pubSubnetIds", pulumi.StringArray(pubSubnetIds)) + + // Create an Internet gateway for the public subnets + gw, err := ec2.NewInternetGateway(ctx, "inetgw", &ec2.InternetGatewayArgs{ + VpcId: vpc.ID(), + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-igw", clusterName), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + }, + }) + if err != nil { + log.Printf("error creating Internet Gateway: %s", err.Error()) + } + ctx.Export("gatewayId", gw.ID()) + + // Adopt the default route table in the new VPC + defrt, err := ec2.NewDefaultRouteTable(ctx, "def-rt", &ec2.DefaultRouteTableArgs{ + DefaultRouteTableId: vpc.DefaultRouteTableId, + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-def-rt", clusterName), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + }, + }) + if err != nil { + log.Printf("error adopting default route table: %s", err.Error()) + } + ctx.Export("defaultRoute", defrt.ID()) + + // Create a route for Internet access in the default route table + route, err := ec2.NewRoute(ctx, "inet-route", &ec2.RouteArgs{ + RouteTableId: defrt.ID(), + DestinationCidrBlock: pulumi.String("0.0.0.0/0"), + GatewayId: gw.ID(), + }) + if err != nil { + log.Printf("error creating route: %s", err.Error()) + } + ctx.Export("inetRoute", route.ID()) + + // Create private subnets in the VPC + privSubnetIds := make([]pulumi.StringInput, numOfAZs) + for idx := 0; idx < numOfAZs; idx++ { + subnetCidr, err := cidr.Subnet(network, newBits, idx+numOfAZs) + if err != nil { + log.Printf("error calculating subnet CIDR: %s", err.Error()) + } + subnet, err := ec2.NewSubnet(ctx, fmt.Sprintf("%s-private-%d", clusterName, idx), &ec2.SubnetArgs{ + VpcId: vpc.ID(), + AvailabilityZone: pulumi.String(azNames[idx]), + CidrBlock: pulumi.String(subnetCidr.String()), + MapPublicIpOnLaunch: pulumi.Bool(false), + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-private-%d", clusterName, idx), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + "kubernetes.io/role/internal-elb": pulumi.String("1"), + }, + }) + if err != nil { + log.Printf("error creating private subnet: %s", err.Error()) + } + privSubnetIds[idx] = subnet.ID() + } + ctx.Export("privSubnetIds", pulumi.StringArray(privSubnetIds)) + + // Create/allocate an Elastic IP address for the NAT gateway for private subnets + eip, err := ec2.NewEip(ctx, "natgw-eip", &ec2.EipArgs{ + Vpc: pulumi.Bool(true), + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-natgw-eip", clusterName), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + }, + }) + if err != nil { + log.Printf("error creating EIP: %s", err.Error()) + } + ctx.Export("EIP", eip.AllocationId) + + // Create a NAT gateway for the private subnets + // All private subnets share one NAT Gateway + natgw, err := ec2.NewNatGateway(ctx, "natgw", &ec2.NatGatewayArgs{ + AllocationId: eip.ID(), + SubnetId: pubSubnetIds[0], + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-natgw", clusterName), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + }, + }, pulumi.DependsOn([]pulumi.Resource{eip})) + if err != nil { + log.Printf("error creating NAT gateway: %s", err.Error()) + } + ctx.Export("natGateway", natgw.ID()) + + // Create a new route table for Internet access from private subnets + privrt, err := ec2.NewRouteTable(ctx, "priv-rt", &ec2.RouteTableArgs{ + VpcId: vpc.ID(), + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-priv-rt", clusterName), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + }, + Routes: ec2.RouteTableRouteArray{ + &ec2.RouteTableRouteArgs{ + CidrBlock: pulumi.String("0.0.0.0/0"), + NatGatewayId: natgw.ID(), + }, + }, + }) + if err != nil { + log.Printf("error creating private route table: %s", err.Error()) + } + ctx.Export("privRouteTableId", privrt.ID()) + + // Associate the private subnets with the NAT gateway route table + for idx := 0; idx < numOfAZs; idx++ { + _, err := ec2.NewRouteTableAssociation(ctx, fmt.Sprintf("priv-rta-%d", idx), &ec2.RouteTableAssociationArgs{ + SubnetId: privSubnetIds[idx], + RouteTableId: privrt.ID(), + }) + if err != nil { + log.Printf("error associating private subnet with route table: %s", err.Error()) + } + } + + // Create a security group for traffic to the SSH bastion host + bastionSecGrp, err := ec2.NewSecurityGroup(ctx, "bastion-sg", &ec2.SecurityGroupArgs{ + Name: pulumi.Sprintf("%s-bastion-sg", clusterName), + VpcId: vpc.ID(), + Description: pulumi.String("Allows SSH traffic to bastion hosts"), + Ingress: ec2.SecurityGroupIngressArray{ + ec2.SecurityGroupIngressArgs{ + Protocol: pulumi.String("tcp"), + ToPort: pulumi.Int(22), + FromPort: pulumi.Int(22), + Description: pulumi.String("Allow inbound SSH (TCP 22) from anywhere"), + CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")}, + }, + ec2.SecurityGroupIngressArgs{ + Protocol: pulumi.String("udp"), + ToPort: pulumi.Int(51280), + FromPort: pulumi.Int(51280), + Description: pulumi.String("Allow Wireguard VPN (UDP 51280) from anywhere"), + CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")}, + }, + }, + Egress: ec2.SecurityGroupEgressArray{ + ec2.SecurityGroupEgressArgs{ + Protocol: pulumi.String("-1"), + ToPort: pulumi.Int(0), + FromPort: pulumi.Int(0), + Description: pulumi.String("Allow all outbound traffic"), + CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")}, + }, + }, + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-bastion-sg", clusterName), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + }, + }) + if err != nil { + log.Printf("error creating security group: %s", err.Error()) + } + ctx.Export("bastionSecGrpId", bastionSecGrp.ID()) + + // Create a security group for Kubernetes nodes in this VPC + nodeSecGrp, err := ec2.NewSecurityGroup(ctx, "node-sg", &ec2.SecurityGroupArgs{ + Name: pulumi.Sprintf("%s-node-sg", clusterName), + VpcId: vpc.ID(), + Description: pulumi.String("Allows traffic between and among K8s nodes"), + Ingress: ec2.SecurityGroupIngressArray{ + ec2.SecurityGroupIngressArgs{ + Protocol: pulumi.String("tcp"), + ToPort: pulumi.Int(22), + FromPort: pulumi.Int(22), + Description: pulumi.String("Allow inbound SSH (TCP 22) from bastion hosts"), + SecurityGroups: pulumi.StringArray{bastionSecGrp.ID()}, + }, + ec2.SecurityGroupIngressArgs{ + Protocol: pulumi.String("-1"), + ToPort: pulumi.Int(0), + FromPort: pulumi.Int(0), + Description: pulumi.String("Allow all traffic from this security group"), + Self: pulumi.Bool(true), + }, + }, + Egress: ec2.SecurityGroupEgressArray{ + ec2.SecurityGroupEgressArgs{ + Protocol: pulumi.String("-1"), + ToPort: pulumi.Int(0), + FromPort: pulumi.Int(0), + Description: pulumi.String("Allow all outbound traffic"), + CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")}, + }, + }, + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-node-sg", clusterName), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + }, + }) + if err != nil { + log.Printf("error creating security group: %s", err.Error()) + } + ctx.Export("nodeSecGrpId", nodeSecGrp.ID()) + + // Create a security group for the AWS cloud provider to manage + k8sSecGrp, err := ec2.NewSecurityGroup(ctx, "k8s-sg", &ec2.SecurityGroupArgs{ + Name: pulumi.Sprintf("%s-k8s-sg", clusterName), + VpcId: vpc.ID(), + Description: pulumi.String("Managed by K8s cloud provider"), + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("%s-k8s-sg", clusterName), + k8sTag: pulumi.String("owned"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + }, + }) + if err != nil { + log.Printf("error creating security group: %s", err.Error()) + } + ctx.Export("k8sSecGrpId", k8sSecGrp.ID()) + + // Get AMI ID for bastion host + mostRecent := true + instanceAmi, err := ec2.LookupAmi(ctx, &ec2.LookupAmiArgs{ + Owners: []string{"099720109477"}, + MostRecent: &mostRecent, + Filters: []ec2.GetAmiFilter{ + {Name: "name", Values: []string{"ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server*"}}, + {Name: "root-device-type", Values: []string{"ebs"}}, + {Name: "virtualization-type", Values: []string{"hvm"}}, + {Name: "architecture", Values: []string{"x86_64"}}, + }, + }) + if err != nil { + log.Printf("error looking up AMI: %s", err.Error()) + } + + // Launch an instance to serve as bastion host + bastion, err := ec2.NewInstance(ctx, "bastion", &ec2.InstanceArgs{ + Ami: pulumi.String(instanceAmi.Id), + InstanceType: pulumi.String(bastionAmiType), + AssociatePublicIpAddress: pulumi.Bool(true), + KeyName: pulumi.String(keyPair), + SubnetId: pubSubnetIds[0], + SourceDestCheck: pulumi.Bool(false), + VpcSecurityGroupIds: pulumi.StringArray{bastionSecGrp.ID()}, + Tags: pulumi.StringMap{ + "Name": pulumi.Sprintf("bastion-%s", clusterName), + k8sTag: pulumi.String("shared"), + "Owner": pulumi.String(ownerTagValue), + "Team": pulumi.String(teamTagValue), + }, + }) + if err != nil { + log.Printf("error launching instance: %s", err.Error()) + } + ctx.Export("bastionInstanceId", bastion.ID()) + ctx.Export("bastionPublicIpAddress", bastion.PublicIp) + ctx.Export("bastionPrivateIpAddress", bastion.PrivateIp) + + return nil + }) +}