AB#2436: Initial support for create/terminate AWS NitroTPM instances

* Add .DS_Store to .gitignore

* Add AWS to config / supported instance types

* Move AWS terraform skeleton to cli/internal/terraform

* Move currently unused IAM to hack/terraform/aws

* Print supported AWS instance types when AWS dev flag is set

* Block everything aTLS related (e.g. init, verify) until AWS attestation is available

* Create/Terminate AWS dev cluster when dev flag is set

* Restrict Nitro instances to NitroTPM supported specifically

* Pin zone for subnets

This is not great for HA, but for now we need to avoid the two subnets
ending up in different zones, causing the load balancer to not be able
to connect to the targets.

Should be replaced later with a better implementation that just uses
multiple subnets within the same region dynamically
based on # of nodes or similar.

* Add AWS/GCP to Terraform TestLoader unit test

* Add uid tag and create log group

Co-authored-by: Daniel Weiße <dw@edgeless.systems>
Co-authored-by: Malte Poll <mp@edgeless.systems>
This commit is contained in:
Nils Hanke 2022-10-21 12:24:18 +02:00 committed by GitHub
parent 07f02a442c
commit 04c4cff9f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 940 additions and 314 deletions

3
.gitignore vendored
View File

@ -43,3 +43,6 @@ image/config.mk
.terraform.lock.hcl .terraform.lock.hcl
.terraform.tfstate.lock.info .terraform.tfstate.lock.info
*.tfvars *.tfvars
# macOS
.DS_Store

View File

@ -46,6 +46,17 @@ func NewCreator(out io.Writer) *Creator {
func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, config *config.Config, name, insType string, controlPlaneCount, workerCount int, func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, config *config.Config, name, insType string, controlPlaneCount, workerCount int,
) (clusterid.File, error) { ) (clusterid.File, error) {
switch provider { switch provider {
case cloudprovider.AWS:
// TODO: Remove this once AWS is supported.
if os.Getenv("CONSTELLATION_AWS_DEV") != "1" {
return clusterid.File{}, fmt.Errorf("AWS isn't supported yet")
}
cl, err := c.newTerraformClient(ctx, provider)
if err != nil {
return clusterid.File{}, err
}
defer cl.RemoveInstaller()
return c.createAWS(ctx, cl, config, name, insType, controlPlaneCount, workerCount)
case cloudprovider.GCP: case cloudprovider.GCP:
cl, err := c.newTerraformClient(ctx, provider) cl, err := c.newTerraformClient(ctx, provider)
if err != nil { if err != nil {
@ -76,6 +87,38 @@ func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, c
} }
} }
func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *config.Config,
name, insType string, controlPlaneCount, workerCount int,
) (idFile clusterid.File, retErr error) {
defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl})
vars := &terraform.AWSVariables{
CommonVariables: terraform.CommonVariables{
Name: name,
CountControlPlanes: controlPlaneCount,
CountWorkers: workerCount,
StateDiskSizeGB: config.StateDiskSizeGB,
},
Region: config.Provider.AWS.Region,
Zone: config.Provider.AWS.Zone,
InstanceType: insType,
AMIImageID: config.Provider.AWS.Image,
IAMProfileControlPlane: config.Provider.AWS.IAMProfileControlPlane,
IAMProfileWorkerNodes: config.Provider.AWS.IAMProfileWorkerNodes,
Debug: config.IsDebugCluster(),
}
ip, err := cl.CreateCluster(ctx, name, vars)
if err != nil {
return clusterid.File{}, err
}
return clusterid.File{
CloudProvider: cloudprovider.AWS,
IP: ip,
}, nil
}
func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *config.Config, func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *config.Config,
name, insType string, controlPlaneCount, workerCount int, name, insType string, controlPlaneCount, workerCount int,
) (idFile clusterid.File, retErr error) { ) (idFile clusterid.File, retErr error) {

View File

@ -36,6 +36,10 @@ type Validator struct {
func NewValidator(provider cloudprovider.Provider, config *config.Config) (*Validator, error) { func NewValidator(provider cloudprovider.Provider, config *config.Config) (*Validator, error) {
v := Validator{} v := Validator{}
if provider == cloudprovider.AWS {
// TODO: Implement AWS validator
return nil, errors.New("no validator for AWS available yet")
}
if provider == cloudprovider.Unknown { if provider == cloudprovider.Unknown {
return nil, errors.New("unknown cloud provider") return nil, errors.New("unknown cloud provider")
} }
@ -100,14 +104,14 @@ func (v *Validator) updatePCR(pcrIndex uint32, encoded string) error {
func (v *Validator) setPCRs(config *config.Config) error { func (v *Validator) setPCRs(config *config.Config) error {
switch v.provider { switch v.provider {
case cloudprovider.GCP: case cloudprovider.AWS:
gcpPCRs := config.Provider.GCP.Measurements awsPCRs := config.Provider.AWS.Measurements
enforcedPCRs := config.Provider.GCP.EnforcedMeasurements enforcedPCRs := config.Provider.AWS.EnforcedMeasurements
if err := v.checkPCRs(gcpPCRs, enforcedPCRs); err != nil { if err := v.checkPCRs(awsPCRs, enforcedPCRs); err != nil {
return err return err
} }
v.enforcedPCRs = enforcedPCRs v.enforcedPCRs = enforcedPCRs
v.pcrs = gcpPCRs v.pcrs = awsPCRs
case cloudprovider.Azure: case cloudprovider.Azure:
azurePCRs := config.Provider.Azure.Measurements azurePCRs := config.Provider.Azure.Measurements
enforcedPCRs := config.Provider.Azure.EnforcedMeasurements enforcedPCRs := config.Provider.Azure.EnforcedMeasurements
@ -116,6 +120,14 @@ func (v *Validator) setPCRs(config *config.Config) error {
} }
v.enforcedPCRs = enforcedPCRs v.enforcedPCRs = enforcedPCRs
v.pcrs = azurePCRs v.pcrs = azurePCRs
case cloudprovider.GCP:
gcpPCRs := config.Provider.GCP.Measurements
enforcedPCRs := config.Provider.GCP.EnforcedMeasurements
if err := v.checkPCRs(gcpPCRs, enforcedPCRs); err != nil {
return err
}
v.enforcedPCRs = enforcedPCRs
v.pcrs = gcpPCRs
case cloudprovider.QEMU: case cloudprovider.QEMU:
qemuPCRs := config.Provider.QEMU.Measurements qemuPCRs := config.Provider.QEMU.Measurements
enforcedPCRs := config.Provider.QEMU.EnforcedMeasurements enforcedPCRs := config.Provider.QEMU.EnforcedMeasurements

View File

@ -20,7 +20,7 @@ func NewConfigCmd() *cobra.Command {
cmd.AddCommand(newConfigGenerateCmd()) cmd.AddCommand(newConfigGenerateCmd())
cmd.AddCommand(newConfigFetchMeasurementsCmd()) cmd.AddCommand(newConfigFetchMeasurementsCmd())
cmd.AddCommand(NewConfigInstanceTypesCmd()) cmd.AddCommand(newConfigInstanceTypesCmd())
return cmd return cmd
} }

View File

@ -8,13 +8,14 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/edgelesssys/constellation/v2/internal/config/instancetypes" "github.com/edgelesssys/constellation/v2/internal/config/instancetypes"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func NewConfigInstanceTypesCmd() *cobra.Command { func newConfigInstanceTypesCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "instance-types", Use: "instance-types",
Short: "Print the supported instance types for all cloud providers", Short: "Print the supported instance types for all cloud providers",
@ -26,7 +27,16 @@ func NewConfigInstanceTypesCmd() *cobra.Command {
return cmd return cmd
} }
// TODO: Merge everything back into one function once AWS is supported.
func printSupportedInstanceTypes(cmd *cobra.Command, args []string) { func printSupportedInstanceTypes(cmd *cobra.Command, args []string) {
if os.Getenv("CONSTELLATION_AWS_DEV") == "1" {
printSupportedInstanceTypesWithAWS()
return
}
printSupportedInstanceTypesWithoutAWS()
}
func printSupportedInstanceTypesWithoutAWS() {
fmt.Printf(`Azure Confidential VM instance types: fmt.Printf(`Azure Confidential VM instance types:
%v %v
Azure Trusted Launch instance types: Azure Trusted Launch instance types:
@ -36,6 +46,18 @@ GCP instance types:
`, formatInstanceTypes(instancetypes.AzureCVMInstanceTypes), formatInstanceTypes(instancetypes.AzureTrustedLaunchInstanceTypes), formatInstanceTypes(instancetypes.GCPInstanceTypes)) `, formatInstanceTypes(instancetypes.AzureCVMInstanceTypes), formatInstanceTypes(instancetypes.AzureTrustedLaunchInstanceTypes), formatInstanceTypes(instancetypes.GCPInstanceTypes))
} }
func printSupportedInstanceTypesWithAWS() {
fmt.Printf(`AWS instance families:
%v
Azure Confidential VM instance types:
%v
Azure Trusted Launch instance types:
%v
GCP instance types:
%v
`, formatInstanceTypes(instancetypes.AWSSupportedInstanceFamilies), formatInstanceTypes(instancetypes.AzureCVMInstanceTypes), formatInstanceTypes(instancetypes.AzureTrustedLaunchInstanceTypes), formatInstanceTypes(instancetypes.GCPInstanceTypes))
}
func formatInstanceTypes(types []string) string { func formatInstanceTypes(types []string) string {
return "\t" + strings.Join(types, "\n\t") return "\t" + strings.Join(types, "\n\t")
} }

View File

@ -92,6 +92,8 @@ func create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler,
provider := config.GetProvider() provider := config.GetProvider()
var instanceType string var instanceType string
switch provider { switch provider {
case cloudprovider.AWS:
instanceType = config.Provider.AWS.InstanceType
case cloudprovider.Azure: case cloudprovider.Azure:
instanceType = config.Provider.Azure.InstanceType instanceType = config.Provider.Azure.InstanceType
case cloudprovider.GCP: case cloudprovider.GCP:

View File

@ -9,6 +9,7 @@ package cmd
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -17,8 +18,8 @@ import (
// warnAWS warns that AWS isn't supported. // warnAWS warns that AWS isn't supported.
func warnAWS(providerPos int) cobra.PositionalArgs { func warnAWS(providerPos int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error {
if cloudprovider.FromString(args[providerPos]) == cloudprovider.AWS { if cloudprovider.FromString(args[providerPos]) == cloudprovider.AWS && os.Getenv("CONSTELLATION_AWS_DEV") != "1" {
return errors.New("AWS isn't supported by this version of Constellation") return errors.New("AWS isn't supported yet")
} }
return nil return nil
} }

View File

@ -22,6 +22,24 @@ func TestLoader(t *testing.T) {
provider cloudprovider.Provider provider cloudprovider.Provider
fileList []string fileList []string
}{ }{
"aws": {
provider: cloudprovider.AWS,
fileList: []string{
"main.tf",
"variables.tf",
"outputs.tf",
"modules",
},
},
"gcp": {
provider: cloudprovider.GCP,
fileList: []string{
"main.tf",
"variables.tf",
"outputs.tf",
"modules",
},
},
"qemu": { "qemu": {
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
fileList: []string{ fileList: []string{

View File

@ -0,0 +1,240 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "3.4.3"
}
}
}
# Configure the AWS Provider
provider "aws" {
region = var.region
}
locals {
uid = random_id.uid.hex
name = "${var.name}-${local.uid}"
ports_node_range = "30000-32767"
ports_ssh = "22"
ports_kubernetes = "6443"
ports_bootstrapper = "9000"
ports_konnectivity = "8132"
ports_verify = "30081"
ports_debugd = "4000"
tags = { constellation-uid = local.uid }
}
resource "random_id" "uid" {
byte_length = 4
}
resource "aws_vpc" "vpc" {
cidr_block = "192.168.0.0/16"
tags = merge(local.tags, { Name = "${local.name}-vpc" })
}
module "public_private_subnet" {
source = "./modules/public_private_subnet"
name = local.name
vpc_id = aws_vpc.vpc.id
cidr_vpc_subnet_nodes = "192.168.178.0/24"
cidr_vpc_subnet_internet = "192.168.0.0/24"
zone = var.zone
tags = local.tags
}
resource "aws_eip" "lb" {
vpc = true
tags = local.tags
}
resource "aws_lb" "front_end" {
name = "${local.name}-loadbalancer"
internal = false
load_balancer_type = "network"
tags = local.tags
subnet_mapping {
subnet_id = module.public_private_subnet.public_subnet_id
allocation_id = aws_eip.lb.id
}
enable_cross_zone_load_balancing = true
}
resource "aws_security_group" "security_group" {
name = local.name
vpc_id = aws_vpc.vpc.id
description = "Security group for ${local.name}"
tags = local.tags
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
ingress {
from_port = split("-", local.ports_node_range)[0]
to_port = split("-", local.ports_node_range)[1]
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "K8s node ports"
}
# TODO: Remove when development is more advanced
dynamic "ingress" {
for_each = var.debug ? [1] : []
content {
from_port = local.ports_ssh
to_port = local.ports_ssh
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "SSH"
}
}
ingress {
from_port = local.ports_bootstrapper
to_port = local.ports_bootstrapper
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "bootstrapper"
}
ingress {
from_port = local.ports_kubernetes
to_port = local.ports_kubernetes
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "kubernetes"
}
ingress {
from_port = local.ports_konnectivity
to_port = local.ports_konnectivity
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "konnectivity"
}
dynamic "ingress" {
for_each = var.debug ? [1] : []
content {
from_port = local.ports_debugd
to_port = local.ports_debugd
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "debugd"
}
}
}
resource "aws_cloudwatch_log_group" "log_group" {
name = local.name
retention_in_days = 30
tags = local.tags
}
module "load_balancer_target_bootstrapper" {
source = "./modules/load_balancer_target"
name = "${local.name}-bootstrapper"
vpc_id = aws_vpc.vpc.id
lb_arn = aws_lb.front_end.arn
port = local.ports_bootstrapper
tags = local.tags
}
module "load_balancer_target_kubernetes" {
source = "./modules/load_balancer_target"
name = "${local.name}-kubernetes"
vpc_id = aws_vpc.vpc.id
lb_arn = aws_lb.front_end.arn
port = local.ports_kubernetes
tags = local.tags
}
module "load_balancer_target_verify" {
source = "./modules/load_balancer_target"
name = "${local.name}-verify"
vpc_id = aws_vpc.vpc.id
lb_arn = aws_lb.front_end.arn
port = local.ports_verify
tags = local.tags
}
module "load_balancer_target_debugd" {
count = var.debug ? 1 : 0 // only deploy debugd in debug mode
source = "./modules/load_balancer_target"
name = "${local.name}-debugd"
vpc_id = aws_vpc.vpc.id
lb_arn = aws_lb.front_end.arn
port = local.ports_debugd
tags = local.tags
}
module "load_balancer_target_konnectivity" {
source = "./modules/load_balancer_target"
name = "${local.name}-konnectivity"
vpc_id = aws_vpc.vpc.id
lb_arn = aws_lb.front_end.arn
port = local.ports_konnectivity
tags = local.tags
}
# TODO: Remove when development is more advanced
module "load_balancer_target_ssh" {
count = var.debug ? 1 : 0 // only deploy SSH in debug mode
source = "./modules/load_balancer_target"
name = "${local.name}-ssh"
vpc_id = aws_vpc.vpc.id
lb_arn = aws_lb.front_end.arn
port = local.ports_ssh
tags = local.tags
}
module "instance_group_control_plane" {
source = "./modules/instance_group"
name = local.name
role = "control-plane"
uid = local.uid
instance_type = var.instance_type
instance_count = var.control_plane_count
image_id = var.ami
state_disk_type = var.state_disk_type
state_disk_size = var.state_disk_size
target_group_arns = flatten([
module.load_balancer_target_bootstrapper.target_group_arn,
module.load_balancer_target_kubernetes.target_group_arn,
module.load_balancer_target_verify.target_group_arn,
module.load_balancer_target_konnectivity.target_group_arn,
var.debug ? [module.load_balancer_target_debugd[0].target_group_arn,
module.load_balancer_target_ssh[0].target_group_arn] : [],
])
security_groups = [aws_security_group.security_group.id]
subnetwork = module.public_private_subnet.private_subnet_id
iam_instance_profile = var.iam_instance_profile_control_plane
}
module "instance_group_worker_nodes" {
source = "./modules/instance_group"
name = local.name
role = "worker"
uid = local.uid
instance_type = var.instance_type
instance_count = var.worker_count
image_id = var.ami
state_disk_type = var.state_disk_type
state_disk_size = var.state_disk_size
subnetwork = module.public_private_subnet.private_subnet_id
target_group_arns = []
security_groups = []
iam_instance_profile = var.iam_instance_profile_worker_nodes
}

View File

@ -17,10 +17,22 @@ resource "aws_launch_configuration" "control_plane_launch_config" {
image_id = var.image_id image_id = var.image_id
instance_type = var.instance_type instance_type = var.instance_type
iam_instance_profile = var.iam_instance_profile iam_instance_profile = var.iam_instance_profile
security_groups = var.security_groups
metadata_options { metadata_options {
http_tokens = "required" http_tokens = "required"
} }
root_block_device {
encrypted = true
}
ebs_block_device {
device_name = "/dev/sdb" # Note: AWS may adjust this to /dev/xvdb, /dev/hdb or /dev/nvme1n1 depending on the disk type. See: https://docs.aws.amazon.com/en_us/AWSEC2/latest/UserGuide/device_naming.html
volume_size = var.state_disk_size
volume_type = var.state_disk_type
encrypted = true
delete_on_termination = true
}
lifecycle { lifecycle {
create_before_destroy = true create_before_destroy = true

View File

@ -28,9 +28,14 @@ variable "image_id" {
description = "Image ID for the nodes." description = "Image ID for the nodes."
} }
variable "disk_size" { variable "state_disk_type" {
type = string
description = "EBS disk type for the state disk of the nodes."
}
variable "state_disk_size" {
type = number type = number
description = "Disk size for the nodes, in GB." description = "Disk size for the state disk of the nodes [GB]."
} }
variable "target_group_arns" { variable "target_group_arns" {
@ -47,3 +52,8 @@ variable "iam_instance_profile" {
type = string type = string
description = "IAM instance profile for the nodes." description = "IAM instance profile for the nodes."
} }
variable "security_groups" {
type = list(string)
description = "List of IDs of the security groups for an instance."
}

View File

@ -7,24 +7,12 @@ terraform {
} }
} }
resource "aws_lb" "front_end" {
name = var.name
internal = false
load_balancer_type = "network"
subnets = [var.subnet]
tags = {
Name = "loadbalancer"
}
enable_cross_zone_load_balancing = true
}
resource "aws_lb_target_group" "front_end" { resource "aws_lb_target_group" "front_end" {
name = var.name name = var.name
port = var.port port = var.port
protocol = "TCP" protocol = "TCP"
vpc_id = var.vpc vpc_id = var.vpc_id
tags = var.tags
health_check { health_check {
port = var.port port = var.port
@ -37,9 +25,10 @@ resource "aws_lb_target_group" "front_end" {
} }
resource "aws_lb_listener" "front_end" { resource "aws_lb_listener" "front_end" {
load_balancer_arn = aws_lb.front_end.arn load_balancer_arn = var.lb_arn
port = var.port port = var.port
protocol = "TCP" protocol = "TCP"
tags = var.tags
default_action { default_action {
type = "forward" type = "forward"

View File

@ -0,0 +1,24 @@
variable "name" {
type = string
description = "Name of the load balancer target."
}
variable "port" {
type = string
description = "Port of the load balancer target."
}
variable "vpc_id" {
type = string
description = "ID of the VPC."
}
variable "lb_arn" {
type = string
description = "ARN of the load balancer."
}
variable "tags" {
type = map(string)
description = "The tags to add to the loadbalancer."
}

View File

@ -0,0 +1,68 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
resource "aws_eip" "nat" {
vpc = true
tags = var.tags
}
resource "aws_subnet" "private" {
vpc_id = var.vpc_id
cidr_block = var.cidr_vpc_subnet_nodes
availability_zone = var.zone
tags = merge(var.tags, { Name = "${var.name}-subnet-nodes" })
}
resource "aws_subnet" "public" {
vpc_id = var.vpc_id
cidr_block = var.cidr_vpc_subnet_internet
availability_zone = var.zone
tags = merge(var.tags, { Name = "${var.name}-subnet-internet" })
}
resource "aws_internet_gateway" "gw" {
vpc_id = var.vpc_id
tags = merge(var.tags, { Name = "${var.name}-internet-gateway" })
}
resource "aws_nat_gateway" "gw" {
subnet_id = aws_subnet.public.id
allocation_id = aws_eip.nat.id
tags = merge(var.tags, { Name = "${var.name}-nat-gateway" })
}
resource "aws_route_table" "private_nat" {
vpc_id = var.vpc_id
tags = merge(var.tags, { Name = "${var.name}-private-nat" })
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.gw.id
}
}
resource "aws_route_table" "public_igw" {
vpc_id = var.vpc_id
tags = merge(var.tags, { Name = "${var.name}-public-igw" })
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
}
resource "aws_route_table_association" "private-nat" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private_nat.id
}
resource "aws_route_table_association" "route_to_internet" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public_igw.id
}

View File

@ -0,0 +1,7 @@
output "private_subnet_id" {
value = aws_subnet.private.id
}
output "public_subnet_id" {
value = aws_subnet.public.id
}

View File

@ -0,0 +1,29 @@
variable "name" {
type = string
description = "Name of your Constellation, which is used as a prefix for tags."
}
variable "vpc_id" {
type = string
description = "ID of the VPC."
}
variable "zone" {
type = string
description = "Availability zone."
}
variable "cidr_vpc_subnet_nodes" {
type = string
description = "CIDR block for the subnet that will contain the nodes."
}
variable "cidr_vpc_subnet_internet" {
type = string
description = "CIDR block for the subnet that contains resources reachable from the Internet."
}
variable "tags" {
type = map(string)
description = "The tags to add to the resource."
}

View File

@ -0,0 +1,3 @@
output "ip" {
value = aws_eip.lb.public_ip
}

View File

@ -0,0 +1,59 @@
variable "name" {
type = string
description = "Name of your Constellation"
}
variable "iam_instance_profile_worker_nodes" {
type = string
description = "Name of the IAM instance profile for worker nodes"
}
variable "iam_instance_profile_control_plane" {
type = string
description = "Name of the IAM instance profile for control plane nodes"
}
variable "instance_type" {
type = string
description = "Instance type for worker nodes"
}
variable "state_disk_type" {
type = string
description = "EBS disk type for the state disk of the nodes"
}
variable "state_disk_size" {
type = number
description = "Disk size for the state disk of the nodes [GB]"
}
variable "control_plane_count" {
type = number
description = "Number of control plane nodes"
}
variable "worker_count" {
type = number
description = "Number of worker nodes"
}
variable "ami" {
type = string
description = "AMI ID"
}
variable "region" {
type = string
description = "The AWS region to create the cluster in"
}
variable "zone" {
type = string
description = "The AWS availability zone name to create the cluster in"
}
variable "debug" {
type = bool
description = "Enable debug mode. This opens up a debugd port that can be used to deploy a custom bootstrapper."
}

View File

@ -39,6 +39,28 @@ func (v *CommonVariables) String() string {
return b.String() return b.String()
} }
// GCPVariables is user configuration for creating a cluster with Terraform on GCP.
type AWSVariables struct {
// CommonVariables contains common variables.
CommonVariables
// Region is the AWS region to use.
Region string
// Zone is the AWS zone to use in the given region.
Zone string
// AMIImageID is the ID of the AMI image to use.
AMIImageID string
// InstanceType is the type of the EC2 instance to use.
InstanceType string
// StateDiskType is the EBS disk type to use for the state disk.
StateDiskType string
// IAMGroupControlPlane is the IAM group to use for the control-plane nodes.
IAMProfileControlPlane string
// IAMGroupWorkerNodes is the IAM group to use for the worker nodes.
IAMProfileWorkerNodes string
// Debug is true if debug mode is enabled.
Debug bool
}
// GCPVariables is user configuration for creating a cluster with Terraform on GCP. // GCPVariables is user configuration for creating a cluster with Terraform on GCP.
type GCPVariables struct { type GCPVariables struct {
// CommonVariables contains common variables. // CommonVariables contains common variables.
@ -62,6 +84,21 @@ type GCPVariables struct {
Debug bool Debug bool
} }
func (v *AWSVariables) String() string {
b := &strings.Builder{}
b.WriteString(v.CommonVariables.String())
writeLinef(b, "region = %q", v.Region)
writeLinef(b, "zone = %q", v.Zone)
writeLinef(b, "ami = %q", v.AMIImageID)
writeLinef(b, "instance_type = %q", v.InstanceType)
writeLinef(b, "state_disk_type = %q", v.StateDiskType)
writeLinef(b, "iam_instance_profile_control_plane = %q", v.IAMProfileControlPlane)
writeLinef(b, "iam_instance_profile_worker_nodes = %q", v.IAMProfileWorkerNodes)
writeLinef(b, "debug = %t", v.Debug)
return b.String()
}
// String returns a string representation of the variables, formatted as Terraform variables. // String returns a string representation of the variables, formatted as Terraform variables.
func (v *GCPVariables) String() string { func (v *GCPVariables) String() string {
b := &strings.Builder{} b := &strings.Builder{}

View File

@ -31,6 +31,11 @@ const (
Version1 = "v1" Version1 = "v1"
) )
var (
azureReleaseImageRegex = regexp.MustCompile(`^\/CommunityGalleries\/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df\/Images\/constellation\/Versions\/[\d]+.[\d]+.[\d]+$`)
gcpReleaseImageRegex = regexp.MustCompile(`^projects\/constellation-images\/global\/images\/constellation-v[\d]+-[\d]+-[\d]+$`)
)
// Config defines configuration used by CLI. // Config defines configuration used by CLI.
type Config struct { type Config struct {
// description: | // description: |
@ -84,6 +89,9 @@ type UserKey struct {
// Fields should remain pointer-types so custom specific configs can nil them // Fields should remain pointer-types so custom specific configs can nil them
// if not required. // if not required.
type ProviderConfig struct { type ProviderConfig struct {
// description: |
// Configuration for AWS as provider.
AWS *AWSConfig `yaml:"aws,omitempty" validate:"omitempty,dive"`
// description: | // description: |
// Configuration for Azure as provider. // Configuration for Azure as provider.
Azure *AzureConfig `yaml:"azure,omitempty" validate:"omitempty,dive"` Azure *AzureConfig `yaml:"azure,omitempty" validate:"omitempty,dive"`
@ -95,6 +103,37 @@ type ProviderConfig struct {
QEMU *QEMUConfig `yaml:"qemu,omitempty" validate:"omitempty,dive"` QEMU *QEMUConfig `yaml:"qemu,omitempty" validate:"omitempty,dive"`
} }
// AWSConfig are AWS specific configuration values used by the CLI.
type AWSConfig struct {
// description: |
// AWS data center region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions
Region string `yaml:"region" validate:"required"`
// description: |
// AWS data center zone name in defined region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones
Zone string `yaml:"zone" validate:"required"`
// description: |
// AMI ID of the machine image used to create Constellation nodes.
Image string `yaml:"image" validate:"required"`
// description: |
// VM instance type to use for Constellation nodes. Needs to support NitroTPM. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html
InstanceType string `yaml:"instanceType" validate:"lowercase,aws_instance_type"`
// description: |
// Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html
StateDiskType string `yaml:"stateDiskType" validate:"oneof=standard gp2 gp3 st1 sc1 io1"`
// description: |
// Name of the IAM profile to use for the control plane nodes.
IAMProfileControlPlane string `yaml:"iamProfileControlPlane" validate:"required"`
// description: |
// Name of the IAM profile to use for the worker nodes.
IAMProfileWorkerNodes string `yaml:"iamProfileWorkerNodes" validate:"required"`
// description: |
// Expected VM measurements.
Measurements Measurements `yaml:"measurements"`
// description: |
// List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning.
EnforcedMeasurements []uint32 `yaml:"enforcedMeasurements"`
}
// AzureConfig are Azure specific configuration values used by the CLI. // AzureConfig are Azure specific configuration values used by the CLI.
type AzureConfig struct { type AzureConfig struct {
// description: | // description: |
@ -221,6 +260,16 @@ func Default() *Config {
StateDiskSizeGB: 30, StateDiskSizeGB: 30,
DebugCluster: func() *bool { b := false; return &b }(), DebugCluster: func() *bool { b := false; return &b }(),
Provider: ProviderConfig{ Provider: ProviderConfig{
AWS: &AWSConfig{
Region: "",
Image: "",
InstanceType: "m6a.xlarge",
StateDiskType: "gp3",
IAMProfileControlPlane: "",
IAMProfileWorkerNodes: "",
Measurements: copyPCRMap(awsPCRs),
EnforcedMeasurements: []uint32{}, // TODO: add default values
},
Azure: &AzureConfig{ Azure: &AzureConfig{
SubscriptionID: "", SubscriptionID: "",
TenantID: "", TenantID: "",
@ -268,6 +317,10 @@ func validateK8sVersion(fl validator.FieldLevel) bool {
return versions.IsSupportedK8sVersion(fl.Field().String()) return versions.IsSupportedK8sVersion(fl.Field().String())
} }
func validateAWSInstanceType(fl validator.FieldLevel) bool {
return validInstanceTypeForProvider(fl.Field().String(), false, cloudprovider.AWS)
}
func validateAzureInstanceType(fl validator.FieldLevel) bool { func validateAzureInstanceType(fl validator.FieldLevel) bool {
azureConfig := fl.Parent().Interface().(AzureConfig) azureConfig := fl.Parent().Interface().(AzureConfig)
var acceptNonCVM bool var acceptNonCVM bool
@ -288,6 +341,9 @@ func validateProvider(sl validator.StructLevel) {
provider := sl.Current().Interface().(ProviderConfig) provider := sl.Current().Interface().(ProviderConfig)
providerCount := 0 providerCount := 0
if provider.AWS != nil {
providerCount++
}
if provider.Azure != nil { if provider.Azure != nil {
providerCount++ providerCount++
} }
@ -314,7 +370,11 @@ func (c *Config) Validate() ([]string, error) {
return nil, err return nil, err
} }
// Register Azure & GCP InstanceType validation error types // Register AWS, Azure & GCP InstanceType validation error types
if err := validate.RegisterTranslation("aws_instance_type", trans, registerTranslateAWSInstanceTypeError, translateAWSInstanceTypeError); err != nil {
return nil, err
}
if err := validate.RegisterTranslation("azure_instance_type", trans, registerTranslateAzureInstanceTypeError, c.translateAzureInstanceTypeError); err != nil { if err := validate.RegisterTranslation("azure_instance_type", trans, registerTranslateAzureInstanceTypeError, c.translateAzureInstanceTypeError); err != nil {
return nil, err return nil, err
} }
@ -337,12 +397,17 @@ func (c *Config) Validate() ([]string, error) {
return nil, err return nil, err
} }
// register custom validator with label azure_instance_type to validate version based on available versionConfigs. // register custom validator with label aws_instance_type to validate the AWS instance type from config input.
if err := validate.RegisterValidation("aws_instance_type", validateAWSInstanceType); err != nil {
return nil, err
}
// register custom validator with label azure_instance_type to validate the Azure instance type from config input.
if err := validate.RegisterValidation("azure_instance_type", validateAzureInstanceType); err != nil { if err := validate.RegisterValidation("azure_instance_type", validateAzureInstanceType); err != nil {
return nil, err return nil, err
} }
// register custom validator with label gcp_instance_type to validate version based on available versionConfigs. // register custom validator with label gcp_instance_type to validate the GCP instance type from config input.
if err := validate.RegisterValidation("gcp_instance_type", validateGCPInstanceType); err != nil { if err := validate.RegisterValidation("gcp_instance_type", validateGCPInstanceType); err != nil {
return nil, err return nil, err
} }
@ -384,6 +449,16 @@ func (c *Config) translateAzureInstanceTypeError(ut ut.Translator, fe validator.
return t return t
} }
func registerTranslateAWSInstanceTypeError(ut ut.Translator) error {
return ut.Add("aws_instance_type", fmt.Sprintf("{0} must be an instance from one of the following families types with size xlarge or higher: %v", instancetypes.AWSSupportedInstanceFamilies), true)
}
func translateAWSInstanceTypeError(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("aws_instance_type", fe.Field())
return t
}
func registerTranslateGCPInstanceTypeError(ut ut.Translator) error { func registerTranslateGCPInstanceTypeError(ut ut.Translator) error {
return ut.Add("gcp_instance_type", fmt.Sprintf("{0} must be one of %v", instancetypes.GCPInstanceTypes), true) return ut.Add("gcp_instance_type", fmt.Sprintf("{0} must be one of %v", instancetypes.GCPInstanceTypes), true)
} }
@ -413,6 +488,9 @@ func (c *Config) translateMoreThanOneProviderError(ut ut.Translator, fe validato
definedProviders := make([]string, 0) definedProviders := make([]string, 0)
// c.Provider should not be nil as Provider would need to be defined for the validation to fail in this place. // c.Provider should not be nil as Provider would need to be defined for the validation to fail in this place.
if c.Provider.AWS != nil {
definedProviders = append(definedProviders, "AWS")
}
if c.Provider.Azure != nil { if c.Provider.Azure != nil {
definedProviders = append(definedProviders, "Azure") definedProviders = append(definedProviders, "Azure")
} }
@ -432,6 +510,8 @@ func (c *Config) translateMoreThanOneProviderError(ut ut.Translator, fe validato
// HasProvider checks whether the config contains the provider. // HasProvider checks whether the config contains the provider.
func (c *Config) HasProvider(provider cloudprovider.Provider) bool { func (c *Config) HasProvider(provider cloudprovider.Provider) bool {
switch provider { switch provider {
case cloudprovider.AWS:
return c.Provider.AWS != nil
case cloudprovider.Azure: case cloudprovider.Azure:
return c.Provider.Azure != nil return c.Provider.Azure != nil
case cloudprovider.GCP: case cloudprovider.GCP:
@ -446,6 +526,9 @@ func (c *Config) HasProvider(provider cloudprovider.Provider) bool {
// If multiple cloud providers are configured (which is not supported) // If multiple cloud providers are configured (which is not supported)
// only a single image is returned. // only a single image is returned.
func (c *Config) Image() string { func (c *Config) Image() string {
if c.HasProvider(cloudprovider.AWS) {
return c.Provider.AWS.Image
}
if c.HasProvider(cloudprovider.Azure) { if c.HasProvider(cloudprovider.Azure) {
return c.Provider.Azure.Image return c.Provider.Azure.Image
} }
@ -456,6 +539,9 @@ func (c *Config) Image() string {
} }
func (c *Config) UpdateMeasurements(newMeasurements Measurements) { func (c *Config) UpdateMeasurements(newMeasurements Measurements) {
if c.Provider.AWS != nil {
c.Provider.AWS.Measurements.CopyFrom(newMeasurements)
}
if c.Provider.Azure != nil { if c.Provider.Azure != nil {
c.Provider.Azure.Measurements.CopyFrom(newMeasurements) c.Provider.Azure.Measurements.CopyFrom(newMeasurements)
} }
@ -474,6 +560,8 @@ func (c *Config) RemoveProviderExcept(provider cloudprovider.Provider) {
currentProviderConfigs := c.Provider currentProviderConfigs := c.Provider
c.Provider = ProviderConfig{} c.Provider = ProviderConfig{}
switch provider { switch provider {
case cloudprovider.AWS:
c.Provider.AWS = currentProviderConfigs.AWS
case cloudprovider.Azure: case cloudprovider.Azure:
c.Provider.Azure = currentProviderConfigs.Azure c.Provider.Azure = currentProviderConfigs.Azure
case cloudprovider.GCP: case cloudprovider.GCP:
@ -490,12 +578,13 @@ func (c *Config) RemoveProviderExcept(provider cloudprovider.Provider) {
// was put inside an image just by looking at its name. // was put inside an image just by looking at its name.
func (c *Config) IsDebugImage() bool { func (c *Config) IsDebugImage() bool {
switch { switch {
case c.Provider.GCP != nil: case c.Provider.AWS != nil:
gcpRegex := regexp.MustCompile(`^projects\/constellation-images\/global\/images\/constellation-v[\d]+-[\d]+-[\d]+$`) // TODO: Add proper image name validation for AWS when we are closer to release.
return !gcpRegex.MatchString(c.Provider.GCP.Image) return true
case c.Provider.Azure != nil: case c.Provider.Azure != nil:
azureRegex := regexp.MustCompile(`^\/CommunityGalleries\/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df\/Images\/constellation\/Versions\/[\d]+.[\d]+.[\d]+$`) return !azureReleaseImageRegex.MatchString(c.Provider.Azure.Image)
return !azureRegex.MatchString(c.Provider.Azure.Image) case c.Provider.GCP != nil:
return !gcpReleaseImageRegex.MatchString(c.Provider.GCP.Image)
default: default:
return false return false
} }
@ -503,6 +592,9 @@ func (c *Config) IsDebugImage() bool {
// GetProvider returns the configured cloud provider. // GetProvider returns the configured cloud provider.
func (c *Config) GetProvider() cloudprovider.Provider { func (c *Config) GetProvider() cloudprovider.Provider {
if c.Provider.AWS != nil {
return cloudprovider.AWS
}
if c.Provider.Azure != nil { if c.Provider.Azure != nil {
return cloudprovider.Azure return cloudprovider.Azure
} }
@ -545,13 +637,8 @@ func copyPCRMap(m map[uint32][]byte) map[uint32][]byte {
func validInstanceTypeForProvider(insType string, acceptNonCVM bool, provider cloudprovider.Provider) bool { func validInstanceTypeForProvider(insType string, acceptNonCVM bool, provider cloudprovider.Provider) bool {
switch provider { switch provider {
case cloudprovider.GCP: case cloudprovider.AWS:
for _, instanceType := range instancetypes.GCPInstanceTypes { return checkIfAWSInstanceTypeIsValid(insType)
if insType == instanceType {
return true
}
}
return false
case cloudprovider.Azure: case cloudprovider.Azure:
if acceptNonCVM { if acceptNonCVM {
for _, instanceType := range instancetypes.AzureTrustedLaunchInstanceTypes { for _, instanceType := range instancetypes.AzureTrustedLaunchInstanceTypes {
@ -567,11 +654,58 @@ func validInstanceTypeForProvider(insType string, acceptNonCVM bool, provider cl
} }
} }
return false return false
case cloudprovider.GCP:
for _, instanceType := range instancetypes.GCPInstanceTypes {
if insType == instanceType {
return true
}
}
return false
default: default:
return false return false
} }
} }
// checkIfAWSInstanceTypeIsValid checks if an AWS instance type passed as user input is in one of the instance families supporting NitroTPM.
func checkIfAWSInstanceTypeIsValid(userInput string) bool {
// Check if user or code does anything weird and tries to pass multiple strings as one
if strings.Contains(userInput, " ") {
return false
}
if strings.Contains(userInput, ",") {
return false
}
if strings.Contains(userInput, ";") {
return false
}
splitInstanceType := strings.Split(userInput, ".")
if len(splitInstanceType) != 2 {
return false
}
userDefinedFamily := splitInstanceType[0]
userDefinedSize := splitInstanceType[1]
// Check if instace type has at least 4 vCPUs (= contains "xlarge" in its name)
hasEnoughVCPUs := strings.Contains(userDefinedSize, "xlarge")
if !hasEnoughVCPUs {
return false
}
// Now check if the user input is a supported family
// Note that we cannot directly use the family split from the Graviton check above, as some instances are directly specified by their full name and not just the family in general
for _, supportedFamily := range instancetypes.AWSSupportedInstanceFamilies {
supportedFamilyLowercase := strings.ToLower(supportedFamily)
if userDefinedFamily == supportedFamilyLowercase {
return true
}
}
return false
}
// IsDebugCluster checks whether the cluster is configured as a debug cluster. // IsDebugCluster checks whether the cluster is configured as a debug cluster.
func (c *Config) IsDebugCluster() bool { func (c *Config) IsDebugCluster() bool {
if c.DebugCluster != nil && *c.DebugCluster { if c.DebugCluster != nil && *c.DebugCluster {

View File

@ -15,6 +15,7 @@ var (
UpgradeConfigDoc encoder.Doc UpgradeConfigDoc encoder.Doc
UserKeyDoc encoder.Doc UserKeyDoc encoder.Doc
ProviderConfigDoc encoder.Doc ProviderConfigDoc encoder.Doc
AWSConfigDoc encoder.Doc
AzureConfigDoc encoder.Doc AzureConfigDoc encoder.Doc
GCPConfigDoc encoder.Doc GCPConfigDoc encoder.Doc
QEMUConfigDoc encoder.Doc QEMUConfigDoc encoder.Doc
@ -120,22 +121,83 @@ func init() {
FieldName: "provider", FieldName: "provider",
}, },
} }
ProviderConfigDoc.Fields = make([]encoder.Doc, 3) ProviderConfigDoc.Fields = make([]encoder.Doc, 4)
ProviderConfigDoc.Fields[0].Name = "azure" ProviderConfigDoc.Fields[0].Name = "aws"
ProviderConfigDoc.Fields[0].Type = "AzureConfig" ProviderConfigDoc.Fields[0].Type = "AWSConfig"
ProviderConfigDoc.Fields[0].Note = "" ProviderConfigDoc.Fields[0].Note = ""
ProviderConfigDoc.Fields[0].Description = "Configuration for Azure as provider." ProviderConfigDoc.Fields[0].Description = "Configuration for AWS as provider."
ProviderConfigDoc.Fields[0].Comments[encoder.LineComment] = "Configuration for Azure as provider." ProviderConfigDoc.Fields[0].Comments[encoder.LineComment] = "Configuration for AWS as provider."
ProviderConfigDoc.Fields[1].Name = "gcp" ProviderConfigDoc.Fields[1].Name = "azure"
ProviderConfigDoc.Fields[1].Type = "GCPConfig" ProviderConfigDoc.Fields[1].Type = "AzureConfig"
ProviderConfigDoc.Fields[1].Note = "" ProviderConfigDoc.Fields[1].Note = ""
ProviderConfigDoc.Fields[1].Description = "Configuration for Google Cloud as provider." ProviderConfigDoc.Fields[1].Description = "Configuration for Azure as provider."
ProviderConfigDoc.Fields[1].Comments[encoder.LineComment] = "Configuration for Google Cloud as provider." ProviderConfigDoc.Fields[1].Comments[encoder.LineComment] = "Configuration for Azure as provider."
ProviderConfigDoc.Fields[2].Name = "qemu" ProviderConfigDoc.Fields[2].Name = "gcp"
ProviderConfigDoc.Fields[2].Type = "QEMUConfig" ProviderConfigDoc.Fields[2].Type = "GCPConfig"
ProviderConfigDoc.Fields[2].Note = "" ProviderConfigDoc.Fields[2].Note = ""
ProviderConfigDoc.Fields[2].Description = "Configuration for QEMU as provider." ProviderConfigDoc.Fields[2].Description = "Configuration for Google Cloud as provider."
ProviderConfigDoc.Fields[2].Comments[encoder.LineComment] = "Configuration for QEMU as provider." ProviderConfigDoc.Fields[2].Comments[encoder.LineComment] = "Configuration for Google Cloud as provider."
ProviderConfigDoc.Fields[3].Name = "qemu"
ProviderConfigDoc.Fields[3].Type = "QEMUConfig"
ProviderConfigDoc.Fields[3].Note = ""
ProviderConfigDoc.Fields[3].Description = "Configuration for QEMU as provider."
ProviderConfigDoc.Fields[3].Comments[encoder.LineComment] = "Configuration for QEMU as provider."
AWSConfigDoc.Type = "AWSConfig"
AWSConfigDoc.Comments[encoder.LineComment] = "AWSConfig are AWS specific configuration values used by the CLI."
AWSConfigDoc.Description = "AWSConfig are AWS specific configuration values used by the CLI."
AWSConfigDoc.AppearsIn = []encoder.Appearance{
{
TypeName: "ProviderConfig",
FieldName: "aws",
},
}
AWSConfigDoc.Fields = make([]encoder.Doc, 9)
AWSConfigDoc.Fields[0].Name = "region"
AWSConfigDoc.Fields[0].Type = "string"
AWSConfigDoc.Fields[0].Note = ""
AWSConfigDoc.Fields[0].Description = "AWS data center region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions"
AWSConfigDoc.Fields[0].Comments[encoder.LineComment] = "AWS data center region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions"
AWSConfigDoc.Fields[1].Name = "zone"
AWSConfigDoc.Fields[1].Type = "string"
AWSConfigDoc.Fields[1].Note = ""
AWSConfigDoc.Fields[1].Description = "AWS data center zone name in defined region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones"
AWSConfigDoc.Fields[1].Comments[encoder.LineComment] = "AWS data center zone name in defined region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones"
AWSConfigDoc.Fields[2].Name = "image"
AWSConfigDoc.Fields[2].Type = "string"
AWSConfigDoc.Fields[2].Note = ""
AWSConfigDoc.Fields[2].Description = "AMI ID of the machine image used to create Constellation nodes."
AWSConfigDoc.Fields[2].Comments[encoder.LineComment] = "AMI ID of the machine image used to create Constellation nodes."
AWSConfigDoc.Fields[3].Name = "instanceType"
AWSConfigDoc.Fields[3].Type = "string"
AWSConfigDoc.Fields[3].Note = ""
AWSConfigDoc.Fields[3].Description = "VM instance type to use for Constellation nodes. Needs to support NitroTPM. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html"
AWSConfigDoc.Fields[3].Comments[encoder.LineComment] = "VM instance type to use for Constellation nodes. Needs to support NitroTPM. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html"
AWSConfigDoc.Fields[4].Name = "stateDiskType"
AWSConfigDoc.Fields[4].Type = "string"
AWSConfigDoc.Fields[4].Note = ""
AWSConfigDoc.Fields[4].Description = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html"
AWSConfigDoc.Fields[4].Comments[encoder.LineComment] = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html"
AWSConfigDoc.Fields[5].Name = "iamProfileControlPlane"
AWSConfigDoc.Fields[5].Type = "string"
AWSConfigDoc.Fields[5].Note = ""
AWSConfigDoc.Fields[5].Description = "Name of the IAM profile to use for the control plane nodes."
AWSConfigDoc.Fields[5].Comments[encoder.LineComment] = "Name of the IAM profile to use for the control plane nodes."
AWSConfigDoc.Fields[6].Name = "iamProfileWorkerNodes"
AWSConfigDoc.Fields[6].Type = "string"
AWSConfigDoc.Fields[6].Note = ""
AWSConfigDoc.Fields[6].Description = "Name of the IAM profile to use for the worker nodes."
AWSConfigDoc.Fields[6].Comments[encoder.LineComment] = "Name of the IAM profile to use for the worker nodes."
AWSConfigDoc.Fields[7].Name = "measurements"
AWSConfigDoc.Fields[7].Type = "Measurements"
AWSConfigDoc.Fields[7].Note = ""
AWSConfigDoc.Fields[7].Description = "Expected VM measurements."
AWSConfigDoc.Fields[7].Comments[encoder.LineComment] = "Expected VM measurements."
AWSConfigDoc.Fields[8].Name = "enforcedMeasurements"
AWSConfigDoc.Fields[8].Type = "[]uint32"
AWSConfigDoc.Fields[8].Note = ""
AWSConfigDoc.Fields[8].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AWSConfigDoc.Fields[8].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AzureConfigDoc.Type = "AzureConfig" AzureConfigDoc.Type = "AzureConfig"
AzureConfigDoc.Comments[encoder.LineComment] = "AzureConfig are Azure specific configuration values used by the CLI." AzureConfigDoc.Comments[encoder.LineComment] = "AzureConfig are Azure specific configuration values used by the CLI."
@ -367,6 +429,10 @@ func (_ ProviderConfig) Doc() *encoder.Doc {
return &ProviderConfigDoc return &ProviderConfigDoc
} }
func (_ AWSConfig) Doc() *encoder.Doc {
return &AWSConfigDoc
}
func (_ AzureConfig) Doc() *encoder.Doc { func (_ AzureConfig) Doc() *encoder.Doc {
return &AzureConfigDoc return &AzureConfigDoc
} }
@ -389,6 +455,7 @@ func GetConfigurationDoc() *encoder.FileDoc {
&UpgradeConfigDoc, &UpgradeConfigDoc,
&UserKeyDoc, &UserKeyDoc,
&ProviderConfigDoc, &ProviderConfigDoc,
&AWSConfigDoc,
&AzureConfigDoc, &AzureConfigDoc,
&GCPConfigDoc, &GCPConfigDoc,
&QEMUConfigDoc, &QEMUConfigDoc,

View File

@ -109,7 +109,7 @@ func TestFromFile(t *testing.T) {
} }
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
const defaultMsgCount = 15 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default const defaultMsgCount = 20 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
testCases := map[string]struct { testCases := map[string]struct {
cnf *Config cnf *Config
@ -170,6 +170,10 @@ func TestImage(t *testing.T) {
cfg *Config cfg *Config
wantImage string wantImage string
}{ }{
"default aws": {
cfg: func() *Config { c := Default(); c.RemoveProviderExcept(cloudprovider.AWS); return c }(),
wantImage: Default().Provider.AWS.Image,
},
"default azure": { "default azure": {
cfg: func() *Config { c := Default(); c.RemoveProviderExcept(cloudprovider.Azure); return c }(), cfg: func() *Config { c := Default(); c.RemoveProviderExcept(cloudprovider.Azure); return c }(),
wantImage: Default().Provider.Azure.Image, wantImage: Default().Provider.Azure.Image,
@ -197,10 +201,15 @@ func TestImage(t *testing.T) {
func TestConfigRemoveProviderExcept(t *testing.T) { func TestConfigRemoveProviderExcept(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
removeExcept cloudprovider.Provider removeExcept cloudprovider.Provider
wantAWS *AWSConfig
wantAzure *AzureConfig wantAzure *AzureConfig
wantGCP *GCPConfig wantGCP *GCPConfig
wantQEMU *QEMUConfig wantQEMU *QEMUConfig
}{ }{
"except aws": {
removeExcept: cloudprovider.AWS,
wantAWS: Default().Provider.AWS,
},
"except azure": { "except azure": {
removeExcept: cloudprovider.Azure, removeExcept: cloudprovider.Azure,
wantAzure: Default().Provider.Azure, wantAzure: Default().Provider.Azure,
@ -215,6 +224,7 @@ func TestConfigRemoveProviderExcept(t *testing.T) {
}, },
"unknown provider": { "unknown provider": {
removeExcept: cloudprovider.Unknown, removeExcept: cloudprovider.Unknown,
wantAWS: Default().Provider.AWS,
wantAzure: Default().Provider.Azure, wantAzure: Default().Provider.Azure,
wantGCP: Default().Provider.GCP, wantGCP: Default().Provider.GCP,
wantQEMU: Default().Provider.QEMU, wantQEMU: Default().Provider.QEMU,
@ -228,6 +238,7 @@ func TestConfigRemoveProviderExcept(t *testing.T) {
conf := Default() conf := Default()
conf.RemoveProviderExcept(tc.removeExcept) conf.RemoveProviderExcept(tc.removeExcept)
assert.Equal(tc.wantAWS, conf.Provider.AWS)
assert.Equal(tc.wantAzure, conf.Provider.Azure) assert.Equal(tc.wantAzure, conf.Provider.Azure)
assert.Equal(tc.wantGCP, conf.Provider.GCP) assert.Equal(tc.wantGCP, conf.Provider.GCP)
assert.Equal(tc.wantQEMU, conf.Provider.QEMU) assert.Equal(tc.wantQEMU, conf.Provider.QEMU)
@ -256,6 +267,15 @@ func TestConfig_UpdateMeasurements(t *testing.T) {
3: []byte{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}, 3: []byte{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
} }
{ // AWS
conf := Default()
conf.RemoveProviderExcept(cloudprovider.AWS)
for k := range conf.Provider.AWS.Measurements {
delete(conf.Provider.AWS.Measurements, k)
}
conf.UpdateMeasurements(newMeasurements)
assert.Equal(newMeasurements, conf.Provider.AWS.Measurements)
}
{ // Azure { // Azure
conf := Default() conf := Default()
conf.RemoveProviderExcept(cloudprovider.Azure) conf.RemoveProviderExcept(cloudprovider.Azure)
@ -290,6 +310,7 @@ func TestConfig_IsImageDebug(t *testing.T) {
conf *Config conf *Config
want bool want bool
}{ }{
// TODO: Add AWS when we know the format of published images & debug images
"gcp release": { "gcp release": {
conf: func() *Config { conf: func() *Config {
conf := Default() conf := Default()
@ -352,6 +373,11 @@ func TestValidInstanceTypeForProvider(t *testing.T) {
instanceTypes: []string{}, instanceTypes: []string{},
expectedResult: false, expectedResult: false,
}, },
"empty aws": {
provider: cloudprovider.AWS,
instanceTypes: []string{},
expectedResult: false,
},
"empty azure only CVMs": { "empty azure only CVMs": {
provider: cloudprovider.Azure, provider: cloudprovider.Azure,
instanceTypes: []string{}, instanceTypes: []string{},
@ -384,7 +410,7 @@ func TestValidInstanceTypeForProvider(t *testing.T) {
instanceTypes: instancetypes.AzureTrustedLaunchInstanceTypes, instanceTypes: instancetypes.AzureTrustedLaunchInstanceTypes,
expectedResult: false, expectedResult: false,
}, },
"azure trusted launch VMs with CVMs disbled": { "azure trusted launch VMs with CVMs disabled": {
provider: cloudprovider.Azure, provider: cloudprovider.Azure,
instanceTypes: instancetypes.AzureTrustedLaunchInstanceTypes, instanceTypes: instancetypes.AzureTrustedLaunchInstanceTypes,
nonCVMsAllowed: true, nonCVMsAllowed: true,
@ -417,6 +443,28 @@ func TestValidInstanceTypeForProvider(t *testing.T) {
nonCVMsAllowed: true, nonCVMsAllowed: true,
expectedResult: false, expectedResult: false,
}, },
// Testing every possible instance type for AWS is not feasible, so we just test a few based on known supported / unsupported families
// Also serves as a test for checkIfInstanceInValidAWSFamilys
"aws two valid instances": {
provider: cloudprovider.AWS,
instanceTypes: []string{"c5.xlarge", "c5a.2xlarge", "c5a.16xlarge", "u-12tb1.112xlarge"},
expectedResult: true,
},
"aws one valid instance one with too little vCPUs": {
provider: cloudprovider.AWS,
instanceTypes: []string{"c5.medium"},
expectedResult: false,
},
"aws graviton sub-family unsupported": {
provider: cloudprovider.AWS,
instanceTypes: []string{"m6g.xlarge", "r6g.2xlarge", "x2gd.xlarge", "g5g.8xlarge"},
expectedResult: false,
},
"aws combined two valid instances as one string": {
provider: cloudprovider.AWS,
instanceTypes: []string{"c5.xlarge, c5a.2xlarge"},
expectedResult: false,
},
} }
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {

View File

@ -0,0 +1,50 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package instancetypes
// Derived from: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html (Last updated: October 20th, 2022).
var AWSSupportedInstanceFamilies = []string{
"C5",
"C5a",
"C5ad",
"C5d",
"C5n",
"C6i",
"D3",
"D3en",
"G4dn",
"G5",
"Hpc6a",
"I3en",
"I4i",
"Inf1",
"M5",
"M5a",
"M5ad",
"M5d",
"M5dn",
"M5n",
"M5zn",
"M6a",
"M6i",
"R5",
"R5a",
"R5ad",
"R5b",
"R5d",
"R5dn",
"R5n",
"R6i",
"U-3tb1",
"U-6tb1",
"U-9tb1",
"U-12tb1",
"X2idn",
"X2iedn",
"X2iezn",
"z1d",
}

View File

@ -43,6 +43,12 @@ var (
uint32(vtpm.PCRIndexClusterID): zero, uint32(vtpm.PCRIndexClusterID): zero,
} }
// awsPCRs are the PCR values for an AWS Nitro Constellation node that are initially set in a generated config file.
awsPCRs = Measurements{
uint32(vtpm.PCRIndexOwnerID): {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
uint32(vtpm.PCRIndexClusterID): {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}
qemuPCRs = Measurements{ qemuPCRs = Measurements{
11: zero, 11: zero,
12: zero, 12: zero,

View File

@ -1,189 +0,0 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "3.4.3"
}
}
}
# Configure the AWS Provider
provider "aws" {
region = var.region
}
locals {
uid = random_id.uid.hex
name = "${var.name}-${local.uid}"
tag = "constellation-${local.uid}"
ports_node_range = "30000-32767"
ports_ssh = "22"
ports_kubernetes = "6443"
ports_bootstrapper = "9000"
ports_konnectivity = "8132"
ports_verify = "30081"
ports_debugd = "4000"
cidr_vpc_subnet_nodes = "192.168.178.0/24"
}
resource "random_id" "uid" {
byte_length = 4
}
resource "aws_vpc" "vpc" {
cidr_block = "192.168.0.0/16"
tags = {
Name = "${local.name}-vpc"
}
}
resource "aws_subnet" "main" {
vpc_id = aws_vpc.vpc.id
cidr_block = local.cidr_vpc_subnet_nodes
tags = {
Name = "${local.name}-subnet"
}
}
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${local.name}-gateway"
}
}
resource "aws_security_group" "security_group" {
name = local.name
vpc_id = aws_vpc.vpc.id
description = "Security group for ${local.name}"
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
ingress {
from_port = split("-", local.ports_node_range)[0]
to_port = split("-", local.ports_node_range)[1]
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "K8s node ports"
}
ingress {
from_port = local.ports_bootstrapper
to_port = local.ports_bootstrapper
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "bootstrapper"
}
ingress {
from_port = local.ports_kubernetes
to_port = local.ports_kubernetes
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "kubernetes"
}
ingress {
from_port = local.ports_konnectivity
to_port = local.ports_konnectivity
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "konnectivity"
}
ingress {
from_port = local.ports_debugd
to_port = local.ports_debugd
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "debugd"
}
}
module "load_balancer_bootstrapper" {
source = "./modules/load_balancer"
name = "${local.name}-bootstrapper"
vpc = aws_vpc.vpc.id
subnet = aws_subnet.main.id
port = local.ports_bootstrapper
}
module "load_balancer_kubernetes" {
source = "./modules/load_balancer"
name = "${local.name}-kubernetes"
vpc = aws_vpc.vpc.id
subnet = aws_subnet.main.id
port = local.ports_kubernetes
}
module "load_balancer_verify" {
source = "./modules/load_balancer"
name = "${local.name}-verify"
vpc = aws_vpc.vpc.id
subnet = aws_subnet.main.id
port = local.ports_verify
}
module "load_balancer_debugd" {
source = "./modules/load_balancer"
name = "${local.name}-debugd"
vpc = aws_vpc.vpc.id
subnet = aws_subnet.main.id
port = local.ports_debugd
}
module "load_balancer_konnectivity" {
source = "./modules/load_balancer"
name = "${local.name}-konnectivity"
vpc = aws_vpc.vpc.id
subnet = aws_subnet.main.id
port = local.ports_konnectivity
}
module "instance_group_control_plane" {
source = "./modules/instance_group"
name = local.name
role = "control-plane"
uid = local.uid
instance_type = var.instance_type
instance_count = var.count_control_plane
image_id = var.ami
disk_size = var.disk_size
target_group_arns = [
module.load_balancer_bootstrapper.target_group_arn,
module.load_balancer_kubernetes.target_group_arn,
module.load_balancer_verify.target_group_arn,
module.load_balancer_debugd.target_group_arn
]
subnetwork = aws_subnet.main.id
iam_instance_profile = var.control_plane_iam_instance_profile
}
module "instance_group_worker_nodes" {
source = "./modules/instance_group"
name = local.name
role = "worker"
uid = local.uid
instance_type = var.instance_type
instance_count = var.count_worker_nodes
image_id = var.ami
disk_size = var.disk_size
subnetwork = aws_subnet.main.id
target_group_arns = []
iam_instance_profile = var.worker_nodes_iam_instance_profile
}

View File

@ -1,19 +0,0 @@
variable "name" {
type = string
description = "Name of the load balancer."
}
variable "port" {
type = string
description = "Port of the load balancer."
}
variable "vpc" {
type = string
description = "ID of the VPC."
}
variable "subnet" {
type = string
description = "ID of the subnets."
}

View File

@ -1,50 +0,0 @@
variable "name" {
type = string
description = "Name of your Constellation"
}
variable "worker_nodes_iam_instance_profile" {
type = string
description = "Name of the IAM instance profile for worker nodes"
}
variable "control_plane_iam_instance_profile" {
type = string
description = "Name of the IAM instance profile for control plane nodes"
}
variable "instance_type" {
type = string
description = "Instance type for worker nodes"
default = "t2.micro"
}
variable "disk_size" {
type = number
description = "Disk size for nodes [GB]"
default = 30
}
variable "count_control_plane" {
type = number
description = "Number of control plane nodes"
default = 1
}
variable "count_worker_nodes" {
type = number
description = "Number of worker nodes"
default = 1
}
variable "ami" {
type = string
description = "AMI ID"
default = "ami-02f3416038bdb17fb" // Ubuntu 22.04 LTS
}
variable "region" {
type = string
description = "AWS region"
default = "us-east-2"
}