Skip to content

Commit

Permalink
refactor: Change snapshot functionality from taking EBS to AMI snapsh…
Browse files Browse the repository at this point in the history
…ots (#16)

* integrate awsdata endpoint changes from
module_changes

* Modified EBS snapshot to AMI snapshot

* Display captured snapshot in list of AMIs

* Fixed bug where snapshots will wipe region field
in dynamodb record

* Fix UI bug

* fix code quality formatting

* modified ami naming to avoid duplicates

* fix code quality formatting

* fix merge conflict

* fix issue where spot has duplicate domain
  • Loading branch information
luqmanbazran authored Aug 7, 2024
1 parent 93f8245 commit 2dee671
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ export class SnapshotConfirmationDialogComponent {
id: this.data.instanceElement.deploymentId,
instanceId: this.data.instanceElement.ec2InstanceId,
hostname: this.data.instanceElement.hostname,
region: Region.AP_SOUTHEAST_3,
region: this.data.instanceElement.availabilityZone,
ami: this.data.instanceElement.ami,
serverSize: this.data.instanceElement.serverSize,
lifecycle: this.data.instanceElement.lifecycle,
timeToExpire: this.data.instanceElement.timeToExpire,
};
this.apiService
.captureInstanceSnapshopt(apiPayload)
Expand Down
1 change: 1 addition & 0 deletions client/src/app/shared/model/deployment-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export class DeploymentApiRequest {
lifecycle!: Lifecycle;
ttlValue?: number;
ttlUnit?: string;
timeToExpire?: number;
}
2 changes: 1 addition & 1 deletion ecr-scripts/instance.tf
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ resource "aws_spot_instance_request" "my_deployed_spot_instances" {
vpc_security_group_ids = local.use_custom_security_group ? [var.security_group_id] : null
user_data = data.aws_s3_object.user_data.body
tags = {
Name = "${each.value.hostname}.${data.aws_route53_zone.hosted_zone.name}"
Name = each.value.hostname
Hostname = replace(each.value.hostname, "/.${data.aws_route53_zone.hosted_zone.name}/", "")
DeploymentID = each.value.id
TimeToExpire = each.value.timeToExpire
Expand Down
5 changes: 3 additions & 2 deletions server/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,21 @@ func UpdateRecord(id string, updateData models.DynamoDBData) error {
return ErrHostnameExists
}

// there was a field for region, removed for now as region is static
update := expression.Set(
expression.Name("ami"), expression.Value(updateData.Ami),
).Set(
expression.Name("serverSize"), expression.Value(updateData.ServerSize),
).Set(
expression.Name("hostname"), expression.Value(updateData.Hostname),
).Set(
expression.Name("region"), expression.Value(updateData.Region),
).Set(
expression.Name("creationUser"), expression.Value(updateData.CreationUser),
).Set(
expression.Name("lifecycle"), expression.Value(updateData.Lifecycle),
).Set(
expression.Name("timeToExpire"), expression.Value(updateData.TimeToExpire),
).Set(
expression.Name("snapShot"), expression.Value(updateData.SnapShot),
)

// Build the update expression.
Expand Down
16 changes: 15 additions & 1 deletion server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log"
"net/http"
"os"
"strconv"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-sdk-go-v2/aws"
Expand Down Expand Up @@ -255,6 +256,13 @@ func GetAWSData(c *gin.Context) {
// add region env
config.Region = regionEnv

// get list of snapshot AMIs, and add to the AMI available
config.Ami, err = instance.GetAvailableAmis(config.Ami)
if err != nil {
log.Printf("Failed to get list of AMIs: %v", err)
return
}

c.JSON(http.StatusOK, config)
}

Expand Down Expand Up @@ -403,11 +411,16 @@ func CaptureInstanceSnapshot(c *gin.Context) {
log.Println("update request for id:", id)

var snapshotID string
if snapshotID, err = instance.CaptureInstanceSnapshot(req.InstanceID); err != nil {
if snapshotID, err = instance.CaptureInstanceImage(req.InstanceID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

timeToLive, err := strconv.ParseInt(req.TimeToExpire, 10, 64)
if err != nil {
log.Printf("Failed to parse ttl with error %v", err)
}

// Convert request to DynamoDBData struct
data := models.DynamoDBData{
ID: id,
Expand All @@ -419,6 +432,7 @@ func CaptureInstanceSnapshot(c *gin.Context) {
Lifecycle: req.Lifecycle,
SnapShot: snapshotID,
ContentDeployment: req.ContentDeployment,
TimeToExpire: timeToLive,
}

// Update the DynamoDB row to include the captured snapshot ID
Expand Down
188 changes: 144 additions & 44 deletions server/instance/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package instance

import (
"context"
"fmt"
"log"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
Expand Down Expand Up @@ -47,47 +49,37 @@ func GetDeployedInstances() ([]models.DeploymentResponse, error) {

for _, reservation := range output.Reservations {
for _, instance := range reservation.Instances {
// Retrieve volume IDs from the instance
var volumeIDs []string
for _, blockDevice := range instance.BlockDeviceMappings {
volumeIDs = append(volumeIDs, aws.ToString(blockDevice.Ebs.VolumeId))
// Get image based on the instance
filter := []types.Filter{
{
Name: aws.String("source-instance-id"),
Values: []string{*instance.InstanceId},
},
{
Name: aws.String("is-public"),
Values: []string{"false"},
},
}

// Check for snapshots and get the latest snapshot ID for the volumes
snapshotID := "None"
var latestSnapshot *types.Snapshot
for _, volumeID := range volumeIDs {
snapshotInput := &ec2.DescribeSnapshotsInput{
Filters: []types.Filter{
{
Name: aws.String("volume-id"),
Values: []string{volumeID},
},
},
}
snapshots, err := ec2Client.DescribeSnapshots(context.Background(), snapshotInput)
if err != nil {
log.Printf("Error retrieving snapshots for volume %s: %v", volumeID, err)
continue
}
for i := range snapshots.Snapshots {
snapshot := snapshots.Snapshots[i]
if latestSnapshot == nil || snapshot.StartTime.After(*latestSnapshot.StartTime) {
latestSnapshot = &snapshots.Snapshots[i]
}
}

imageResult, err := getImage(filter)
if err != nil {
log.Printf("failed to resolve image for instance %s: %v", *instance.InstanceId, err)
return nil, err
}

if latestSnapshot != nil {
snapshotID = aws.ToString(latestSnapshot.SnapshotId)
var imageID string
if len(imageResult.Images) == 0 {
imageID = "none"
} else {
imageID = *imageResult.Images[0].ImageId
}

deployment := models.DeploymentResponse{
InstanceID: aws.ToString(instance.InstanceId),
DeploymentID: getInstanceTagValue("DeploymentID", instance.Tags),
Hostname: getInstanceTagValue("Name", instance.Tags),
TimeToExpire: getInstanceTagValue("TimeToExpire", instance.Tags),
SnapshotID: snapshotID,
SnapshotID: imageID,
Ami: aws.ToString(instance.ImageId),
ServerSize: string(instance.InstanceType),
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
Expand Down Expand Up @@ -149,28 +141,136 @@ func getLifecycle(lifecycle types.InstanceLifecycleType) string {
return string(lifecycle)
}

func CaptureInstanceSnapshot(instanceID string) (string, error) {
describeInstanceInput := &ec2.DescribeInstancesInput{
InstanceIds: []string{instanceID},
func CaptureInstanceImage(instanceID string) (string, error) {
// check if an image for that instance already exists
filter := []types.Filter{
{
Name: aws.String("source-instance-id"),
Values: []string{instanceID},
},
{
Name: aws.String("is-public"),
Values: []string{"false"},
},
}
instanceResult, err := ec2Client.DescribeInstances(context.Background(), describeInstanceInput)

imageResult, err := getImage(filter)
if err != nil {
log.Printf("failed to describe instance %s: %v", instanceID, err)
log.Printf("failed to resolve image for instance %s: %v", instanceID, err)
return "", err
}

// snapshot the first volume
volumeID := instanceResult.Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId
// if it exists deregister it
if len(imageResult.Images) == 0 {
log.Printf("No images returned for deregistering")
} else {
describeDeregisterImage := &ec2.DeregisterImageInput{
ImageId: imageResult.Images[0].ImageId,
}
log.Printf("The id of the image is %s, deregistering...", *imageResult.Images[0].ImageId)
_, err := ec2Client.DeregisterImage(context.Background(), describeDeregisterImage)
if err != nil {
log.Printf("failed to deregister image for instance %s: %v", instanceID, err)
return "", err
}
}

// get tags of the instance
describeInstanceTags := &ec2.DescribeTagsInput{
Filters: []types.Filter{
{
Name: aws.String("resource-id"),
Values: []string{instanceID},
},
},
}

tagsResult, err := ec2Client.DescribeTags(context.Background(), describeInstanceTags)
if err != nil {
log.Printf("failed to describe tags for instance %s: %v", instanceID, err)
return "", err
}

instanceName := "None"
for _, tags := range tagsResult.Tags {
if *tags.Key == "Name" {
instanceName = *tags.Value
}
}

// get current time
now := time.Now()
date := now.Format(time.DateOnly)
time := fmt.Sprintf("%d%d%d", now.Hour(), now.Minute(), now.Second())
formattedName := instanceName + "_" + date + "_" + time

snapshotInput := &ec2.CreateSnapshotInput{
VolumeId: volumeID,
// snapshot the instance
imageInput := &ec2.CreateImageInput{
InstanceId: aws.String(instanceID),
Name: aws.String(formattedName),
TagSpecifications: []types.TagSpecification{
{
ResourceType: types.ResourceType("image"),
Tags: []types.Tag{
{
Key: aws.String("DeployedBy"),
Value: aws.String("turbo-deploy"),
},
},
},
},
}
result, err := ec2Client.CreateSnapshot(context.Background(), snapshotInput)
result, err := ec2Client.CreateImage(context.Background(), imageInput)
if err != nil {
log.Printf("failed to create snapshot for instance %s: %v", instanceID, err)
log.Printf("failed to create image for instance %s: %v", instanceID, err)
return "", err
}

log.Printf("Snapshot for instance %s created successfully: %s", instanceID, aws.ToString(result.SnapshotId))
return aws.ToString(result.SnapshotId), nil
log.Printf("Image for instance %s created successfully: %s", instanceID, aws.ToString(result.ImageId))
return aws.ToString(result.ImageId), nil
}

func GetAvailableAmis(amilist []string) ([]string, error) {
// check if an image for that instance already exists
filter := []types.Filter{
{
Name: aws.String("is-public"),
Values: []string{"false"},
},
{
Name: aws.String("tag:DeployedBy"),
Values: []string{"turbo-deploy"},
},
}

imageResult, err := getImage(filter)
if err != nil {
log.Printf("failed to retrieve images: %v", err)
}

// if it exists grab it
if len(imageResult.Images) == 0 {
log.Printf("No images returned for extra listing")
} else {
for i := range imageResult.Images {
image := imageResult.Images[i]
log.Printf("Image %s found, appending to the list...", *image.ImageId)
amilist = append(amilist, *image.ImageId)
}
}

return amilist, nil
}

func getImage(filter []types.Filter) (*ec2.DescribeImagesOutput, error) {
describeInstanceImage := &ec2.DescribeImagesInput{
Filters: filter,
}

imageResult, err := ec2Client.DescribeImages(context.Background(), describeInstanceImage)
if err != nil {
return nil, err
}

return imageResult, nil
}

0 comments on commit 2dee671

Please sign in to comment.