azure: fit and finish

* More unit testing to assert customer's configuration.
 * Further reduce the options that are needed to power an Azure build.
   This seems like a much more manageable level.
 * Update all of the examples to use a more current VM sku.
 * Add an example for RHEL.
 * Move from OpenSuSE to SuSE.
 * Update the docs.
This commit is contained in:
Christopher Boumenot 2017-06-07 15:01:10 -07:00
parent 51e03d39e6
commit c09f8b84b9
18 changed files with 296 additions and 106 deletions

View File

@ -16,11 +16,25 @@ const (
)
type Artifact struct {
// VHD
StorageAccountLocation string
OSDiskUri string
TemplateUri string
OSDiskUriReadOnlySas string
TemplateUriReadOnlySas string
// Managed Image
ManagedImageResourceGroupName string
ManagedImageName string
ManagedImageLocation string
}
func NewManagedImageArtifact(resourceGroup, name, location string) (*Artifact, error) {
return &Artifact{
ManagedImageResourceGroupName: resourceGroup,
ManagedImageName: name,
ManagedImageLocation: location,
}, nil
}
func NewArtifact(template *CaptureTemplate, getSasUrl func(name string) string) (*Artifact, error) {
@ -78,6 +92,10 @@ func storageUriToTemplateUri(su *url.URL) (*url.URL, error) {
return url.Parse(strings.Replace(su.String(), filename, templateFilename, 1))
}
func (a *Artifact) isMangedImage() bool {
return a.ManagedImageResourceGroupName != ""
}
func (*Artifact) BuilderId() string {
return BuilderId
}
@ -103,11 +121,17 @@ func (a *Artifact) String() string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("%s:\n\n", a.BuilderId()))
buf.WriteString(fmt.Sprintf("StorageAccountLocation: %s\n", a.StorageAccountLocation))
buf.WriteString(fmt.Sprintf("OSDiskUri: %s\n", a.OSDiskUri))
buf.WriteString(fmt.Sprintf("OSDiskUriReadOnlySas: %s\n", a.OSDiskUriReadOnlySas))
buf.WriteString(fmt.Sprintf("TemplateUri: %s\n", a.TemplateUri))
buf.WriteString(fmt.Sprintf("TemplateUriReadOnlySas: %s\n", a.TemplateUriReadOnlySas))
if a.isMangedImage() {
buf.WriteString(fmt.Sprintf("ManagedImageResourceGroupName: %s\n", a.ManagedImageResourceGroupName))
buf.WriteString(fmt.Sprintf("ManagedImageName: %s\n", a.ManagedImageName))
buf.WriteString(fmt.Sprintf("ManagedImageLocation: %s\n", a.ManagedImageLocation))
} else {
buf.WriteString(fmt.Sprintf("StorageAccountLocation: %s\n", a.StorageAccountLocation))
buf.WriteString(fmt.Sprintf("OSDiskUri: %s\n", a.OSDiskUri))
buf.WriteString(fmt.Sprintf("OSDiskUriReadOnlySas: %s\n", a.OSDiskUriReadOnlySas))
buf.WriteString(fmt.Sprintf("TemplateUri: %s\n", a.TemplateUri))
buf.WriteString(fmt.Sprintf("TemplateUriReadOnlySas: %s\n", a.TemplateUriReadOnlySas))
}
return buf.String()
}

View File

@ -180,23 +180,27 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string
azureClient.VaultClient.ResponseInspector = byInspecting(maxlen)
azureClient.VaultClient.UserAgent += packerUserAgent
accountKeys, err := azureClient.AccountsClient.ListKeys(resourceGroupName, storageAccountName)
if err != nil {
return nil, err
// If this is a managed disk build, this should be ignored.
if resourceGroupName != "" && storageAccountName != "" {
accountKeys, err := azureClient.AccountsClient.ListKeys(resourceGroupName, storageAccountName)
if err != nil {
return nil, err
}
storageClient, err := storage.NewClient(
storageAccountName,
*(*accountKeys.Keys)[0].Value,
cloud.StorageEndpointSuffix,
storage.DefaultAPIVersion,
true /*useHttps*/)
if err != nil {
return nil, err
}
azureClient.BlobStorageClient = storageClient.GetBlobService()
}
storageClient, err := storage.NewClient(
storageAccountName,
*(*accountKeys.Keys)[0].Value,
cloud.StorageEndpointSuffix,
storage.DefaultAPIVersion,
true /*useHttps*/)
if err != nil {
return nil, err
}
azureClient.BlobStorageClient = storageClient.GetBlobService()
return azureClient, nil
}

View File

@ -104,14 +104,16 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
}
account, err := b.getBlobAccount(azureClient, b.config.ResourceGroupName, b.config.StorageAccount)
if err != nil {
return nil, err
}
b.config.storageAccountBlobEndpoint = *account.AccountProperties.PrimaryEndpoints.Blob
if b.config.StorageAccount != "" {
account, err := b.getBlobAccount(azureClient, b.config.ResourceGroupName, b.config.StorageAccount)
if err != nil {
return nil, err
}
b.config.storageAccountBlobEndpoint = *account.AccountProperties.PrimaryEndpoints.Blob
if !b.config.isManagedImage() && equalLocation(*account.Location, b.config.Location) == false {
return nil, fmt.Errorf("The storage account is located in %s, but the build will take place in %s. The locations must be identical", *account.Location, b.config.Location)
if !equalLocation(*account.Location, b.config.Location) {
return nil, fmt.Errorf("The storage account is located in %s, but the build will take place in %s. The locations must be identical", *account.Location, b.config.Location)
}
}
endpointConnectType := PublicEndpoint
@ -197,7 +199,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, errors.New("Build was halted.")
}
if template, ok := b.stateBag.GetOk(constants.ArmCaptureTemplate); ok {
if b.config.isManagedImage() {
return NewManagedImageArtifact(b.config.ManagedImageResourceGroupName, b.config.ManagedImageName, b.config.manageImageLocation)
} else if template, ok := b.stateBag.GetOk(constants.ArmCaptureTemplate); ok {
return NewArtifact(
template.(*CaptureTemplate),
func(name string) string {

View File

@ -470,39 +470,61 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) {
/////////////////////////////////////////////
// Capture
if c.CaptureContainerName == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must be specified"))
if c.CaptureContainerName == "" && c.ManagedImageName == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name or managed_image_name must be specified"))
}
if !reCaptureContainerName.MatchString(c.CaptureContainerName) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must satisfy the regular expression %q.", reCaptureContainerName.String()))
if c.CaptureNamePrefix == "" && c.ManagedImageResourceGroupName == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix or managed_image_resource_group_name must be specified"))
}
if strings.HasSuffix(c.CaptureContainerName, "-") {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must not end with a hyphen, e.g. '-'."))
if (c.CaptureNamePrefix != "" || c.CaptureContainerName != "") && (c.ManagedImageResourceGroupName != "" || c.ManagedImageName != "") {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Either a VHD or a managed image can be built, but not both. Please specify either capture_container_name and capture_name_prefix or managed_image_resource_group_name and managed_image_name."))
}
if strings.Contains(c.CaptureContainerName, "--") {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must not contain consecutive hyphens, e.g. '--'."))
}
if c.CaptureContainerName != "" {
if !reCaptureContainerName.MatchString(c.CaptureContainerName) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must satisfy the regular expression %q.", reCaptureContainerName.String()))
}
if c.CaptureNamePrefix == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must be specified"))
}
if strings.HasSuffix(c.CaptureContainerName, "-") {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must not end with a hyphen, e.g. '-'."))
}
if !reCaptureNamePrefix.MatchString(c.CaptureNamePrefix) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must satisfy the regular expression %q.", reCaptureNamePrefix.String()))
}
if strings.Contains(c.CaptureContainerName, "--") {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_container_name must not contain consecutive hyphens, e.g. '--'."))
}
if strings.HasSuffix(c.CaptureNamePrefix, "-") || strings.HasSuffix(c.CaptureNamePrefix, ".") {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must not end with a hyphen or period."))
if c.CaptureNamePrefix == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must be specified"))
}
if !reCaptureNamePrefix.MatchString(c.CaptureNamePrefix) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must satisfy the regular expression %q.", reCaptureNamePrefix.String()))
}
if strings.HasSuffix(c.CaptureNamePrefix, "-") || strings.HasSuffix(c.CaptureNamePrefix, ".") {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A capture_name_prefix must not end with a hyphen or period."))
}
}
/////////////////////////////////////////////
// Compute
if c.ImageUrl != "" &&
(c.CustomManagedImageName != "" || c.CustomManagedImageResourceGroupName != "") &&
(c.ImagePublisher != "" || c.ImageOffer != "" || c.ImageSku != "") {
toInt := func(b bool) int {
if b {
return 1
} else {
return 0
}
}
isImageUrl := c.ImageUrl != ""
isCustomManagedImage := c.CustomManagedImageName != "" || c.CustomManagedImageResourceGroupName != ""
isPlatformImage := c.ImagePublisher != "" || c.ImageOffer != "" || c.ImageSku != ""
countSourceInputs := toInt(isImageUrl) + toInt(isCustomManagedImage) + toInt(isPlatformImage)
if countSourceInputs > 1 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Specify either a VHD (image_url), Image Reference (image_publisher, image_offer, image_sku) or a Managed Disk (custom_managed_disk_image_name, custom_managed_disk_resource_group_name"))
}
@ -541,12 +563,23 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) {
/////////////////////////////////////////////
// Deployment
if c.StorageAccount == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A storage_account must be specified"))
xor := func(a, b bool) bool {
return (a || b) && !(a && b)
}
if c.ResourceGroupName == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A resource_group_name must be specified"))
if !xor((c.StorageAccount != "" || c.ResourceGroupName != ""), (c.ManagedImageName != "" || c.ManagedImageResourceGroupName != "")) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Specify either a VHD (storage_account and resource_group_name) or Managed Image (managed_image_resource_group_name and managed_image_name) output"))
}
if c.ManagedImageName == "" && c.ManagedImageResourceGroupName == "" {
if c.StorageAccount == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A storage_account must be specified"))
}
if c.ResourceGroupName == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A resource_group_name must be specified"))
}
}
if c.VirtualNetworkName == "" && c.VirtualNetworkResourceGroupName != "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("If virtual_network_resource_group_name is specified, so must virtual_network_name"))
}

View File

@ -737,6 +737,95 @@ func TestConfigShouldRejectMissingCustomDataFile(t *testing.T) {
}
}
func TestConfigShouldAcceptPlatformManagedImageBuild(t *testing.T) {
config := map[string]interface{}{
"image_offer": "ignore",
"image_publisher": "ignore",
"image_sku": "ignore",
"location": "ignore",
"subscription_id": "ignore",
"communicator": "none",
"managed_image_resource_group_name": "ignore",
"managed_image_name": "ignore",
// Does not matter for this test case, just pick one.
"os_type": constants.Target_Linux,
}
_, _, err := newConfig(config, getPackerConfiguration())
if err != nil {
t.Fatal("expected config to accept platform managed image build")
}
}
// If the user specified a build for a VHD and a Managed Image it should be rejected.
func TestConfigShouldRejectVhdAndManagedImageOutput(t *testing.T) {
config := map[string]interface{}{
"image_offer": "ignore",
"image_publisher": "ignore",
"image_sku": "ignore",
"location": "ignore",
"subscription_id": "ignore",
"communicator": "none",
"capture_container_name": "ignore",
"capture_name_prefix": "ignore",
"managed_image_resource_group_name": "ignore",
"managed_image_name": "ignore",
// Does not matter for this test case, just pick one.
"os_type": constants.Target_Linux,
}
_, _, err := newConfig(config, getPackerConfiguration())
if err == nil {
t.Fatal("expected config to reject VHD and Managed Image build")
}
}
func TestConfigShouldRejectCustomAndPlatformManagedImageBuild(t *testing.T) {
config := map[string]interface{}{
"custom_managed_image_resource_group_name": "ignore",
"custom_managed_image_name": "ignore",
"image_offer": "ignore",
"image_publisher": "ignore",
"image_sku": "ignore",
"location": "ignore",
"subscription_id": "ignore",
"communicator": "none",
"managed_image_resource_group_name": "ignore",
"managed_image_name": "ignore",
// Does not matter for this test case, just pick one.
"os_type": constants.Target_Linux,
}
_, _, err := newConfig(config, getPackerConfiguration())
if err == nil {
t.Fatal("expected config to reject custom and platform input for a managed image build")
}
}
func TestConfigShouldRejectCustomAndImageUrlForManagedImageBuild(t *testing.T) {
config := map[string]interface{}{
"image_url": "ignore",
"custom_managed_image_resource_group_name": "ignore",
"custom_managed_image_name": "ignore",
"location": "ignore",
"subscription_id": "ignore",
"communicator": "none",
"managed_image_resource_group_name": "ignore",
"managed_image_name": "ignore",
// Does not matter for this test case, just pick one.
"os_type": constants.Target_Linux,
}
_, _, err := newConfig(config, getPackerConfiguration())
if err == nil {
t.Fatal("expected config to reject custom and platform input for a managed image build")
}
}
func getArmBuilderConfiguration() map[string]string {
m := make(map[string]string)
for _, v := range requiredConfigValues {

View File

@ -261,11 +261,7 @@ growpart:
// Ensure the VM template is correct when building from a custom managed image.
func TestVirtualMachineDeployment08(t *testing.T) {
config := map[string]interface{}{
"capture_name_prefix": "ignore",
"capture_container_name": "ignore",
"location": "ignore",
"resource_group_name": "ignore",
"storage_account": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Linux,
"communicator": "none",
@ -294,11 +290,7 @@ func TestVirtualMachineDeployment08(t *testing.T) {
// Ensure the VM template is correct when building from a platform managed image.
func TestVirtualMachineDeployment09(t *testing.T) {
config := map[string]interface{}{
"capture_name_prefix": "ignore",
"capture_container_name": "ignore",
"location": "ignore",
"resource_group_name": "ignore",
"storage_account": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Linux,
"communicator": "none",

View File

@ -28,12 +28,12 @@
"os_type": "Linux",
"image_publisher": "OpenLogic",
"image_offer": "CentOS",
"image_sku": "7.2",
"image_sku": "7.3",
"image_version": "latest",
"ssh_pty": "true",
"location": "South Central US",
"vm_size": "Standard_A2"
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"execute_command": "echo '{{user `ssh_pass`}}' | {{ .Vars }} sudo -S -E sh '{{ .Path }}'",

View File

@ -30,7 +30,7 @@
"ssh_pty": "true",
"location": "South Central US",
"vm_size": "Standard_A2"
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"execute_command": "echo '{{user `ssh_pass`}}' | {{ .Vars }} sudo -S -E sh '{{ .Path }}'",

View File

@ -28,7 +28,7 @@
},
"location": "West US",
"vm_size": "Standard_A2"
"vm_size": "Standard_DS2_v2"
}
],
"provisioners": [{

View File

@ -2,8 +2,6 @@
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}"
},
"builders": [
@ -12,13 +10,8 @@
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"resource_group_name": "{{user `resource_group`}}",
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"os_type": "Linux",
"custom_managed_image_resource_group_name": "MyResourceGroup",
"custom_managed_image_name": "MyImage",
@ -31,7 +24,7 @@
},
"location": "West US",
"vm_size": "Standard_A2"
"vm_size": "Standard_DS2_v2"
}
],
"provisioners": [{

48
examples/azure/rhel.json Normal file
View File

@ -0,0 +1,48 @@
{
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}",
"tenant_id": "{{env `ARM_TENANT_ID`}}",
"ssh_user": "centos",
"ssh_pass": null
},
"builders": [{
"type": "azure-arm",
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"resource_group_name": "{{user `resource_group`}}",
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"tenant_id": "{{user `tenant_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"ssh_username": "{{user `ssh_user`}}",
"ssh_password": "{{user `ssh_pass`}}",
"os_type": "Linux",
"image_publisher": "RedHat",
"image_offer": "RHEL",
"image_sku": "7.3",
"image_version": "latest",
"ssh_pty": "true",
"location": "South Central US",
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"execute_command": "echo '{{user `ssh_pass`}}' | {{ .Vars }} sudo -S -E sh '{{ .Path }}'",
"inline": [
"yum update -y",
"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync"
],
"inline_shebang": "/bin/sh -x",
"type": "shell",
"skip_clean": true
}]
}

View File

@ -25,12 +25,12 @@
"os_type": "Linux",
"image_publisher": "SUSE",
"image_offer": "openSUSE",
"image_sku": "13.2",
"image_offer": "SLES",
"image_sku": "12-SP2",
"ssh_pty": "true",
"location": "South Central US",
"vm_size": "Standard_A2"
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"execute_command": "echo '{{user `ssh_pass`}}' | {{ .Vars }} sudo -S -E sh '{{ .Path }}'",

View File

@ -21,7 +21,7 @@
"os_type": "Linux",
"image_publisher": "Canonical",
"image_offer": "UbuntuServer",
"image_sku": "16.04.0-LTS",
"image_sku": "16.04-LTS",
"azure_tags": {
"dept": "engineering",
@ -29,7 +29,7 @@
},
"location": "West US",
"vm_size": "Standard_A2"
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",

View File

@ -2,8 +2,6 @@
"variables": {
"client_id": "{{env `ARM_CLIENT_ID`}}",
"client_secret": "{{env `ARM_CLIENT_SECRET`}}",
"resource_group": "{{env `ARM_RESOURCE_GROUP`}}",
"storage_account": "{{env `ARM_STORAGE_ACCOUNT`}}",
"subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}"
},
"builders": [{
@ -11,13 +9,8 @@
"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"resource_group_name": "{{user `resource_group`}}",
"storage_account": "{{user `storage_account`}}",
"subscription_id": "{{user `subscription_id`}}",
"capture_container_name": "images",
"capture_name_prefix": "packer",
"os_type": "Linux",
"image_publisher": "Canonical",
"image_offer": "UbuntuServer",
@ -32,7 +25,7 @@
},
"location": "West US",
"vm_size": "Standard_A2"
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",

View File

@ -20,7 +20,7 @@
"image_sku": "16.04-LTS",
"location": "West US",
"vm_size": "Standard_DS1_v2"
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"execute_command": "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'",

View File

@ -32,7 +32,7 @@
"winrm_username": "packer",
"location": "West US",
"vm_size": "Standard_A2"
"vm_size": "Standard_DS2_v2"
}],
"provisioners": [{
"type": "powershell",

View File

@ -28,7 +28,7 @@
},
"location": "West US",
"vm_size": "Standard_A2"
"vm_size": "Standard_DS2_v2"
}
],
"provisioners": [{

View File

@ -29,15 +29,8 @@ builder.
- `client_secret` (string) The password or secret for your service principal.
- `resource_group_name` (string) Resource group under which the final artifact will be stored.
- `storage_account` (string) Storage account under which the final artifact will be stored.
- `subscription_id` (string) Subscription under which the build will be performed. **The service principal specified in `client_id` must have full access to this subscription.**
- `capture_container_name` (string) Destination container name. Essentially the "directory" where your VHD will be organized in Azure. The captured VHD's URL will be <https://><storage_account>.blob.core.windows.net/system/Microsoft.Compute/Images/<capture_container_name>/<capture_name_prefix>.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.vhd.
- `capture_name_prefix` (string) VHD prefix. The final artifacts will be named `PREFIX-osDisk.UUID` and `PREFIX-vmTemplate.UUID`.
- `capture_container_name` (string) Destination container name. Essentially the "directory" where your VHD will be organized in Azure. The captured VHD's URL will be https://<storage_account>.blob.core.windows.net/system/Microsoft.Compute/Images/<capture_container_name>/<capture_name_prefix>.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.vhd.
- `image_publisher` (string) PublisherName for your base image. See [documentation](https://azure.microsoft.com/en-us/documentation/articles/resource-groups-vm-searching/) for details.
@ -54,6 +47,32 @@ builder.
- `location` (string) Azure datacenter in which your VM will build.
CLI example `azure location list`
#### VHD or Managed Image
The Azure builder can create either a VHD, or a managed image. When creating a VHD the following two options are required.
- `capture_container_name` (string) Destination container name. Essentially the "directory" where your VHD will be
organized in Azure. The captured VHD's URL will be https://<storage_account>.blob.core.windows.net/system/Microsoft.Compute/Images/<capture_container_name>/<capture_name_prefix>.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.vhd.
- `capture_name_prefix` (string) VHD prefix. The final artifacts will be named `PREFIX-osDisk.UUID` and
`PREFIX-vmTemplate.UUID`.
- `resource_group_name` (string) Resource group under which the final artifact will be stored.
- `storage_account` (string) Storage account under which the final artifact will be stored.
When creating a managed image the following two options are required.
- `managed_image_name` (string) Specify the managed image name where the result of the Packer build will be saved. The
image name must not exist ahead of time, and will not be overwritten. If this value is set, the value
`managed_image_resource_group_name` must also be set. See [documentation](https://docs.microsoft.com/en-us/azure/storage/storage-managed-disks-overview#images)
to learn more about managed images.
- `managed_image_resource_group_name` (string) Specify the managed image resource group name where the result of the Packer build will be
saved. The resource group must already exist. If this value is set, the value `managed_image_name` must also be
set. See [documentation](https://docs.microsoft.com/en-us/azure/storage/storage-managed-disks-overview#images) to
learn more about managed images.
### Optional:
@ -89,16 +108,7 @@ builder.
- `image_url` (string) Specify a custom VHD to use. If this value is set, do not set image\_publisher, image\_offer,
image\_sku, or image\_version.
- `managed_image_name` (string) Specify the managed image name where the result of the Packer build will be saved. The
image name must not exist ahead of time, and will not be overwritten. If this value is set, the value
`managed_image_resource_group_name` must also be set. See [documentation](https://docs.microsoft.com/en-us/azure/storage/storage-managed-disks-overview#images)
to learn more about managed images.
- `managed_image_resource_group_name` (string) Specify the managed image resource group name where the result of the Packer build will be
saved. The resource group must already exist. If this value is set, the value `managed_image_name` must also be
set. See [documentation](https://docs.microsoft.com/en-us/azure/storage/storage-managed-disks-overview#images) to
learn more about managed images.
- `object_id` (string) Specify an OAuth Object ID to protect WinRM certificates
created at runtime. This variable is required when creating images based on
Windows; this variable is not used by non-Windows builds. See `Windows`