From 69a87dc3da9f1fa0d86d4ab6e8b79d7ba8f77811 Mon Sep 17 00:00:00 2001 From: Tom Elliff Date: Mon, 11 Dec 2017 18:34:28 +0000 Subject: [PATCH 1/6] Add encrypt at rest to ES domains For now just adds encrypt at rest when creating an ES domain, doesn't yet handle reading encrypt at rest options so will likely cause Terraform to rebuild the domain on the next operation. The AWS console handily creates a service KMS key for ES when you are creating an encrypted ES domain via the console. This resource doesn't currently do that but that functionality could be added. --- aws/resource_aws_elasticsearch_domain.go | 33 +++++++++++++ aws/resource_aws_elasticsearch_domain_test.go | 49 +++++++++++++++++++ aws/structure.go | 13 +++++ 3 files changed, 95 insertions(+) diff --git a/aws/resource_aws_elasticsearch_domain.go b/aws/resource_aws_elasticsearch_domain.go index 8e5ab7e39a2..d0c0e6b9d46 100644 --- a/aws/resource_aws_elasticsearch_domain.go +++ b/aws/resource_aws_elasticsearch_domain.go @@ -89,6 +89,24 @@ func resourceAwsElasticSearchDomain() *schema.Resource { }, }, }, + "encrypt_at_rest": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Required: true, + ForceNew: true, + }, + "kms_key_id": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, "cluster_config": { Type: schema.TypeList, Optional: true, @@ -291,6 +309,21 @@ func resourceAwsElasticSearchDomainCreate(d *schema.ResourceData, meta interface } } + if v, ok := d.GetOk("encrypt_at_rest"); ok { + options := v.([]interface{}) + + if len(options) > 1 { + return fmt.Errorf("Only a single encrypt_at_rest block is expected") + } else if len(options) == 1 { + if options[0] == nil { + return fmt.Errorf("At least one field is expected inside encrypt_at_rest") + } + + s := options[0].(map[string]interface{}) + input.EncryptionAtRestOptions = expandESEncryptAtRestOptions(s) + } + } + if v, ok := d.GetOk("cluster_config"); ok { config := v.([]interface{}) diff --git a/aws/resource_aws_elasticsearch_domain_test.go b/aws/resource_aws_elasticsearch_domain_test.go index 5640fffb8cd..15a7034baed 100644 --- a/aws/resource_aws_elasticsearch_domain_test.go +++ b/aws/resource_aws_elasticsearch_domain_test.go @@ -261,6 +261,24 @@ func TestAccAWSElasticSearchDomain_policy(t *testing.T) { }) } +func TestAccAWSElasticSearchDomain_encrypt_at_rest(t *testing.T) { + var domain elasticsearch.ElasticsearchDomainStatus + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckESDomainDestroy, + Steps: []resource.TestStep{ + { + Config: testAccESDomainConfigWithEncryptAtRest(acctest.RandInt()), + Check: resource.ComposeTestCheckFunc( + testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), + ), + }, + }, + }) +} + func TestAccAWSElasticSearchDomain_tags(t *testing.T) { var domain elasticsearch.ElasticsearchDomainStatus var td elasticsearch.ListTagsOutput @@ -411,6 +429,7 @@ func testAccESDomainConfig(randInt int) string { return fmt.Sprintf(` resource "aws_elasticsearch_domain" "example" { domain_name = "tf-test-%d" + ebs_options { ebs_enabled = true volume_size = 10 @@ -504,6 +523,36 @@ data "aws_iam_policy_document" "instance-assume-role-policy" { `, randESId, randRoleId) } +func testAccESDomainConfigWithEncryptAtRest(randESId int) string { + return fmt.Sprintf(` +resource "aws_kms_key" "es" { + description = "kms-key-for-tf-test-%d" + deletion_window_in_days = 7 +} + +resource "aws_elasticsearch_domain" "example" { + domain_name = "tf-test-%d" + + elasticsearch_version = "6.0" + + # Encrypt at rest requires m4/c4/r4/i2 instances. See http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html + cluster_config { + instance_type = "m4.large.elasticsearch" + } + + ebs_options { + ebs_enabled = true + volume_size = 10 + } + + encrypt_at_rest { + enabled = true + kms_key_id = "${aws_kms_key.es.key_id}" + } +} +`, randESId, randESId) +} + func testAccESDomainConfig_complex(randInt int) string { return fmt.Sprintf(` resource "aws_elasticsearch_domain" "example" { diff --git a/aws/structure.go b/aws/structure.go index e512f194f05..fc7cee31498 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -1045,6 +1045,19 @@ func expandESEBSOptions(m map[string]interface{}) *elasticsearch.EBSOptions { return &options } +func expandESEncryptAtRestOptions(m map[string]interface{}) *elasticsearch.EncryptionAtRestOptions { + options := elasticsearch.EncryptionAtRestOptions{} + + if v, ok := m["enabled"]; ok { + options.Enabled = aws.Bool(v.(bool)) + } + if v, ok := m["kms_key_id"]; ok && v.(string) != "" { + options.KmsKeyId = aws.String(v.(string)) + } + + return &options +} + func flattenESVPCDerivedInfo(o *elasticsearch.VPCDerivedInfo) []map[string]interface{} { m := map[string]interface{}{} From 793f0f1bfb50dd928ebdce9fcf811ff66e3f5769 Mon Sep 17 00:00:00 2001 From: Tom Elliff Date: Tue, 12 Dec 2017 11:36:00 +0000 Subject: [PATCH 2/6] Allow encryption of ES domain with service KMS key If you don't specify a KMS key then AWS will use the account's service KMS key (which is created for you automatically if you don't already have it). --- aws/resource_aws_elasticsearch_domain.go | 2 +- aws/resource_aws_elasticsearch_domain_test.go | 61 ++++++++++++++++++- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/aws/resource_aws_elasticsearch_domain.go b/aws/resource_aws_elasticsearch_domain.go index d0c0e6b9d46..c48d3c9c608 100644 --- a/aws/resource_aws_elasticsearch_domain.go +++ b/aws/resource_aws_elasticsearch_domain.go @@ -102,7 +102,7 @@ func resourceAwsElasticSearchDomain() *schema.Resource { }, "kms_key_id": { Type: schema.TypeString, - Required: true, + Optional: true, }, }, }, diff --git a/aws/resource_aws_elasticsearch_domain_test.go b/aws/resource_aws_elasticsearch_domain_test.go index 15a7034baed..284239f2b44 100644 --- a/aws/resource_aws_elasticsearch_domain_test.go +++ b/aws/resource_aws_elasticsearch_domain_test.go @@ -261,7 +261,7 @@ func TestAccAWSElasticSearchDomain_policy(t *testing.T) { }) } -func TestAccAWSElasticSearchDomain_encrypt_at_rest(t *testing.T) { +func TestAccAWSElasticSearchDomain_encrypt_at_rest_default_key(t *testing.T) { var domain elasticsearch.ElasticsearchDomainStatus resource.Test(t, resource.TestCase{ @@ -270,9 +270,29 @@ func TestAccAWSElasticSearchDomain_encrypt_at_rest(t *testing.T) { CheckDestroy: testAccCheckESDomainDestroy, Steps: []resource.TestStep{ { - Config: testAccESDomainConfigWithEncryptAtRest(acctest.RandInt()), + Config: testAccESDomainConfigWithEncryptAtRestDefaultKey(acctest.RandInt()), Check: resource.ComposeTestCheckFunc( testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), + testAccCheckESEncrypted(true, &domain), + ), + }, + }, + }) +} + +func TestAccAWSElasticSearchDomain_encrypt_at_rest_specify_key(t *testing.T) { + var domain elasticsearch.ElasticsearchDomainStatus + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckESDomainDestroy, + Steps: []resource.TestStep{ + { + Config: testAccESDomainConfigWithEncryptAtRestWithKey(acctest.RandInt()), + Check: resource.ComposeTestCheckFunc( + testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), + testAccCheckESEncrypted(true, &domain), ), }, }, @@ -357,6 +377,16 @@ func testAccCheckESNumberOfInstances(numberOfInstances int, status *elasticsearc } } +func testAccCheckESEncrypted(encrypted bool, status *elasticsearch.ElasticsearchDomainStatus) resource.TestCheckFunc { + return func(s *terraform.State) error { + conf := status.EncryptionAtRestOptions + if *conf.Enabled != encrypted { + return fmt.Errorf("Encrypt at rest not set properly. Given: %t, Expected: %t", *conf.Enabled, encrypted) + } + return nil + } +} + func testAccLoadESTags(conf *elasticsearch.ElasticsearchDomainStatus, td *elasticsearch.ListTagsOutput) resource.TestCheckFunc { return func(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).esconn @@ -523,7 +553,32 @@ data "aws_iam_policy_document" "instance-assume-role-policy" { `, randESId, randRoleId) } -func testAccESDomainConfigWithEncryptAtRest(randESId int) string { +func testAccESDomainConfigWithEncryptAtRestDefaultKey(randESId int) string { + return fmt.Sprintf(` + +resource "aws_elasticsearch_domain" "example" { + domain_name = "tf-test-%d" + + elasticsearch_version = "6.0" + + # Encrypt at rest requires m4/c4/r4/i2 instances. See http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html + cluster_config { + instance_type = "m4.large.elasticsearch" + } + + ebs_options { + ebs_enabled = true + volume_size = 10 + } + + encrypt_at_rest { + enabled = true + } +} +`, randESId) +} + +func testAccESDomainConfigWithEncryptAtRestWithKey(randESId int) string { return fmt.Sprintf(` resource "aws_kms_key" "es" { description = "kms-key-for-tf-test-%d" From 5d1b54593247a7d12efd10e2650471eaeae8aa53 Mon Sep 17 00:00:00 2001 From: Tom Elliff Date: Tue, 12 Dec 2017 12:25:38 +0000 Subject: [PATCH 3/6] Add documentation for ES encryption at rest --- website/docs/r/elasticsearch_domain.html.markdown | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/docs/r/elasticsearch_domain.html.markdown b/website/docs/r/elasticsearch_domain.html.markdown index dd825dcfc50..3e255df0e60 100644 --- a/website/docs/r/elasticsearch_domain.html.markdown +++ b/website/docs/r/elasticsearch_domain.html.markdown @@ -57,6 +57,7 @@ The following arguments are supported: * `access_policies` - (Optional) IAM policy document specifying the access policies for the domain * `advanced_options` - (Optional) Key-value string pairs to specify advanced configuration options. * `ebs_options` - (Optional) EBS related options, may be required based on chosen [instance size](https://aws.amazon.com/elasticsearch-service/pricing/). See below. +* `encrypt_at_rest` - (Optional) Encrypt at rest options. Only available for [certain instance types](http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html). See below. * `cluster_config` - (Optional) Cluster configuration of the domain, see below. * `snapshot_options` - (Optional) Snapshot related options, see below. * `vpc_options` - (Optional) VPC related options, see below. Adding or removing this configuration forces a new resource ([documentation](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-vpc.html#es-vpc-limitations)). @@ -73,6 +74,11 @@ The following arguments are supported: * `iops` - (Optional) The baseline input/output (I/O) performance of EBS volumes attached to data nodes. Applicable only for the Provisioned IOPS EBS volume type. +**encrypt_at_rest** supports the following attributes: + +* `enabled` - (Required) Whether to enable encryption at rest. If the `encrypt_at_rest` block is not provided then this defaults to `false`. +* `kms_key_id` - (Optional) The KMS key id to encrypt the Elasticsearch domain with. If not specified then it defaults to using the `aws/es` service KMS key. + **cluster_config** supports the following attributes: * `instance_type` - (Optional) Instance type of data nodes in the cluster. From ca73236503971f2c2f0394070d120926a077add4 Mon Sep 17 00:00:00 2001 From: Tom Elliff Date: Tue, 12 Dec 2017 13:37:04 +0000 Subject: [PATCH 4/6] Read KMS key for encrypt at rest from AWS API --- aws/resource_aws_elasticsearch_domain.go | 19 +++++++++++++++++-- aws/structure.go | 13 +++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/aws/resource_aws_elasticsearch_domain.go b/aws/resource_aws_elasticsearch_domain.go index c48d3c9c608..f8753935678 100644 --- a/aws/resource_aws_elasticsearch_domain.go +++ b/aws/resource_aws_elasticsearch_domain.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" + "strings" ) func resourceAwsElasticSearchDomain() *schema.Resource { @@ -101,8 +102,11 @@ func resourceAwsElasticSearchDomain() *schema.Resource { ForceNew: true, }, "kms_key_id": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + DiffSuppressFunc: suppressEquivalentKmsKeyIds, }, }, }, @@ -494,6 +498,10 @@ func resourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface{} if err != nil { return err } + err = d.Set("encrypt_at_rest", flattenESEncryptAtRestOptions(ds.EncryptionAtRestOptions)) + if err != nil { + return err + } err = d.Set("cluster_config", flattenESClusterConfig(ds.ElasticsearchClusterConfig)) if err != nil { return err @@ -707,3 +715,10 @@ func resourceAwsElasticSearchDomainDelete(d *schema.ResourceData, meta interface return err } + +func suppressEquivalentKmsKeyIds(k, old, new string, d *schema.ResourceData) bool { + // The Elasticsearch API accepts a short KMS key id but always returns the ARN of the key. + // The ARN is of the format 'arn:aws:kms:REGION:ACCOUNT_ID:key/KMS_KEY_ID'. + // These should be treated as equivalent. + return strings.Contains(old, new) +} diff --git a/aws/structure.go b/aws/structure.go index fc7cee31498..c0ecb0d9c1e 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -1045,6 +1045,19 @@ func expandESEBSOptions(m map[string]interface{}) *elasticsearch.EBSOptions { return &options } +func flattenESEncryptAtRestOptions(o *elasticsearch.EncryptionAtRestOptions) []map[string]interface{} { + m := map[string]interface{}{} + + if o.Enabled != nil { + m["enabled"] = *o.Enabled + } + if o.KmsKeyId != nil { + m["kms_key_id"] = *o.KmsKeyId + } + + return []map[string]interface{}{m} +} + func expandESEncryptAtRestOptions(m map[string]interface{}) *elasticsearch.EncryptionAtRestOptions { options := elasticsearch.EncryptionAtRestOptions{} From a56d511fb9ae890601e76c2423762e6b38830003 Mon Sep 17 00:00:00 2001 From: Tom Elliff Date: Sun, 21 Jan 2018 19:39:14 +0000 Subject: [PATCH 5/6] Fix PR feed back on #2632 --- aws/resource_aws_elasticsearch_domain.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/aws/resource_aws_elasticsearch_domain.go b/aws/resource_aws_elasticsearch_domain.go index f8753935678..2680b582418 100644 --- a/aws/resource_aws_elasticsearch_domain.go +++ b/aws/resource_aws_elasticsearch_domain.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "regexp" + "strings" "time" "github.com/aws/aws-sdk-go/aws" @@ -13,7 +14,6 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" - "strings" ) func resourceAwsElasticSearchDomain() *schema.Resource { @@ -94,6 +94,7 @@ func resourceAwsElasticSearchDomain() *schema.Resource { Type: schema.TypeList, Optional: true, Computed: true, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "enabled": { @@ -315,17 +316,12 @@ func resourceAwsElasticSearchDomainCreate(d *schema.ResourceData, meta interface if v, ok := d.GetOk("encrypt_at_rest"); ok { options := v.([]interface{}) - - if len(options) > 1 { - return fmt.Errorf("Only a single encrypt_at_rest block is expected") - } else if len(options) == 1 { - if options[0] == nil { - return fmt.Errorf("At least one field is expected inside encrypt_at_rest") - } - - s := options[0].(map[string]interface{}) - input.EncryptionAtRestOptions = expandESEncryptAtRestOptions(s) + if options[0] == nil { + return fmt.Errorf("At least one field is expected inside encrypt_at_rest") } + + s := options[0].(map[string]interface{}) + input.EncryptionAtRestOptions = expandESEncryptAtRestOptions(s) } if v, ok := d.GetOk("cluster_config"); ok { From a13f2a9d8e4262c624cabf6ea328c8eefffbc050 Mon Sep 17 00:00:00 2001 From: Tom Elliff Date: Mon, 22 Jan 2018 07:42:34 +0000 Subject: [PATCH 6/6] fixup a56d511fb9ae890601e76c2423762e6b38830003 --- aws/structure.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aws/structure.go b/aws/structure.go index 45ed4fe6d35..ddebb751d13 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -1047,6 +1047,10 @@ func expandESEBSOptions(m map[string]interface{}) *elasticsearch.EBSOptions { } func flattenESEncryptAtRestOptions(o *elasticsearch.EncryptionAtRestOptions) []map[string]interface{} { + if o == nil { + return []map[string]interface{}{} + } + m := map[string]interface{}{} if o.Enabled != nil {