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 <scott.lowe@scottlowe.org>
This commit is contained in:
Scott S. Lowe 2023-11-12 19:52:18 -07:00 committed by GitHub
parent 987deaf62a
commit 962caf0213
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 509 additions and 267 deletions

6
.gitignore vendored
View file

@ -18,3 +18,9 @@ consul-*.config.json
*.defaults
ssh.cfg
known_hosts
# Ignore Pulumi stack files
Pulumi.*.yaml
# Ignore TypeScript/NodeJS directories
node_modules/

View file

@ -0,0 +1,3 @@
name: aws-k8s-infra
runtime: go
description: A Go Pulumi program to stand up AWS infrastructure for Kubernetes

View file

@ -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

View file

@ -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
)

View file

@ -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<string> = 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);

View file

@ -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
})
}