Skip to content

Commit

Permalink
Add local snapshots on outposts (#2130)
Browse files Browse the repository at this point in the history
* Add local snapshots on outposts

* Fix OutPostArnKey in switch statement

* Add arn verification and VSC paramater doc

* Fix assignment of outpostArn outisde if statement

* Fix typo in snapshot.md

* Removed information found in linked docs
  • Loading branch information
ElijahQuinones authored Sep 9, 2024
1 parent dd215d5 commit 59b7baa
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 12 deletions.
41 changes: 33 additions & 8 deletions docs/fast-snapshot-restores.md → docs/snapshot.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
## Supported Parameters
| Parameter | Description of value |
|------------------------------------|-----------------------------------------------------------|
| fastSnapshotRestoreAvailabilityZones | Comma separated list of availability zones |
| outpostArn | Arn of the outpost you wish to have the snapshot saved to |

The AWS EBS CSI Driver supports [tagging](tagging.md) through `VolumeSnapshotClass.parameters` (in v1.6.0 and later).
## Prerequisites

- Install the [Kubernetes Volume Snapshot CRDs](https://github.com/kubernetes-csi/external-snapshotter/tree/master/client/config/crd) and external-snapshotter sidecar. For installation instructions, see [CSI Snapshotter Usage](https://github.com/kubernetes-csi/external-snapshotter#usage).

# Fast Snapshot Restores

The EBS CSI Driver provides support for [Fast Snapshot Restores(FSR)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-fast-snapshot-restore.html) via `VolumeSnapshotClass.parameters.fastSnapshotRestoreAvailabilityZones`.

Amazon EBS fast snapshot restore (FSR) enables you to create a volume from a snapshot that is fully initialized at creation. This eliminates the latency of I/O operations on a block when it is accessed for the first time. Volumes that are created using fast snapshot restore instantly deliver all of their provisioned performance.

Availability zones are specified as a comma separated list.

- The EBS CSI Driver must be given permission to access the [`EnableFastSnapshotRestores` EC2 API](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EnableFastSnapshotRestores.html). This example snippet can be used in an IAM policy to grant access to `EnableFastSnapshotRestores`:

**Example**
```
apiVersion: snapshot.storage.k8s.io/v1
Expand All @@ -18,12 +30,6 @@ parameters:
fastSnapshotRestoreAvailabilityZones: "us-east-1a, us-east-1b"
```

## Prerequisites

- Install the [Kubernetes Volume Snapshot CRDs](https://github.com/kubernetes-csi/external-snapshotter/tree/master/client/config/crd) and external-snapshotter sidecar. For installation instructions, see [CSI Snapshotter Usage](https://github.com/kubernetes-csi/external-snapshotter#usage).

- The EBS CSI Driver must be given permission to access the [`EnableFastSnapshotRestores` EC2 API](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EnableFastSnapshotRestores.html). This example snippet can be used in an IAM policy to grant access to `EnableFastSnapshotRestores`:

```json
{
"Effect": "Allow",
Expand All @@ -34,6 +40,25 @@ parameters:
}
```

## Failure Mode
## Failure Mode

The driver will attempt to check if the availability zones provided are supported for fast snapshot restore before attempting to create the snapshot. If the `EnableFastSnapshotRestores` API call fails, the driver will hard-fail the request and delete the snapshot. This is to ensure that the snapshot is not left in an inconsistent state.


# Amazon EBS Local Snapshots on Outposts

The EBS CSI Driver provides support for [Amazon EBS local snapshots on Outposts](https://docs.aws.amazon.com/ebs/latest/userguide/snapshots-outposts.html) via `VolumeSnapshotClass.parameters.outpostArn`.



**Example**
```
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-aws-vsc
driver: ebs.csi.aws.com
deletionPolicy: Delete
parameters:
outpostarn: {arn of your outpost}
```
10 changes: 7 additions & 3 deletions pkg/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ type ListSnapshotsResponse struct {

// SnapshotOptions represents parameters to create an EBS volume
type SnapshotOptions struct {
Tags map[string]string
Tags map[string]string
OutpostArn string
}

// ec2ListSnapshotsResponse is a helper struct returned from the AWS API calling function to the main ListSnapshots function
Expand Down Expand Up @@ -1275,19 +1276,22 @@ func (c *cloud) CreateSnapshot(ctx context.Context, volumeID string, snapshotOpt
descriptions := "Created by AWS EBS CSI driver for volume " + volumeID

var tags []types.Tag
var request *ec2.CreateSnapshotInput
for key, value := range snapshotOptions.Tags {
tags = append(tags, types.Tag{Key: aws.String(key), Value: aws.String(value)})
}
tagSpec := types.TagSpecification{
ResourceType: types.ResourceTypeSnapshot,
Tags: tags,
}
request := &ec2.CreateSnapshotInput{
request = &ec2.CreateSnapshotInput{
VolumeId: aws.String(volumeID),
TagSpecifications: []types.TagSpecification{tagSpec},
Description: aws.String(descriptions),
}

if snapshotOptions.OutpostArn != "" {
request.OutpostArn = aws.String(snapshotOptions.OutpostArn)
}
res, err := c.ec2.CreateSnapshot(ctx, request, func(o *ec2.Options) {
o.Retryer = c.rm.createSnapshotRetryer
})
Expand Down
19 changes: 19 additions & 0 deletions pkg/cloud/cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2059,6 +2059,25 @@ func TestCreateSnapshot(t *testing.T) {
},
expErr: nil,
},
{
name: "success: outpost",
snapshotName: "snap-test-name",
snapshotOptions: &SnapshotOptions{
Tags: map[string]string{
SnapshotNameTagKey: "snap-test-name",
AwsEbsDriverTagKey: "true",
"extra-tag-key": "extra-tag-value",
},
OutpostArn: "arn:aws:outposts:us-east-1:222222222222:outpost/aa-aaaaaaaaaaaaaaaaa",
},
expSnapshot: &Snapshot{
SnapshotID: "snap-test-name",
SourceVolumeID: "snap-test-volume",
Size: 10,
ReadyToUse: true,
},
expErr: nil,
},
}

for _, tc := range testCases {
Expand Down
3 changes: 3 additions & 0 deletions pkg/driver/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ const (
// TagKeyPrefix contains the prefix of a volume parameter that designates it as
// a tag to be attached to the resource
TagKeyPrefix = "tagSpecification"

// OutpostArn represents key for outpost's arn
OutpostArnKey = "outpostarn"
)

// constants of keys in snapshot parameters
Expand Down
10 changes: 9 additions & 1 deletion pkg/driver/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS

snapshotName := req.GetName()
volumeID := req.GetSourceVolumeId()
var outpostArn string

// check if a request is already in-flight
if ok := d.inFlight.Insert(snapshotName); !ok {
Expand Down Expand Up @@ -709,6 +710,12 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
case FastSnapshotRestoreAvailabilityZones:
f := strings.ReplaceAll(value, " ", "")
fsrAvailabilityZones = strings.Split(f, ",")
case OutpostArnKey:
if arn.IsARN(value) {
outpostArn = value
} else {
return nil, status.Errorf(codes.InvalidArgument, "Invalid parameter value %s is not a valid arn", value)
}
default:
if strings.HasPrefix(key, TagKeyPrefix) {
vscTags = append(vscTags, value)
Expand Down Expand Up @@ -741,7 +748,8 @@ func (d *ControllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
}

opts := &cloud.SnapshotOptions{
Tags: snapshotTags,
Tags: snapshotTags,
OutpostArn: outpostArn,
}

// Check if the availability zone is supported for fast snapshot restore
Expand Down
72 changes: 72 additions & 0 deletions pkg/driver/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2271,6 +2271,49 @@ func TestCreateSnapshot(t *testing.T) {
}
},
},
{
name: "success outpost",
testFunc: func(t *testing.T) {
req := &csi.CreateSnapshotRequest{
Name: "test-snapshot",
Parameters: map[string]string{
"outpostArn": "arn:aws:outposts:us-east-1:222222222222:outpost/aa-aaaaaaaaaaaaaaaaa",
},
SourceVolumeId: "vol-test",
}
expSnapshot := &csi.Snapshot{
ReadyToUse: true,
}

ctx := context.Background()
mockSnapshot := &cloud.Snapshot{
SnapshotID: fmt.Sprintf("snapshot-%d", rand.New(rand.NewSource(time.Now().UnixNano())).Uint64()),
SourceVolumeID: req.GetSourceVolumeId(),
Size: 1,
CreationTime: time.Now(),
}
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()

mockCloud := cloud.NewMockCloud(mockCtl)
mockCloud.EXPECT().CreateSnapshot(gomock.Eq(ctx), gomock.Eq(req.GetSourceVolumeId()), gomock.Any()).Return(mockSnapshot, nil)
mockCloud.EXPECT().GetSnapshotByName(gomock.Eq(ctx), gomock.Eq(req.GetName())).Return(nil, cloud.ErrNotFound)

awsDriver := ControllerService{
cloud: mockCloud,
inFlight: internal.NewInFlight(),
options: &Options{},
}
resp, err := awsDriver.CreateSnapshot(context.Background(), req)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

if snap := resp.GetSnapshot(); snap == nil {
t.Fatalf("Expected snapshot %v, got nil", expSnapshot)
}
},
},
{
name: "success with cluster-id",
testFunc: func(t *testing.T) {
Expand Down Expand Up @@ -2418,6 +2461,35 @@ func TestCreateSnapshot(t *testing.T) {
}
},
},
{
name: "fail outpost arn not valid",
testFunc: func(t *testing.T) {
req := &csi.CreateSnapshotRequest{
Name: "test-snapshot",
Parameters: map[string]string{
"outpostArn": "notAnArn",
},
SourceVolumeId: "vol-test",
}

ctx := context.Background()

mockCtl := gomock.NewController(t)
defer mockCtl.Finish()

mockCloud := cloud.NewMockCloud(mockCtl)
mockCloud.EXPECT().GetSnapshotByName(gomock.Eq(ctx), gomock.Eq(req.GetName())).Return(nil, cloud.ErrNotFound)

awsDriver := ControllerService{
cloud: mockCloud,
inFlight: internal.NewInFlight(),
options: &Options{},
}
_, err := awsDriver.CreateSnapshot(context.Background(), req)
checkExpectedErrorCode(t, err, codes.InvalidArgument)

},
},
{
name: "fail same name different volume ID",
testFunc: func(t *testing.T) {
Expand Down

0 comments on commit 59b7baa

Please sign in to comment.