Skip to content

Commit

Permalink
common: setup default VPC/subnet for public IPs
Browse files Browse the repository at this point in the history
If a template requests a public IP for the instance being created, and
no VPC or subnet is, we get the default VPC ID and a random subnet in
this VPC to associate for the instance being created.

This fixes a bug where the public IP was requested, but because no
subnet was specified, it wasn't associated explicitely.
  • Loading branch information
lbajolet-hashicorp committed Apr 17, 2023
1 parent 0a1c9ae commit 682b403
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 35 deletions.
77 changes: 70 additions & 7 deletions builder/common/step_network_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"math/rand"
"sort"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
confighelper "github.com/hashicorp/packer-plugin-sdk/template/config"
)

// StepNetworkInfo queries AWS for information about
Expand All @@ -20,13 +22,14 @@ import (
// subnet_id string - the Subnet ID
// availability_zone string - the AZ name
type StepNetworkInfo struct {
VpcId string
VpcFilter VpcFilterOptions
SubnetId string
SubnetFilter SubnetFilterOptions
AvailabilityZone string
SecurityGroupIds []string
SecurityGroupFilter SecurityGroupFilterOptions
VpcId string
VpcFilter VpcFilterOptions
SubnetId string
SubnetFilter SubnetFilterOptions
AssociatePublicIpAddress confighelper.Trilean
AvailabilityZone string
SecurityGroupIds []string
SecurityGroupFilter SecurityGroupFilterOptions
}

type subnetsSort []*ec2.Subnet
Expand Down Expand Up @@ -158,6 +161,66 @@ func (s *StepNetworkInfo) Run(ctx context.Context, state multistep.StateBag) mul
}
}

if s.AssociatePublicIpAddress != confighelper.TriUnset && s.SubnetId == "" {
ui.Say(fmt.Sprintf("Setting public IP address to %t on instance without a subnet ID",
*s.AssociatePublicIpAddress.ToBoolPointer()))
if s.VpcId == "" {
ui.Say("No VPC ID provided, Packer will choose one from the provided or default VPC")
vpcs, err := ec2conn.DescribeVpcs(&ec2.DescribeVpcsInput{
Filters: []*ec2.Filter{
{
Name: aws.String("is-default"),
Values: []*string{aws.String("true")},
},
},
})
if err != nil {
err := fmt.Errorf("Failed to describe VPCs: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}

if len(vpcs.Vpcs) != 1 {
err := fmt.Errorf("No default VPC found, please set one up for associating a public IP address to the instance")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defaultVPC := vpcs.Vpcs[0]

s.VpcId = *defaultVPC.VpcId
}

var err error

ui.Say(fmt.Sprintf("Inferring subnet from the selected VPC %q", s.VpcId))
params := &ec2.DescribeSubnetsInput{}
filters := map[string]string{
"vpc-id": s.VpcId,
"state": "available",
}
params.Filters, err = buildEc2Filters(filters)
if err != nil {
err := fmt.Errorf("Failed to prepare subnet filters: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
subnets, err := ec2conn.DescribeSubnets(params)
if err != nil {
err := fmt.Errorf("Failed to describe subnets: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}

subnet := mostFreeSubnet(subnets.Subnets)
s.SubnetId = *subnet.SubnetId

ui.Say(fmt.Sprintf("Set subnet as %q", s.SubnetId))
}

state.Put("vpc_id", s.VpcId)
state.Put("availability_zone", s.AvailabilityZone)
state.Put("subnet_id", s.SubnetId)
Expand Down
3 changes: 3 additions & 0 deletions builder/common/step_run_source_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ func (s *StepRunSourceInstance) Run(ctx context.Context, state multistep.StateBa
subnetId := state.Get("subnet_id").(string)

if subnetId != "" && s.AssociatePublicIpAddress != confighelper.TriUnset {
ui.Say(fmt.Sprintf("changing public IP address config to %t for instance on subnet %q",
*s.AssociatePublicIpAddress.ToBoolPointer(),
subnetId))
runOpts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{
{
DeviceIndex: aws.Int64(0),
Expand Down
5 changes: 5 additions & 0 deletions builder/common/step_run_spot_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ func (s *StepRunSpotInstance) CreateTemplateData(userData *string, az string,

}

ui := state.Get("ui").(packersdk.Ui)

iamInstanceProfile := aws.String(state.Get("iamInstanceProfile").(string))

// Create a launch template.
Expand Down Expand Up @@ -142,6 +144,9 @@ func (s *StepRunSpotInstance) CreateTemplateData(userData *string, az string,
SubnetId: aws.String(subnetId),
}
if s.AssociatePublicIpAddress != confighelper.TriUnset {
ui.Say(fmt.Sprintf("changing public IP address config to %t for instance on subnet %q",
*s.AssociatePublicIpAddress.ToBoolPointer(),
subnetId))
networkInterface.SetAssociatePublicIpAddress(*s.AssociatePublicIpAddress.ToBoolPointer())
}
templateData.SetNetworkInterfaces([]*ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{&networkInterface})
Expand Down
15 changes: 8 additions & 7 deletions builder/ebs/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,13 +297,14 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
AMIVirtType: b.config.AMIVirtType,
},
&awscommon.StepNetworkInfo{
VpcId: b.config.VpcId,
VpcFilter: b.config.VpcFilter,
SecurityGroupIds: b.config.SecurityGroupIds,
SecurityGroupFilter: b.config.SecurityGroupFilter,
SubnetId: b.config.SubnetId,
SubnetFilter: b.config.SubnetFilter,
AvailabilityZone: b.config.AvailabilityZone,
VpcId: b.config.VpcId,
VpcFilter: b.config.VpcFilter,
SecurityGroupIds: b.config.SecurityGroupIds,
SecurityGroupFilter: b.config.SecurityGroupFilter,
SubnetId: b.config.SubnetId,
SubnetFilter: b.config.SubnetFilter,
AvailabilityZone: b.config.AvailabilityZone,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
},
&awscommon.StepKeyPair{
Debug: b.config.PackerDebug,
Expand Down
129 changes: 129 additions & 0 deletions builder/ebs/builder_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,85 @@ func TestAccBuilder_EBSWithSSHPassword_NoTempKeyCreated(t *testing.T) {
acctest.TestPlugin(t, testcase)
}

func TestAccBuilder_SpotInstanceWithPublicIPAddressExplicitelySet(t *testing.T) {
nonSpotInstance := amazon_acc.AMIHelper{
Region: "us-east-1",
Name: fmt.Sprintf("packer-ebs-explicit-public-ip-%d", time.Now().Unix()),
}

spotInstance := amazon_acc.AMIHelper{
Region: "us-east-1",
Name: fmt.Sprintf("packer-ebs-spot-explicit-public-ip-%d", time.Now().Unix()),
}
tests := []struct {
name string
IPVal bool
amiSetup amazon_acc.AMIHelper
template string
expectErr bool
}{
{
"Spot instance, with public IP explicitely set",
true,
spotInstance,
testSetupPublicIPWithoutVPCOrSubnetOnSpotInstance,
false,
},
{
"Spot instance, with public IP explicitely unset",
false,
spotInstance,
testSetupPublicIPWithoutVPCOrSubnetOnSpotInstance,
true, // We expect an error without a public IP since no outbound connections work in this case, so SSM doesn't work with the current config
},
{
"Non-Spot instance, with public IP explicitely set",
true,
nonSpotInstance,
testSetupPublicIPWithoutVPCOrSubnet,
false,
},
{
"Non-Spot instance, with public IP explicitely unset",
false,
nonSpotInstance,
testSetupPublicIPWithoutVPCOrSubnet,
true, // We expect an error without a public IP since no outbound connections work in this case, so SSM doesn't work with the current config
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testcase := &acctest.PluginTestCase{
Name: tt.name,
Template: fmt.Sprintf(tt.template, tt.amiSetup.Name, tt.IPVal),
Check: func(buildCommand *exec.Cmd, logfile string) error {
if (buildCommand.ProcessState.ExitCode() != 0) != tt.expectErr {
return fmt.Errorf("Bad exit code, expected %t error, got %d. Logfile: %s",
tt.expectErr,
buildCommand.ProcessState.ExitCode(),
logfile)
}

logs, err := os.ReadFile(logfile)
if err != nil {
return fmt.Errorf("couldn't read logs from logfile %s: %s", logfile, err)
}

expectMsg := fmt.Sprintf("changing public IP address config to %t for instance on subnet", tt.IPVal)

if !strings.Contains(string(logs), expectMsg) {
return fmt.Errorf("did not change the public IP setting for the instance")
}

return nil
},
}
acctest.TestPlugin(t, testcase)
})
}
}

const testBuilderAccBasic = `
{
"builders": [{
Expand Down Expand Up @@ -1541,6 +1620,56 @@ build {
}
`

const testSetupPublicIPWithoutVPCOrSubnet = `
source "amazon-ebs" "test_build" {
region = "us-east-1"
ami_name = "%s"
source_ami = "ami-06e46074ae430fba6" # Amazon Linux 2023 x86-64
instance_type = "t2.micro"
communicator = "ssh"
ssh_username = "ec2-user"
ssh_interface = "session_manager"
iam_instance_profile = "SSMInstanceProfile"
associate_public_ip_address = %t
skip_create_ami = true
}
build {
sources = ["amazon-ebs.test_build"]
}
`

const testSetupPublicIPWithoutVPCOrSubnetOnSpotInstance = `
source "amazon-ebs" "test" {
region = "us-east-1"
spot_price = "auto"
source_ami = "ami-06e46074ae430fba6" # Amazon Linux 2023 x86-64
instance_type = "t2.micro"
ssh_username = "ec2-user"
ssh_interface = "session_manager"
iam_instance_profile = "SSMInstanceProfile"
ami_name = "%s"
skip_create_ami = true
associate_public_ip_address = %t
temporary_iam_instance_profile_policy_document {
Version = "2012-10-17"
Statement {
Effect = "Allow"
Action = [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeInstanceTypeOfferings",
"ec2:DescribeInstanceCreditSpecifications"
]
Resource = ["*"]
}
}
}
build {
sources = ["source.amazon-ebs.test"]
}
`

const testWindowsFastBoot = `
source "amazon-ebs" "windows-fastboot" {
ami_name = "%s"
Expand Down
15 changes: 8 additions & 7 deletions builder/ebssurrogate/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,13 +325,14 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
AMIVirtType: b.config.AMIVirtType,
},
&awscommon.StepNetworkInfo{
VpcId: b.config.VpcId,
VpcFilter: b.config.VpcFilter,
SecurityGroupIds: b.config.SecurityGroupIds,
SecurityGroupFilter: b.config.SecurityGroupFilter,
SubnetId: b.config.SubnetId,
SubnetFilter: b.config.SubnetFilter,
AvailabilityZone: b.config.AvailabilityZone,
VpcId: b.config.VpcId,
VpcFilter: b.config.VpcFilter,
SecurityGroupIds: b.config.SecurityGroupIds,
SecurityGroupFilter: b.config.SecurityGroupFilter,
SubnetId: b.config.SubnetId,
SubnetFilter: b.config.SubnetFilter,
AvailabilityZone: b.config.AvailabilityZone,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
},
&awscommon.StepKeyPair{
Debug: b.config.PackerDebug,
Expand Down
15 changes: 8 additions & 7 deletions builder/ebsvolume/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,14 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
AmiFilters: b.config.SourceAmiFilter,
},
&awscommon.StepNetworkInfo{
VpcId: b.config.VpcId,
VpcFilter: b.config.VpcFilter,
SecurityGroupIds: b.config.SecurityGroupIds,
SecurityGroupFilter: b.config.SecurityGroupFilter,
SubnetId: b.config.SubnetId,
SubnetFilter: b.config.SubnetFilter,
AvailabilityZone: b.config.AvailabilityZone,
VpcId: b.config.VpcId,
VpcFilter: b.config.VpcFilter,
SecurityGroupIds: b.config.SecurityGroupIds,
SecurityGroupFilter: b.config.SecurityGroupFilter,
SubnetId: b.config.SubnetId,
SubnetFilter: b.config.SubnetFilter,
AvailabilityZone: b.config.AvailabilityZone,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
},
&awscommon.StepKeyPair{
Debug: b.config.PackerDebug,
Expand Down
15 changes: 8 additions & 7 deletions builder/instance/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,13 +342,14 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
AMIVirtType: b.config.AMIVirtType,
},
&awscommon.StepNetworkInfo{
VpcId: b.config.VpcId,
VpcFilter: b.config.VpcFilter,
SecurityGroupIds: b.config.SecurityGroupIds,
SecurityGroupFilter: b.config.SecurityGroupFilter,
SubnetId: b.config.SubnetId,
SubnetFilter: b.config.SubnetFilter,
AvailabilityZone: b.config.AvailabilityZone,
VpcId: b.config.VpcId,
VpcFilter: b.config.VpcFilter,
SecurityGroupIds: b.config.SecurityGroupIds,
SecurityGroupFilter: b.config.SecurityGroupFilter,
SubnetId: b.config.SubnetId,
SubnetFilter: b.config.SubnetFilter,
AvailabilityZone: b.config.AvailabilityZone,
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
},
&awscommon.StepKeyPair{
Debug: b.config.PackerDebug,
Expand Down

0 comments on commit 682b403

Please sign in to comment.