Repository: getmoto/moto

Issue:
EBS volumes don't store KmsKeyId 
```python
from moto import mock_ec2
import boto3

mock = mock_ec2()
mock.start()

resource = boto3.resource("ec2")

kwargs = {
    "AvailabilityZone": "ap-southeast-2a",
    "Size": 8,
    "Encrypted": True,
    "KmsKeyId": "key"    
}

volume = resource.create_volume(**kwargs)

print(volume.kms_key_id)
```

The above prints `None`. I was expecting it to print `key`.

moto 1.3.14


Discussion:
Thanks for raising this @tekumara - marking this as an enhancement

Resolving patch:
diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py
--- a/moto/ec2/exceptions.py
+++ b/moto/ec2/exceptions.py
@@ -573,3 +573,13 @@ def __init__(self):
             "InvalidLaunchTemplateName.AlreadyExistsException",
             "Launch template name already in use.",
         )
+
+
+class InvalidParameterDependency(EC2ClientError):
+    def __init__(self, param, param_needed):
+        super(InvalidParameterDependency, self).__init__(
+            "InvalidParameterDependency",
+            "The parameter [{0}] requires the parameter {1} to be set.".format(
+                param, param_needed
+            ),
+        )
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -28,6 +28,7 @@
     camelcase_to_underscores,
 )
 from moto.core import ACCOUNT_ID
+from moto.kms import kms_backends
 
 from .exceptions import (
     CidrLimitExceeded,
@@ -97,6 +98,7 @@
     ResourceAlreadyAssociatedError,
     RulesPerSecurityGroupLimitExceededError,
     TagLimitExceeded,
+    InvalidParameterDependency,
 )
 from .utils import (
     EC2_RESOURCE_TO_PREFIX,
@@ -2425,7 +2427,14 @@ def create_from_cloudformation_json(
 
 class Volume(TaggedEC2Resource, CloudFormationModel):
     def __init__(
-        self, ec2_backend, volume_id, size, zone, snapshot_id=None, encrypted=False
+        self,
+        ec2_backend,
+        volume_id,
+        size,
+        zone,
+        snapshot_id=None,
+        encrypted=False,
+        kms_key_id=None,
     ):
         self.id = volume_id
         self.size = size
@@ -2435,6 +2444,7 @@ def __init__(
         self.snapshot_id = snapshot_id
         self.ec2_backend = ec2_backend
         self.encrypted = encrypted
+        self.kms_key_id = kms_key_id
 
     @staticmethod
     def cloudformation_name_type():
@@ -2548,7 +2558,13 @@ def __init__(self):
         self.snapshots = {}
         super(EBSBackend, self).__init__()
 
-    def create_volume(self, size, zone_name, snapshot_id=None, encrypted=False):
+    def create_volume(
+        self, size, zone_name, snapshot_id=None, encrypted=False, kms_key_id=None
+    ):
+        if kms_key_id and not encrypted:
+            raise InvalidParameterDependency("KmsKeyId", "Encrypted")
+        if encrypted and not kms_key_id:
+            kms_key_id = self._get_default_encryption_key()
         volume_id = random_volume_id()
         zone = self.get_zone_by_name(zone_name)
         if snapshot_id:
@@ -2557,7 +2573,7 @@ def create_volume(self, size, zone_name, snapshot_id=None, encrypted=False):
                 size = snapshot.volume.size
             if snapshot.encrypted:
                 encrypted = snapshot.encrypted
-        volume = Volume(self, volume_id, size, zone, snapshot_id, encrypted)
+        volume = Volume(self, volume_id, size, zone, snapshot_id, encrypted, kms_key_id)
         self.volumes[volume_id] = volume
         return volume
 
@@ -2705,6 +2721,25 @@ def remove_create_volume_permission(self, snapshot_id, user_ids=None, groups=Non
 
         return True
 
+    def _get_default_encryption_key(self):
+        # https://aws.amazon.com/kms/features/#AWS_Service_Integration
+        # An AWS managed CMK is created automatically when you first create
+        # an encrypted resource using an AWS service integrated with KMS.
+        kms = kms_backends[self.region_name]
+        ebs_alias = "alias/aws/ebs"
+        if not kms.alias_exists(ebs_alias):
+            key = kms.create_key(
+                policy="",
+                key_usage="ENCRYPT_DECRYPT",
+                customer_master_key_spec="SYMMETRIC_DEFAULT",
+                description="Default master key that protects my EBS volumes when no other key is defined",
+                tags=None,
+                region=self.region_name,
+            )
+            kms.add_alias(key.id, ebs_alias)
+        ebs_key = kms.describe_key(ebs_alias)
+        return ebs_key.arn
+
 
 class VPC(TaggedEC2Resource, CloudFormationModel):
     def __init__(
diff --git a/moto/ec2/responses/elastic_block_store.py b/moto/ec2/responses/elastic_block_store.py
--- a/moto/ec2/responses/elastic_block_store.py
+++ b/moto/ec2/responses/elastic_block_store.py
@@ -46,9 +46,12 @@ def create_volume(self):
         snapshot_id = self._get_param("SnapshotId")
         tags = self._parse_tag_specification("TagSpecification")
         volume_tags = tags.get("volume", {})
-        encrypted = self._get_param("Encrypted", if_none=False)
+        encrypted = self._get_bool_param("Encrypted", if_none=False)
+        kms_key_id = self._get_param("KmsKeyId")
         if self.is_not_dryrun("CreateVolume"):
-            volume = self.ec2_backend.create_volume(size, zone, snapshot_id, encrypted)
+            volume = self.ec2_backend.create_volume(
+                size, zone, snapshot_id, encrypted, kms_key_id
+            )
             volume.add_tags(volume_tags)
             template = self.response_template(CREATE_VOLUME_RESPONSE)
             return template.render(volume=volume)
@@ -161,7 +164,10 @@ def reset_snapshot_attribute(self):
   {% else %}
     <snapshotId/>
   {% endif %}
-  <encrypted>{{ volume.encrypted }}</encrypted>
+  <encrypted>{{ 'true' if volume.encrypted else 'false' }}</encrypted>
+  {% if volume.encrypted %}
+  <kmsKeyId>{{ volume.kms_key_id }}</kmsKeyId>
+  {% endif %}
   <availabilityZone>{{ volume.zone.name }}</availabilityZone>
   <status>creating</status>
   <createTime>{{ volume.create_time}}</createTime>
@@ -192,7 +198,10 @@ def reset_snapshot_attribute(self):
              {% else %}
                <snapshotId/>
              {% endif %}
-             <encrypted>{{ volume.encrypted }}</encrypted>
+             <encrypted>{{ 'true' if volume.encrypted else 'false' }}</encrypted>
+             {% if volume.encrypted %}
+             <kmsKeyId>{{ volume.kms_key_id }}</kmsKeyId>
+             {% endif %}
              <availabilityZone>{{ volume.zone.name }}</availabilityZone>
              <status>{{ volume.status }}</status>
              <createTime>{{ volume.create_time}}</createTime>


Test patch:
diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py
--- a/tests/test_ec2/test_elastic_block_store.py
+++ b/tests/test_ec2/test_elastic_block_store.py
@@ -13,6 +13,7 @@
 
 from moto import mock_ec2_deprecated, mock_ec2
 from moto.ec2.models import OWNER_ID
+from moto.kms import mock_kms
 
 
 @mock_ec2_deprecated
@@ -915,3 +916,65 @@ def test_search_for_many_snapshots():
     snapshots_response = ec2_client.describe_snapshots(SnapshotIds=snapshot_ids)
 
     assert len(snapshots_response["Snapshots"]) == len(snapshot_ids)
+
+
+@mock_ec2
+def test_create_unencrypted_volume_with_kms_key_fails():
+    resource = boto3.resource("ec2", region_name="us-east-1")
+    with assert_raises(ClientError) as ex:
+        resource.create_volume(
+            AvailabilityZone="us-east-1a", Encrypted=False, KmsKeyId="key", Size=10
+        )
+    ex.exception.response["Error"]["Code"].should.equal("InvalidParameterDependency")
+    ex.exception.response["Error"]["Message"].should.contain("KmsKeyId")
+
+
+@mock_kms
+@mock_ec2
+def test_create_encrypted_volume_without_kms_key_should_use_default_key():
+    kms = boto3.client("kms", region_name="us-east-1")
+    # Default master key for EBS does not exist until needed.
+    with assert_raises(ClientError) as ex:
+        kms.describe_key(KeyId="alias/aws/ebs")
+    ex.exception.response["Error"]["Code"].should.equal("NotFoundException")
+    # Creating an encrypted volume should create (and use) the default key.
+    resource = boto3.resource("ec2", region_name="us-east-1")
+    volume = resource.create_volume(
+        AvailabilityZone="us-east-1a", Encrypted=True, Size=10
+    )
+    default_ebs_key_arn = kms.describe_key(KeyId="alias/aws/ebs")["KeyMetadata"]["Arn"]
+    volume.kms_key_id.should.equal(default_ebs_key_arn)
+    volume.encrypted.should.be.true
+    # Subsequent encrypted volumes should use the now-created default key.
+    volume = resource.create_volume(
+        AvailabilityZone="us-east-1a", Encrypted=True, Size=10
+    )
+    volume.kms_key_id.should.equal(default_ebs_key_arn)
+    volume.encrypted.should.be.true
+
+
+@mock_ec2
+def test_create_volume_with_kms_key():
+    resource = boto3.resource("ec2", region_name="us-east-1")
+    volume = resource.create_volume(
+        AvailabilityZone="us-east-1a", Encrypted=True, KmsKeyId="key", Size=10
+    )
+    volume.kms_key_id.should.equal("key")
+    volume.encrypted.should.be.true
+
+
+@mock_ec2
+def test_kms_key_id_property_hidden_when_volume_not_encrypted():
+    client = boto3.client("ec2", region_name="us-east-1")
+    resp = client.create_volume(AvailabilityZone="us-east-1a", Encrypted=False, Size=10)
+    resp["Encrypted"].should.be.false
+    resp.should_not.have.key("KmsKeyId")
+    resp = client.describe_volumes(VolumeIds=[resp["VolumeId"]])
+    resp["Volumes"][0]["Encrypted"].should.be.false
+    resp["Volumes"][0].should_not.have.key("KmsKeyId")
+    resource = boto3.resource("ec2", region_name="us-east-1")
+    volume = resource.create_volume(
+        AvailabilityZone="us-east-1a", Encrypted=False, Size=10
+    )
+    volume.encrypted.should.be.false
+    volume.kms_key_id.should.be.none