diff --git a/aws/resource_aws_elasticsearch_domain.go b/aws/resource_aws_elasticsearch_domain.go index 4ef72d140ec..b67b1b59395 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" @@ -94,6 +95,28 @@ func resourceAwsElasticSearchDomain() *schema.Resource { }, }, }, + "encrypt_at_rest": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Required: true, + ForceNew: true, + }, + "kms_key_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + DiffSuppressFunc: suppressEquivalentKmsKeyIds, + }, + }, + }, + }, "cluster_config": { Type: schema.TypeList, Optional: true, @@ -296,6 +319,16 @@ func resourceAwsElasticSearchDomainCreate(d *schema.ResourceData, meta interface } } + if v, ok := d.GetOk("encrypt_at_rest"); ok { + options := v.([]interface{}) + 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{}) @@ -466,6 +499,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 @@ -684,6 +721,13 @@ 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) +} + func getKibanaEndpoint(d *schema.ResourceData) string { return d.Get("endpoint").(string) + "/_plugin/kibana/" } diff --git a/aws/resource_aws_elasticsearch_domain_test.go b/aws/resource_aws_elasticsearch_domain_test.go index 6c9ed40bc87..885cfd23a1d 100644 --- a/aws/resource_aws_elasticsearch_domain_test.go +++ b/aws/resource_aws_elasticsearch_domain_test.go @@ -262,6 +262,44 @@ func TestAccAWSElasticSearchDomain_policy(t *testing.T) { }) } +func TestAccAWSElasticSearchDomain_encrypt_at_rest_default_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: 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), + ), + }, + }, + }) +} + func TestAccAWSElasticSearchDomain_tags(t *testing.T) { var domain elasticsearch.ElasticsearchDomainStatus var td elasticsearch.ListTagsOutput @@ -340,6 +378,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 @@ -412,6 +460,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 @@ -505,6 +554,61 @@ data "aws_iam_policy_document" "instance-assume-role-policy" { `, randESId, randRoleId) } +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" + 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 f4261153efc..ddebb751d13 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -1046,6 +1046,36 @@ func expandESEBSOptions(m map[string]interface{}) *elasticsearch.EBSOptions { return &options } +func flattenESEncryptAtRestOptions(o *elasticsearch.EncryptionAtRestOptions) []map[string]interface{} { + if o == nil { + return []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{} + + 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{}{} diff --git a/website/docs/r/elasticsearch_domain.html.markdown b/website/docs/r/elasticsearch_domain.html.markdown index bc86cf60fcf..84272c6d269 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.