command: require resource to be in config before import
Previously we encouraged users to import a resource and _then_ write the configuration block for it. This ordering creates lots of risk, since for various reasons users can end up subsequently running Terraform without any configuration in place, which then causes Terraform to want to destroy the resource that was imported. Now we invert this and require a minimal configuration block be written first. This helps ensure that the user ends up with a correlated resource config and state, protecting against any inconsistency caused by typos. This addresses #11835.
This commit is contained in:
parent
7d8719150c
commit
9a398a7793
|
@ -78,14 +78,43 @@ func (c *ImportCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var conf *config.Config
|
// Verify that the given address points to something that exists in config.
|
||||||
if mod != nil {
|
// This is to reduce the risk that a typo in the resource address will
|
||||||
conf = mod.Config()
|
// import something that Terraform will want to immediately destroy on
|
||||||
|
// the next plan, and generally acts as a reassurance of user intent.
|
||||||
|
targetMod := mod.Child(addr.Path)
|
||||||
|
if targetMod == nil {
|
||||||
|
modulePath := addr.WholeModuleAddress().String()
|
||||||
|
if modulePath == "" {
|
||||||
|
c.Ui.Error(importCommandMissingConfigMsg)
|
||||||
|
} else {
|
||||||
|
c.Ui.Error(fmt.Sprintf(importCommandMissingModuleFmt, modulePath))
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
rcs := targetMod.Config().Resources
|
||||||
|
var rc *config.Resource
|
||||||
|
for _, thisRc := range rcs {
|
||||||
|
if addr.MatchesConfig(targetMod, thisRc) {
|
||||||
|
rc = thisRc
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rc == nil {
|
||||||
|
modulePath := addr.WholeModuleAddress().String()
|
||||||
|
if modulePath == "" {
|
||||||
|
modulePath = "the root module"
|
||||||
|
}
|
||||||
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
importCommandMissingResourceFmt,
|
||||||
|
addr, modulePath, addr.Type, addr.Name,
|
||||||
|
))
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the backend
|
// Load the backend
|
||||||
b, err := c.Backend(&BackendOpts{
|
b, err := c.Backend(&BackendOpts{
|
||||||
Config: conf,
|
Config: mod.Config(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
|
@ -138,13 +167,7 @@ func (c *ImportCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
c.Ui.Output(c.Colorize().Color("[reset][green]\n" + importCommandSuccessMsg))
|
||||||
"[reset][green]\n" +
|
|
||||||
"Import success! The resources imported are shown above. These are\n" +
|
|
||||||
"now in your Terraform state. Import does not currently generate\n" +
|
|
||||||
"configuration, so you must do this next. If you do not create configuration\n" +
|
|
||||||
"for the above resources, then the next `terraform plan` will mark\n" +
|
|
||||||
"them for destruction.")))
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -238,3 +261,34 @@ const importCommandResourceModeMsg = `Error: resource address must refer to a ma
|
||||||
|
|
||||||
Data resources cannot be imported.
|
Data resources cannot be imported.
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const importCommandMissingConfigMsg = `Error: no configuration files in this directory.
|
||||||
|
|
||||||
|
"terraform import" can only be run in a Terraform configuration directory.
|
||||||
|
Create one or more .tf files in this directory to import here.
|
||||||
|
`
|
||||||
|
|
||||||
|
const importCommandMissingModuleFmt = `Error: %s does not exist in the configuration.
|
||||||
|
|
||||||
|
Please add the configuration for the module before importing resources into it.
|
||||||
|
`
|
||||||
|
|
||||||
|
const importCommandMissingResourceFmt = `Error: resource address %q does not exist in the configuration.
|
||||||
|
|
||||||
|
Before importing this resource, please create its configuration in %s. For example:
|
||||||
|
|
||||||
|
resource %q %q {
|
||||||
|
# (resource arguments)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const importCommandSuccessMsg = `Import successful!
|
||||||
|
|
||||||
|
The resources that were imported are shown above. These resources are now in
|
||||||
|
your Terraform state and will henceforth be managed by Terraform.
|
||||||
|
|
||||||
|
Import does not generate configuration, so the next step is to ensure that
|
||||||
|
the resource configurations match the current (or desired) state of the
|
||||||
|
imported resources. You can use the output from "terraform plan" to verify that
|
||||||
|
the configuration is correct and complete.
|
||||||
|
`
|
||||||
|
|
|
@ -316,6 +316,66 @@ func TestImport_customProvider(t *testing.T) {
|
||||||
testStateOutput(t, statePath, testImportCustomProviderStr)
|
testStateOutput(t, statePath, testImportCustomProviderStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImport_missingResourceConfig(t *testing.T) {
|
||||||
|
defer testChdir(t, testFixturePath("import-missing-resource-config"))()
|
||||||
|
|
||||||
|
statePath := testTempFile(t)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &ImportCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state", statePath,
|
||||||
|
"test_instance.foo",
|
||||||
|
"bar",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 1 {
|
||||||
|
t.Fatalf("import succeeded; expected failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := ui.ErrorWriter.String()
|
||||||
|
if want := `resource address "test_instance.foo" does not exist`; !strings.Contains(msg, want) {
|
||||||
|
t.Errorf("incorrect message\nwant substring: %s\ngot:\n%s", want, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImport_missingModuleConfig(t *testing.T) {
|
||||||
|
defer testChdir(t, testFixturePath("import-missing-resource-config"))()
|
||||||
|
|
||||||
|
statePath := testTempFile(t)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &ImportCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state", statePath,
|
||||||
|
"module.baz.test_instance.foo",
|
||||||
|
"bar",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 1 {
|
||||||
|
t.Fatalf("import succeeded; expected failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := ui.ErrorWriter.String()
|
||||||
|
if want := `module.baz does not exist in the configuration`; !strings.Contains(msg, want) {
|
||||||
|
t.Errorf("incorrect message\nwant substring: %s\ngot:\n%s", want, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestImport_dataResource(t *testing.T) {
|
func TestImport_dataResource(t *testing.T) {
|
||||||
defer testChdir(t, testFixturePath("import-missing-resource-config"))()
|
defer testChdir(t, testFixturePath("import-missing-resource-config"))()
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,6 @@ provider "test" {
|
||||||
|
|
||||||
alias = "alias"
|
alias = "alias"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "test_instance" "foo" {
|
||||||
|
}
|
||||||
|
|
|
@ -3,3 +3,6 @@ variable "foo" {}
|
||||||
provider "test" {
|
provider "test" {
|
||||||
foo = "${var.foo}"
|
foo = "${var.foo}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "test_instance" "foo" {
|
||||||
|
}
|
||||||
|
|
|
@ -3,3 +3,6 @@ variable "foo" {}
|
||||||
provider "test" {
|
provider "test" {
|
||||||
foo = "${var.foo}"
|
foo = "${var.foo}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "test_instance" "foo" {
|
||||||
|
}
|
||||||
|
|
|
@ -3,3 +3,6 @@ variable "foo" {}
|
||||||
provider "test" {
|
provider "test" {
|
||||||
foo = "${var.foo}"
|
foo = "${var.foo}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "test_instance" "foo" {
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
provider "test" {
|
provider "test" {
|
||||||
foo = "bar"
|
foo = "bar"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "test_instance" "foo" {
|
||||||
|
}
|
||||||
|
|
|
@ -24,12 +24,10 @@ The current implementation of Terraform import can only import resources
|
||||||
into the [state](/docs/state). It does not generate configuration. A future
|
into the [state](/docs/state). It does not generate configuration. A future
|
||||||
version of Terraform will also generate configuration.
|
version of Terraform will also generate configuration.
|
||||||
|
|
||||||
Because of this, the behavior of importing resources into Terraform right now
|
Because of this, prior to running `terraform import` it is necessary to write
|
||||||
is that after an import, if you run a `terraform plan`, Terraform views it
|
manually a `resource` configuration block for the resource, to which the
|
||||||
as an orphan (a resource with no configuration) and marks it for destruction.
|
imported object will be attached.
|
||||||
After importing a resource you have to manually write configuration to match
|
|
||||||
the resource.
|
|
||||||
|
|
||||||
While this may seem tedious, it still gives Terraform users an avenue for
|
While this may seem tedious, it still gives Terraform users an avenue for
|
||||||
importing existing resources. A future version of Terraform will fully generate
|
importing existing resources. A future version of Terraform will fully generate
|
||||||
configuration significantly simplifying this process.
|
configuration, significantly simplifying this process.
|
||||||
|
|
|
@ -15,26 +15,39 @@ you can't yet point Terraform import to an entire collection of resources
|
||||||
such as an AWS VPC and import all of it. A future version of Terraform will
|
such as an AWS VPC and import all of it. A future version of Terraform will
|
||||||
be able to do this.
|
be able to do this.
|
||||||
|
|
||||||
Using `terraform import` is simple. An example is shown below:
|
To import a resource, first write a resource block for it in your
|
||||||
|
configuration, establishing the name by which it will be known in Terraform:
|
||||||
|
|
||||||
|
```
|
||||||
|
resource "aws_instance" "bar" {
|
||||||
|
# ...instance configuration...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If desired, you can leave the body of the resource block blank for now and
|
||||||
|
return to fill it in once the instance is imported.
|
||||||
|
|
||||||
|
Now `terraform import` can be run to attach an existing instance to this
|
||||||
|
resource configuration:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ terraform import aws_instance.bar i-abcd1234
|
$ terraform import aws_instance.bar i-abcd1234
|
||||||
```
|
```
|
||||||
|
|
||||||
The above command imports an AWS instance with the given ID to the
|
The above command imports an AWS instance with the given ID and attaches
|
||||||
address `aws_instance.bar`. You can also import resources into modules.
|
it to the name `aws_instance.bar`. You can also import resources into modules.
|
||||||
See the [resource addressing](/docs/internals/resource-addressing.html)
|
See the [resource addressing](/docs/internals/resource-addressing.html)
|
||||||
page for more details on the full range of addresses supported.
|
page for more details on the full range of addresses supported.
|
||||||
|
|
||||||
The ID given is dependent on the resource type being imported. For example,
|
The ID given is dependent on the resource type being imported. For example,
|
||||||
AWS instances use their direct IDs. However, AWS Route53 zones use the
|
AWS instances use their direct IDs. However, AWS Route53 zones use the
|
||||||
domain name itself. Reference the resource documentation for details on
|
domain name itself. Console the resource documentation for details on what
|
||||||
what the ID it expects is.
|
form of ID each resource expects.
|
||||||
|
|
||||||
As a result of the above command, the resource is put into the state file.
|
As a result of the above command, the resource is recorded in the state file.
|
||||||
If you run `terraform plan`, you should see Terraform plan your resource
|
You can now run `terraform plan` to see how the configuration compares to
|
||||||
for destruction. You now have to create a matching configuration so that
|
the imported resource, and make any adjustments to the configuration to
|
||||||
Terraform doesn't plan a destroy.
|
align with the current (or desired) state of the imported object.
|
||||||
|
|
||||||
## Complex Imports
|
## Complex Imports
|
||||||
|
|
||||||
|
@ -43,7 +56,10 @@ into the state file. An import may also result in a "complex import" where
|
||||||
multiple resources are imported. For example, an AWS security group imports
|
multiple resources are imported. For example, an AWS security group imports
|
||||||
an `aws_security_group` but also one `aws_security_group_rule` for each rule.
|
an `aws_security_group` but also one `aws_security_group_rule` for each rule.
|
||||||
|
|
||||||
In this case, the name of the resource is shown as part of the import output.
|
In this scenario, the secondary resources will not already exist in
|
||||||
You'll have to create a configuration for each resource imported. If you want
|
configuration, so it is necessary to consult the import output and create
|
||||||
to rename or otherwise modify the imported resources, the
|
a `resource` block in configuration for each secondary resource. If this is
|
||||||
[state management commands](/docs/commands/state/index.html) should be used.
|
not done, Terraform will plan to destroy the imported objects on the next run.
|
||||||
|
|
||||||
|
If you want to rename or otherwise modify the imported resources, the
|
||||||
|
[state management commands](/docs/commands/state/index.html) can be used.
|
||||||
|
|
Loading…
Reference in New Issue