-
Notifications
You must be signed in to change notification settings - Fork 9.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support VPC configuration of aws_elasticsearch_domain resources. #1958
Changes from 2 commits
1a2d978
f57cd53
54d14e1
4b3dd70
737d763
d4945cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -137,6 +137,38 @@ func resourceAwsElasticSearchDomain() *schema.Resource { | |
}, | ||
}, | ||
}, | ||
"vpc_options": { | ||
Type: schema.TypeList, | ||
Optional: true, | ||
Elem: &schema.Resource{ | ||
Schema: map[string]*schema.Schema{ | ||
"security_group_ids": { | ||
Type: schema.TypeSet, | ||
Optional: true, | ||
Elem: &schema.Schema{Type: schema.TypeString}, | ||
Set: schema.HashString, | ||
}, | ||
"subnet_ids": { | ||
Type: schema.TypeSet, | ||
Optional: true, | ||
Elem: &schema.Schema{Type: schema.TypeString}, | ||
Set: schema.HashString, | ||
}, | ||
}, | ||
}, | ||
}, | ||
"vpc_availability_zones": { | ||
Type: schema.TypeSet, | ||
Optional: true, | ||
Computed: true, | ||
Elem: &schema.Schema{Type: schema.TypeString}, | ||
Set: schema.HashString, | ||
}, | ||
"vpc_id": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Computed: true, | ||
}, | ||
"elasticsearch_version": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
|
@@ -230,6 +262,21 @@ func resourceAwsElasticSearchDomainCreate(d *schema.ResourceData, meta interface | |
} | ||
} | ||
|
||
if v, ok := d.GetOk("vpc_options"); ok { | ||
options := v.([]interface{}) | ||
|
||
if len(options) > 1 { | ||
return fmt.Errorf("Only a single vpc_options block is expected") | ||
} else if len(options) == 1 { | ||
if options[0] == nil { | ||
return fmt.Errorf("At least one field is expected inside vpc_options") | ||
} | ||
|
||
s := options[0].(map[string]interface{}) | ||
input.VPCOptions = expandESVPCOptions(s) | ||
} | ||
} | ||
|
||
log.Printf("[DEBUG] Creating ElasticSearch domain: %s", input) | ||
|
||
// IAM Roles can take some time to propagate if set in AccessPolicies and created in the same terraform | ||
|
@@ -289,7 +336,7 @@ func waitForElasticSearchDomainCreation(conn *elasticsearch.ElasticsearchService | |
return resource.NonRetryableError(err) | ||
} | ||
|
||
if !*out.DomainStatus.Processing && out.DomainStatus.Endpoint != nil { | ||
if !*out.DomainStatus.Processing && (out.DomainStatus.Endpoint != nil || out.DomainStatus.Endpoints != nil) { | ||
return nil | ||
} | ||
|
||
|
@@ -332,9 +379,6 @@ func resourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface{} | |
d.Set("domain_id", ds.DomainId) | ||
d.Set("domain_name", ds.DomainName) | ||
d.Set("elasticsearch_version", ds.ElasticsearchVersion) | ||
if ds.Endpoint != nil { | ||
d.Set("endpoint", *ds.Endpoint) | ||
} | ||
|
||
err = d.Set("ebs_options", flattenESEBSOptions(ds.EBSOptions)) | ||
if err != nil { | ||
|
@@ -349,6 +393,35 @@ func resourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface{} | |
"automated_snapshot_start_hour": *ds.SnapshotOptions.AutomatedSnapshotStartHour, | ||
}) | ||
} | ||
if ds.VPCOptions != nil { | ||
err = d.Set("vpc_options", flattenESVPCDerivedInfo(ds.VPCOptions)) | ||
if err != nil { | ||
return err | ||
} | ||
err = d.Set("vpc_availability_zones", schema.NewSet(schema.HashString, flattenStringList(ds.VPCOptions.AvailabilityZones))) | ||
if err != nil { | ||
return err | ||
} | ||
err = d.Set("vpc_id", *ds.VPCOptions.VPCId) | ||
if err != nil { | ||
return err | ||
} | ||
endpoints := pointersMapToStringList(ds.Endpoints) | ||
err = d.Set("endpoint", endpoints["vpc"]) | ||
if err != nil { | ||
return err | ||
} | ||
if ds.Endpoint != nil { | ||
return fmt.Errorf("%q: Elasticsearch domain in VPC expected to have null Endpoint value", d.Id()) | ||
} | ||
} else { | ||
if ds.Endpoint != nil { | ||
d.Set("endpoint", *ds.Endpoint) | ||
} | ||
if ds.Endpoints != nil { | ||
return fmt.Errorf("%q: Elasticsearch domain not in VPC expected to have null Endpoints value", d.Id()) | ||
} | ||
} | ||
|
||
d.Set("arn", ds.ARN) | ||
|
||
|
@@ -431,6 +504,17 @@ func resourceAwsElasticSearchDomainUpdate(d *schema.ResourceData, meta interface | |
} | ||
} | ||
|
||
if d.HasChange("vpc_options") { | ||
options := d.Get("vpc_options").([]interface{}) | ||
|
||
if len(options) > 1 { | ||
return fmt.Errorf("Only a single vpc_options block is expected") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can do the same validation just by adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. I was cribbing/copying/keeping style with the other optional config sections in this provider, I didn't know that |
||
} else if len(options) == 1 { | ||
s := options[0].(map[string]interface{}) | ||
input.VPCOptions = expandESVPCOptions(s) | ||
} | ||
} | ||
|
||
_, err := conn.UpdateElasticsearchDomainConfig(&input) | ||
if err != nil { | ||
return err | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -145,6 +145,25 @@ func TestAccAWSElasticSearchDomain_complex(t *testing.T) { | |
}) | ||
} | ||
|
||
func TestAccAWSElasticSearchDomain_vpc(t *testing.T) { | ||
var domain elasticsearch.ElasticsearchDomainStatus | ||
ri := acctest.RandInt() | ||
|
||
resource.Test(t, resource.TestCase{ | ||
PreCheck: func() { testAccPreCheck(t) }, | ||
Providers: testAccProviders, | ||
CheckDestroy: testAccCheckESDomainDestroy, | ||
Steps: []resource.TestStep{ | ||
{ | ||
Config: testAccESDomainConfig_vpc(ri), | ||
Check: resource.ComposeTestCheckFunc( | ||
testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), | ||
), | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add another step, where we would update the VPC configuration, so that we ensure the update works as expected? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Ninir Yeah, I can do that, once I sort the larger IAM problem for the test (update re: that coming next). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disregard "larger IAM problem", I figured it out. 🎉 |
||
}, | ||
}) | ||
} | ||
|
||
func TestAccAWSElasticSearchDomain_policy(t *testing.T) { | ||
var domain elasticsearch.ElasticsearchDomainStatus | ||
|
||
|
@@ -448,3 +467,48 @@ resource "aws_elasticsearch_domain" "example" { | |
} | ||
`, randInt) | ||
} | ||
|
||
func testAccESDomainConfig_vpc(randInt int) string { | ||
return fmt.Sprintf(` | ||
data "aws_availability_zones" "available" { | ||
state = "available" | ||
} | ||
|
||
resource "aws_default_vpc" "default" {} | ||
|
||
resource "aws_default_subnet" "first" { | ||
availability_zone = "${data.aws_availability_zones.available.names[0]}" | ||
} | ||
|
||
resource "aws_default_subnet" "second" { | ||
availability_zone = "${data.aws_availability_zones.available.names[1]}" | ||
} | ||
|
||
resource "aws_security_group" "first" { | ||
vpc_id = "${aws_default_vpc.default.id}" | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To avoid sharing any resources and allow running multiple tests in parallel do you mind building custom VPC & subnets here, instead of creating default ones? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done & re-running this test as I write this. It hasn't completed yet, but it brought up the VPC and subnets and SGs, and the ES domain is creating, so I have very high confidence the tear-down will work. :-) |
||
|
||
resource "aws_security_group" "second" { | ||
vpc_id = "${aws_default_vpc.default.id}" | ||
} | ||
|
||
resource "aws_elasticsearch_domain" "example" { | ||
domain_name = "tf-test-%d" | ||
|
||
ebs_options { | ||
ebs_enabled = false | ||
} | ||
|
||
cluster_config { | ||
instance_count = 2 | ||
zone_awareness_enabled = true | ||
instance_type = "r3.large.elasticsearch" | ||
} | ||
|
||
vpc_options { | ||
security_group_ids = ["${aws_security_group.first.id}", "${aws_security_group.second.id}"] | ||
subnet_ids = ["${aws_default_subnet.first.id}", "${aws_default_subnet.second.id}"] | ||
} | ||
} | ||
`, randInt) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any particular reason for keeping those two fields outside of
vpc_options
and drift away from the API?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ES-in-VPC API is kind of weird compared to the other AWS APIs, from what I've seen. You don't actually pass in the
vpc_id
oravailability_zones
as a parameter to the API, you pass in a list of subnets that you want the ES domain to have endpoints in, and AWS enforces that all of those subnets are in the same VPC, and calculatesvpc_id
andavailability_zones
from the subnets, and returns them to you in a slightly different datatype.Structure
VPCOptions
hasSubnetIds
andSecurityGroupIds
, and you use that for create and update requests for the domain.Structure
VPCDerivedInfo
hasVPCId
,SubnetIds
,AvailabilityZones
, andSecurityGroupIds
, and you get that back as part of aElasticsearchDomainStatus
struct during create or delete requests.And then there's a structure
VPCDerivedInfoStatus
, which contains both aVPCDerivedInfo
and anOptionStatus
structure (unrelated to VPC), which you get back as part ofElasticsearchDomainConfig
if you describe the domain.And then to make your life even more interesting, the API uses the name
VPCOptions
to refer to all three of the structures, without even trying to do anything Go-idiomatic like defining an interface that just has the two common fields (which would work forVPCOptions
andVPCDerivedInfo
, but not forVPCDerivedInfoStatus
, becauseVPCDerivedInfo
is a named member of it).So, the whole thing is kind of a mess, but after puzzling all of that out, I figured the least complicated thing was, for the Terraform code, to keep
vpc_options
equivalent toVPCOptions
.But the end of it is: You can't specify the VPC id or the availability zones via the API, you have to let AWS compute them for you and return them, so they're never options that an operator will specify. And, once they are returned, I wanted to export them as top-level attributes of the resource, as I see other resources do, and I don't see a way to export schema sub-members as top-level attributes, other than defining them twice and exporting them twice?
Feedback encouraged if I am missing a more Terraform-idiomatic way to implement this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SubnetIds are globally unique. This behavior of submitting just subnets is seen elsewhere (ie ALB) and should be familiar to anyone used to using VPC.
Filtering by VPC is often just client-side to narrow down subnet list.
There are exceptions of course, for resources such as security groups that go into a VPC but not into a subnet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@handlerbot understood, but I don't think that gives us a reason to drift away from the API. They can both live nested under
vpc_options
if I understand correctly?Additionally we should remove
Optional
from both mentioned fields as the user cannot specify them in the config.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow - the user can access nested attributes of any
TypeList
like this:the
0th
index may feel a bit weird, but that's how most other resources do it, so I'd just stick with it. It's something we plan to address in core/HCL eventually.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only found one example of the docs specifying a usage like that, and only in passing (
cache_nodes
in https://www.terraform.io/docs/providers/aws/r/elasticache_cluster.html). I am not personally convinced, but you folks are the bosses, so. :-)