//go:generate struct-markdown //go:generate mapstructure-to-hcl2 -type BlockDevice package common import ( "fmt" "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/packer-plugin-sdk/template/config" "github.com/hashicorp/packer-plugin-sdk/template/interpolate" ) const ( minIops = 100 maxIops = 64000 minIopsGp3 = 3000 maxIopsGp3 = 16000 minThroughput = 125 maxThroughput = 1000 ) // These will be attached when launching your instance. Your // options here may vary depending on the type of VM you use. // // Example use case: // // The following mapping will tell Packer to encrypt the root volume of the // build instance at launch using a specific non-default kms key: // // JSON example: // // ```json // launch_block_device_mappings: [ // { // "device_name": "/dev/sda1", // "encrypted": true, // "kms_key_id": "1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d" // } // ] // ``` // // HCL2 example: // // ```hcl // launch_block_device_mappings { // device_name = "/dev/sda1" // encrypted = true // kms_key_id = "1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d" // } // ``` // // Please note that the kms_key_id option in this example exists for // launch_block_device_mappings but not ami_block_device_mappings. // // Documentation for Block Devices Mappings can be found here: // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html // type BlockDevice struct { // Indicates whether the EBS volume is deleted on instance termination. // Default false. NOTE: If this value is not explicitly set to true and // volumes are not cleaned up by an alternative method, additional volumes // will accumulate after every build. DeleteOnTermination bool `mapstructure:"delete_on_termination" required:"false"` // The device name exposed to the instance (for example, /dev/sdh or xvdh). // Required for every device in the block device mapping. DeviceName string `mapstructure:"device_name" required:"false"` // Indicates whether or not to encrypt the volume. By default, Packer will // keep the encryption setting to what it was in the source image. Setting // false will result in an unencrypted device, and true will result in an // encrypted one. Encrypted config.Trilean `mapstructure:"encrypted" required:"false"` // The number of I/O operations per second (IOPS) that the volume supports. // See the documentation on // [IOPs](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EbsBlockDevice.html) // for more information IOPS int64 `mapstructure:"iops" required:"false"` // Suppresses the specified device included in the block device mapping of // the AMI. NoDevice bool `mapstructure:"no_device" required:"false"` // The ID of the snapshot. SnapshotId string `mapstructure:"snapshot_id" required:"false"` // The throughput for gp3 volumes, only valid for gp3 types // See the documentation on // [Throughput](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EbsBlockDevice.html) // for more information Throughput int64 `mapstructure:"throughput" required:"false"` // The virtual device name. See the documentation on Block Device Mapping // for more information. VirtualName string `mapstructure:"virtual_name" required:"false"` // The volume type. gp2 & gp3 for General Purpose (SSD) volumes, io1 & io2 // for Provisioned IOPS (SSD) volumes, st1 for Throughput Optimized HDD, // sc1 for Cold HDD, and standard for Magnetic volumes. VolumeType string `mapstructure:"volume_type" required:"false"` // The size of the volume, in GiB. Required if not specifying a // snapshot_id. VolumeSize int64 `mapstructure:"volume_size" required:"false"` // ID, alias or ARN of the KMS key to use for boot volume encryption. // This option exists for launch_block_device_mappings but not // ami_block_device_mappings. The kms key id defined here only applies to // the original build region; if the AMI gets copied to other regions, the // volume in those regions will be encrypted by the default EBS KMS key. // For valid formats see KmsKeyId in the [AWS API docs - // CopyImage](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CopyImage.html) // This field is validated by Packer. When using an alias, you will have to // prefix kms_key_id with alias/. KmsKeyId string `mapstructure:"kms_key_id" required:"false"` } type BlockDevices []BlockDevice func (bds BlockDevices) BuildEC2BlockDeviceMappings() []*ec2.BlockDeviceMapping { var blockDevices []*ec2.BlockDeviceMapping for _, blockDevice := range bds { blockDevices = append(blockDevices, blockDevice.BuildEC2BlockDeviceMapping()) } return blockDevices } func (blockDevice BlockDevice) BuildEC2BlockDeviceMapping() *ec2.BlockDeviceMapping { mapping := &ec2.BlockDeviceMapping{ DeviceName: aws.String(blockDevice.DeviceName), } if blockDevice.NoDevice { mapping.NoDevice = aws.String("") return mapping } else if blockDevice.VirtualName != "" { if strings.HasPrefix(blockDevice.VirtualName, "ephemeral") { mapping.VirtualName = aws.String(blockDevice.VirtualName) } return mapping } ebsBlockDevice := &ec2.EbsBlockDevice{ DeleteOnTermination: aws.Bool(blockDevice.DeleteOnTermination), } if blockDevice.VolumeType != "" { ebsBlockDevice.VolumeType = aws.String(blockDevice.VolumeType) } if blockDevice.VolumeSize > 0 { ebsBlockDevice.VolumeSize = aws.Int64(blockDevice.VolumeSize) } switch blockDevice.VolumeType { case "io1", "io2", "gp3": ebsBlockDevice.Iops = aws.Int64(blockDevice.IOPS) } // Throughput is only valid for gp3 types if blockDevice.VolumeType == "gp3" { ebsBlockDevice.Throughput = aws.Int64(blockDevice.Throughput) } // You cannot specify Encrypted if you specify a Snapshot ID if blockDevice.SnapshotId != "" { ebsBlockDevice.SnapshotId = aws.String(blockDevice.SnapshotId) } ebsBlockDevice.Encrypted = blockDevice.Encrypted.ToBoolPointer() if blockDevice.KmsKeyId != "" { ebsBlockDevice.KmsKeyId = aws.String(blockDevice.KmsKeyId) } mapping.Ebs = ebsBlockDevice return mapping } var iopsRatios = map[string]int64{ "io1": 50, "io2": 500, } func (b *BlockDevice) Prepare(ctx *interpolate.Context) error { if b.DeviceName == "" { return fmt.Errorf("The `device_name` must be specified " + "for every device in the block device mapping.") } // Warn that encrypted must be true or nil when setting kms_key_id if b.KmsKeyId != "" && b.Encrypted.False() { return fmt.Errorf("The device %v, must also have `encrypted: "+ "true` when setting a kms_key_id.", b.DeviceName) } if ratio, ok := iopsRatios[b.VolumeType]; b.VolumeSize != 0 && ok { if b.IOPS/b.VolumeSize > ratio { return fmt.Errorf("%s: the maximum ratio of provisioned IOPS to requested volume size "+ "(in GiB) is %v:1 for %s volumes", b.DeviceName, ratio, b.VolumeType) } if b.IOPS < minIops || b.IOPS > maxIops { return fmt.Errorf("IOPS must be between %d and %d for device %s", minIops, maxIops, b.DeviceName) } } if b.VolumeType == "gp3" { if b.Throughput < minThroughput || b.Throughput > maxThroughput { return fmt.Errorf("Throughput must be between %d and %d for device %s", minThroughput, maxThroughput, b.DeviceName) } if b.IOPS < minIopsGp3 || b.IOPS > maxIopsGp3 { return fmt.Errorf("IOPS must be between %d and %d for device %s", minIopsGp3, maxIopsGp3, b.DeviceName) } } else if b.Throughput > 0 { return fmt.Errorf("Throughput is not available for device %s", b.DeviceName) } _, err := interpolate.RenderInterface(&b, ctx) return err } func (bds BlockDevices) Prepare(ctx *interpolate.Context) (errs []error) { for _, block := range bds { if err := block.Prepare(ctx); err != nil { errs = append(errs, err) } } return errs }