Merge pull request #3855 from jeremy-asher/chroot-from-scratch

amazon-chroot build AMI from scratch
This commit is contained in:
Rickard von Essen 2016-09-12 13:48:04 +02:00 committed by GitHub
commit ed1b664d65
10 changed files with 370 additions and 84 deletions

View File

@ -25,19 +25,24 @@ const BuilderId = "mitchellh.amazon.chroot"
// Config is the configuration that is chained through the steps and
// settable from the template.
type Config struct {
common.PackerConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
awscommon.AMIConfig `mapstructure:",squash"`
common.PackerConfig `mapstructure:",squash"`
awscommon.AMIBlockDevices `mapstructure:",squash"`
awscommon.AMIConfig `mapstructure:",squash"`
awscommon.AccessConfig `mapstructure:",squash"`
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
CommandWrapper string `mapstructure:"command_wrapper"`
CopyFiles []string `mapstructure:"copy_files"`
DevicePath string `mapstructure:"device_path"`
MountPath string `mapstructure:"mount_path"`
SourceAmi string `mapstructure:"source_ami"`
RootVolumeSize int64 `mapstructure:"root_volume_size"`
MountOptions []string `mapstructure:"mount_options"`
MountPartition int `mapstructure:"mount_partition"`
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
CommandWrapper string `mapstructure:"command_wrapper"`
CopyFiles []string `mapstructure:"copy_files"`
DevicePath string `mapstructure:"device_path"`
FromScratch bool `mapstructure:"from_scratch"`
MountOptions []string `mapstructure:"mount_options"`
MountPartition int `mapstructure:"mount_partition"`
MountPath string `mapstructure:"mount_path"`
PostMountCommands []string `mapstructure:"post_mount_commands"`
PreMountCommands []string `mapstructure:"pre_mount_commands"`
RootDeviceName string `mapstructure:"root_device_name"`
RootVolumeSize int64 `mapstructure:"root_volume_size"`
SourceAmi string `mapstructure:"source_ami"`
ctx interpolate.Context
}
@ -59,6 +64,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"command_wrapper",
"post_mount_commands",
"pre_mount_commands",
"mount_path",
},
},
@ -86,7 +93,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
}
}
if len(b.config.CopyFiles) == 0 {
if len(b.config.CopyFiles) == 0 && !b.config.FromScratch {
b.config.CopyFiles = []string{"/etc/resolv.conf"}
}
@ -102,8 +109,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.MountPartition = 1
}
// Accumulate any errors
// Accumulate any errors or warnings
var errs *packer.MultiError
var warns []string
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(&b.config.ctx)...)
@ -115,16 +124,49 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
}
}
if b.config.SourceAmi == "" {
errs = packer.MultiErrorAppend(errs, errors.New("source_ami is required."))
if b.config.FromScratch {
if b.config.SourceAmi != "" {
warns = append(warns, "source_ami is unused when from_scratch is true")
}
if b.config.RootVolumeSize == 0 {
errs = packer.MultiErrorAppend(
errs, errors.New("root_volume_size is required with from_scratch."))
}
if len(b.config.PreMountCommands) == 0 {
errs = packer.MultiErrorAppend(
errs, errors.New("pre_mount_commands is required with from_scratch."))
}
if b.config.AMIVirtType == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("ami_virtualization_type is required with from_scratch."))
}
if b.config.RootDeviceName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("root_device_name is required with from_scratch."))
}
if len(b.config.AMIMappings) == 0 {
errs = packer.MultiErrorAppend(
errs, errors.New("ami_block_device_mappings is required with from_scratch."))
}
} else {
if b.config.SourceAmi == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("source_ami is required."))
}
if len(b.config.AMIMappings) != 0 {
warns = append(warns, "ami_block_device_mappings are unused when from_scratch is false")
}
if b.config.RootDeviceName != "" {
warns = append(warns, "root_device_name is unused when from_scratch is false")
}
}
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
return warns, errs
}
log.Println(common.ScrubConfig(b.config, b.config.AccessKey, b.config.SecretKey))
return nil, nil
return warns, nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
@ -161,11 +203,19 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
ForceDeregister: b.config.AMIForceDeregister,
},
&StepInstanceInfo{},
&awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi,
EnhancedNetworking: b.config.AMIEnhancedNetworking,
},
&StepCheckRootDevice{},
}
if !b.config.FromScratch {
steps = append(steps,
&awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi,
EnhancedNetworking: b.config.AMIEnhancedNetworking,
},
&StepCheckRootDevice{},
)
}
steps = append(steps,
&StepFlock{},
&StepPrepareDevice{},
&StepCreateVolume{
@ -173,10 +223,16 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
&StepAttachVolume{},
&StepEarlyUnflock{},
&StepPreMountCommands{
Commands: b.config.PreMountCommands,
},
&StepMountDevice{
MountOptions: b.config.MountOptions,
MountPartition: b.config.MountPartition,
},
&StepPostMountCommands{
Commands: b.config.PostMountCommands,
},
&StepMountExtra{},
&StepCopyFiles{},
&StepChrootProvision{},
@ -203,7 +259,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&awscommon.StepCreateTags{
Tags: b.config.AMITags,
},
}
)
// Run!
if b.config.PackerDebug {

View File

@ -0,0 +1,37 @@
package chroot
import (
"fmt"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/post-processor/shell-local"
"github.com/mitchellh/packer/template/interpolate"
)
func RunLocalCommands(commands []string, wrappedCommand CommandWrapper, ctx interpolate.Context, ui packer.Ui) error {
for _, rawCmd := range commands {
intCmd, err := interpolate.Render(rawCmd, &ctx)
if err != nil {
return fmt.Errorf("Error interpolating: %s", err)
}
command, err := wrappedCommand(intCmd)
if err != nil {
return fmt.Errorf("Error wrapping command: %s", err)
}
ui.Say(fmt.Sprintf("Executing command: %s", command))
comm := &shell_local.Communicator{}
cmd := &packer.RemoteCmd{Command: command}
if err := cmd.StartWithUi(comm, ui); err != nil {
return fmt.Errorf("Error executing command: %s", err)
}
if cmd.ExitStatus != 0 {
return fmt.Errorf(
"Received non-zero exit code %d from command: %s",
cmd.ExitStatus,
command)
}
}
return nil
}

View File

@ -22,40 +22,52 @@ type StepCreateVolume struct {
}
func (s *StepCreateVolume) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ec2conn := state.Get("ec2").(*ec2.EC2)
image := state.Get("source_image").(*ec2.Image)
instance := state.Get("instance").(*ec2.Instance)
ui := state.Get("ui").(packer.Ui)
// Determine the root device snapshot
log.Printf("Searching for root device of the image (%s)", *image.RootDeviceName)
var rootDevice *ec2.BlockDeviceMapping
for _, device := range image.BlockDeviceMappings {
if *device.DeviceName == *image.RootDeviceName {
rootDevice = device
break
var createVolume *ec2.CreateVolumeInput
if config.FromScratch {
createVolume = &ec2.CreateVolumeInput{
AvailabilityZone: instance.Placement.AvailabilityZone,
Size: aws.Int64(s.RootVolumeSize),
VolumeType: aws.String(ec2.VolumeTypeGp2),
}
} else {
// Determine the root device snapshot
image := state.Get("source_image").(*ec2.Image)
log.Printf("Searching for root device of the image (%s)", *image.RootDeviceName)
var rootDevice *ec2.BlockDeviceMapping
for _, device := range image.BlockDeviceMappings {
if *device.DeviceName == *image.RootDeviceName {
rootDevice = device
break
}
}
if rootDevice == nil {
err := fmt.Errorf("Couldn't find root device!")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Creating the root volume...")
vs := *rootDevice.Ebs.VolumeSize
if s.RootVolumeSize > *rootDevice.Ebs.VolumeSize {
vs = s.RootVolumeSize
}
createVolume = &ec2.CreateVolumeInput{
AvailabilityZone: instance.Placement.AvailabilityZone,
Size: aws.Int64(vs),
SnapshotId: rootDevice.Ebs.SnapshotId,
VolumeType: rootDevice.Ebs.VolumeType,
Iops: rootDevice.Ebs.Iops,
}
}
if rootDevice == nil {
err := fmt.Errorf("Couldn't find root device!")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Creating the root volume...")
vs := *rootDevice.Ebs.VolumeSize
if s.RootVolumeSize > *rootDevice.Ebs.VolumeSize {
vs = s.RootVolumeSize
}
createVolume := &ec2.CreateVolumeInput{
AvailabilityZone: instance.Placement.AvailabilityZone,
Size: aws.Int64(vs),
SnapshotId: rootDevice.Ebs.SnapshotId,
VolumeType: rootDevice.Ebs.VolumeType,
Iops: rootDevice.Ebs.Iops,
}
log.Printf("Create args: %+v", createVolume)
createVolumeResp, err := ec2conn.CreateVolume(createVolume)

View File

@ -33,10 +33,18 @@ type StepMountDevice struct {
func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
image := state.Get("source_image").(*ec2.Image)
device := state.Get("device").(string)
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
var virtualizationType string
if config.FromScratch {
virtualizationType = config.AMIVirtType
} else {
image := state.Get("source_image").(*ec2.Image)
virtualizationType = *image.VirtualizationType
log.Printf("Source image virtualization type is: %s", virtualizationType)
}
ctx := config.ctx
ctx.Data = &mountPathData{Device: filepath.Base(device)}
mountPath, err := interpolate.Render(config.MountPath, &ctx)
@ -65,9 +73,8 @@ func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt
}
log.Printf("Source image virtualization type is: %s", *image.VirtualizationType)
deviceMount := device
if *image.VirtualizationType == "hvm" {
if virtualizationType == "hvm" {
deviceMount = fmt.Sprintf("%s%d", device, s.MountPartition)
}
state.Put("deviceMount", deviceMount)

View File

@ -0,0 +1,45 @@
package chroot
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type postMountCommandsData struct {
Device string
MountPath string
}
// StepPostMountCommands allows running arbitrary commands after mounting the
// device, but prior to the bind mount and copy steps.
type StepPostMountCommands struct {
Commands []string
}
func (s *StepPostMountCommands) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
device := state.Get("device").(string)
mountPath := state.Get("mount_path").(string)
ui := state.Get("ui").(packer.Ui)
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
if len(s.Commands) == 0 {
return multistep.ActionContinue
}
ctx := config.ctx
ctx.Data = &postMountCommandsData{
Device: device,
MountPath: mountPath,
}
ui.Say("Running post-mount commands...")
if err := RunLocalCommands(s.Commands, wrappedCommand, ctx, ui); err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepPostMountCommands) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,39 @@
package chroot
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type preMountCommandsData struct {
Device string
}
// StepPreMountCommands sets up the a new block device when building from scratch
type StepPreMountCommands struct {
Commands []string
}
func (s *StepPreMountCommands) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
device := state.Get("device").(string)
ui := state.Get("ui").(packer.Ui)
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
if len(s.Commands) == 0 {
return multistep.ActionContinue
}
ctx := config.ctx
ctx.Data = &preMountCommandsData{Device: device}
ui.Say("Running device setup commands...")
if err := RunLocalCommands(s.Commands, wrappedCommand, ctx, ui); err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepPreMountCommands) Cleanup(state multistep.StateBag) {}

View File

@ -18,22 +18,36 @@ type StepRegisterAMI struct {
func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ec2conn := state.Get("ec2").(*ec2.EC2)
image := state.Get("source_image").(*ec2.Image)
snapshotId := state.Get("snapshot_id").(string)
ui := state.Get("ui").(packer.Ui)
ui.Say("Registering the AMI...")
blockDevices := make([]*ec2.BlockDeviceMapping, len(image.BlockDeviceMappings))
for i, device := range image.BlockDeviceMappings {
var (
registerOpts *ec2.RegisterImageInput
blockDevices []*ec2.BlockDeviceMapping
image *ec2.Image
rootDeviceName string
)
if config.FromScratch {
blockDevices = config.AMIBlockDevices.BuildAMIDevices()
rootDeviceName = config.RootDeviceName
} else {
image = state.Get("source_image").(*ec2.Image)
blockDevices = make([]*ec2.BlockDeviceMapping, len(image.BlockDeviceMappings))
rootDeviceName = *image.RootDeviceName
}
for i, device := range blockDevices {
newDevice := device
if *newDevice.DeviceName == *image.RootDeviceName {
if *newDevice.DeviceName == rootDeviceName {
if newDevice.Ebs != nil {
newDevice.Ebs.SnapshotId = aws.String(snapshotId)
} else {
newDevice.Ebs = &ec2.EbsBlockDevice{SnapshotId: aws.String(snapshotId)}
}
if s.RootVolumeSize > *newDevice.Ebs.VolumeSize {
if config.FromScratch || s.RootVolumeSize > *newDevice.Ebs.VolumeSize {
newDevice.Ebs.VolumeSize = aws.Int64(s.RootVolumeSize)
}
}
@ -47,7 +61,17 @@ func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
blockDevices[i] = newDevice
}
registerOpts := buildRegisterOpts(config, image, blockDevices)
if config.FromScratch {
registerOpts = &ec2.RegisterImageInput{
Name: &config.AMIName,
Architecture: aws.String(ec2.ArchitectureValuesX8664),
RootDeviceName: aws.String(rootDeviceName),
VirtualizationType: aws.String(config.AMIVirtType),
BlockDeviceMappings: blockDevices,
}
} else {
registerOpts = buildRegisterOpts(config, image, blockDevices)
}
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
if config.AMIEnhancedNetworking {
@ -105,6 +129,5 @@ func buildRegisterOpts(config *Config, image *ec2.Image, blockDevices []*ec2.Blo
registerOpts.KernelId = image.KernelId
registerOpts.RamdiskId = image.RamdiskId
}
return registerOpts
}

View File

@ -22,7 +22,15 @@ type BlockDevice struct {
}
type BlockDevices struct {
AMIMappings []BlockDevice `mapstructure:"ami_block_device_mappings"`
AMIBlockDevices `mapstructure:",squash"`
LaunchBlockDevices `mapstructure:",squash"`
}
type AMIBlockDevices struct {
AMIMappings []BlockDevice `mapstructure:"ami_block_device_mappings"`
}
type LaunchBlockDevices struct {
LaunchMappings []BlockDevice `mapstructure:"launch_block_device_mappings"`
}
@ -77,10 +85,10 @@ func (b *BlockDevices) Prepare(ctx *interpolate.Context) []error {
return nil
}
func (b *BlockDevices) BuildAMIDevices() []*ec2.BlockDeviceMapping {
func (b *AMIBlockDevices) BuildAMIDevices() []*ec2.BlockDeviceMapping {
return buildBlockDevices(b.AMIMappings)
}
func (b *BlockDevices) BuildLaunchDevices() []*ec2.BlockDeviceMapping {
func (b *LaunchBlockDevices) BuildLaunchDevices() []*ec2.BlockDeviceMapping {
return buildBlockDevices(b.LaunchMappings)
}

View File

@ -124,22 +124,26 @@ func TestBlockDevice(t *testing.T) {
}
for _, tc := range cases {
blockDevices := BlockDevices{
AMIMappings: []BlockDevice{*tc.Config},
amiBlockDevices := AMIBlockDevices{
AMIMappings: []BlockDevice{*tc.Config},
}
launchBlockDevices := LaunchBlockDevices{
LaunchMappings: []BlockDevice{*tc.Config},
}
expected := []*ec2.BlockDeviceMapping{tc.Result}
got := blockDevices.BuildAMIDevices()
if !reflect.DeepEqual(expected, got) {
amiResults := amiBlockDevices.BuildAMIDevices()
if !reflect.DeepEqual(expected, amiResults) {
t.Fatalf("Bad block device, \nexpected: %#v\n\ngot: %#v",
expected, got)
expected, amiResults)
}
if !reflect.DeepEqual(expected, blockDevices.BuildLaunchDevices()) {
launchResults := launchBlockDevices.BuildLaunchDevices()
if !reflect.DeepEqual(expected, launchResults) {
t.Fatalf("Bad block device, \nexpected: %#v\n\ngot: %#v",
expected,
blockDevices.BuildLaunchDevices())
expected, launchResults)
}
}
}

View File

@ -6,7 +6,7 @@ description: |
in the EC2 documentation.
layout: docs
page_title: 'Amazon AMI Builder (chroot)'
...
---
# AMI Builder (chroot)
@ -69,7 +69,8 @@ each category, the available configuration keys are alphabetized.
- `source_ami` (string) - The source AMI whose root volume will be copied and
provisioned on the currently running instance. This must be an EBS-backed
AMI with a root volume snapshot that you have access to.
AMI with a root volume snapshot that you have access to. Note: this is not
used when `from_scratch` is set to true.
### Optional:
@ -102,11 +103,11 @@ each category, the available configuration keys are alphabetized.
section below. Please read that section for more information on how to
use this.
- `command_wrapper` (string) - How to run shell commands. This defaults
to "{{.Command}}". This may be useful to set if you want to set
environmental variables or perhaps run it with `sudo` or so on. This is a
configuration template where the `.Command` variable is replaced with the
command to be run.
- `command_wrapper` (string) - How to run shell commands. This defaults to
`{{.Command}}`. This may be useful to set if you want to set environmental
variables or perhaps run it with `sudo` or so on. This is a configuration
template where the `.Command` variable is replaced with the command to
be run.
- `copy_files` (array of strings) - Paths to files on the running EC2 instance
that will be copied into the chroot environment prior to provisioning. This
@ -121,7 +122,20 @@ each category, the available configuration keys are alphabetized.
`ec2:ModifyInstanceAttribute` to your AWS IAM policy.
- `force_deregister` (boolean) - Force Packer to first deregister an existing
AMI if one with the same name already exists. Default `false`.
AMI if one with the same name already exists. Default false.
- `from_scratch` (boolean) - Build a new volume instead of starting from an
existing AMI root volume snapshot. Default false. If true, `source_ami` is
no longer used and the following options become required:
`ami_virtualization_type`, `pre_mount_commands` and `root_volume_size`. The
below options are also required in this mode only:
- `ami_block_device_mappings` (array of block device mappings) An entry
matching `root_device_name` should be set. See the
[amazon-ebs](/docs/builders/amazon-ebs.html) documentation for more
details on this parameter.
- `root_device_name` (string) - The root device name. For example, `xvda`.
- `mount_path` (string) - The path where the volume will be mounted. This is
where the chroot environment will be. This defaults to
@ -129,8 +143,8 @@ each category, the available configuration keys are alphabetized.
where the `.Device` variable is replaced with the name of the device where
the volume is attached.
- `mount_partition` (integer) - The partition number containing the /
partition. By default this is the first partition of the volume.
- `mount_partition` (integer) - The partition number containing the
/ partition. By default this is the first partition of the volume.
- `mount_options` (array of strings) - Options to supply the `mount` command
when mounting devices. Each option will be prefixed with `-o` and supplied
@ -139,11 +153,22 @@ each category, the available configuration keys are alphabetized.
command](http://linuxcommand.org/man_pages/mount8.html) for valid file
system specific options
- `pre_mount_commands` (array of strings) - A series of commands to execute
after attaching the root volume and before mounting the chroot. This is not
required unless using `from_scratch`. If so, this should include any
partitioning and filesystem creation commands. The path to the device is
provided by `{{.Device}}`.
- `post_mount_commands` (array of strings) - As `pre_mount_commands`, but the
commands are executed after mounting the root device and before the extra
mount and copy steps. The device and mount path are provided by
`{{.Device}}` and `{{.MountPath}}`.
- `root_volume_size` (integer) - The size of the root volume for the chroot
environment, and the resulting AMI
- `skip_region_validation` (boolean) - Set to true if you want to skip
validation of the ami_regions configuration option. Defaults to false.
- `skip_region_validation` (boolean) - Set to true if you want to skip
validation of the `ami_regions` configuration option. Defaults to false.
- `tags` (object of key/value strings) - Tags applied to the AMI.
@ -236,3 +261,33 @@ services:
]
}
```
## Building From Scratch
This example demonstrates the essentials of building an image from scratch. A
15G gp2 (SSD) device is created (overriding the default of standard/magnetic).
The device setup commands partition the device with one partition for use as an
HVM image and format it ext4. This builder block should be followed by
provisioning commands to install the os and bootloader.
``` {.javascript}
{
"type": "amazon-chroot",
"ami_name": "packer-from-scratch {{timestamp}}"
"from_scratch": true,
"ami_virtualization_type": "hvm",
"device_setup_commands": [
"parted {{.Device}} mklabel msdos mkpart primary 1M 100% set 1 boot on print",
"mkfs.ext4 {{.Device}}1"
],
"root_volume_size": 15,
"root_device_name": "xvda",
"ami_block_device_mappings": [
{
"device_name": "xvda",
"delete_on_termination": true,
"volume_type": "gp2"
}
]
}
```