Skip to content

Commit

Permalink
Merge pull request #59 from Cameronsplaze/feature-support-multiple-vo…
Browse files Browse the repository at this point in the history
…lumes

[Feature] support multiple volumes
  • Loading branch information
Cameronsplaze authored Nov 4, 2024
2 parents 1b5186c + 352e2f4 commit 8d4e758
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 169 deletions.
34 changes: 23 additions & 11 deletions ContainerManager/leaf_stack/NestedStacks/EcsAsg.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ def __init__(
task_definition: ecs.Ec2TaskDefinition,
ec2_config: dict,
sg_container_traffic: ec2.SecurityGroup,
efs_file_system: efs.FileSystem,
host_access_point: efs.AccessPoint,
efs_file_systems: list[efs.FileSystem],
efs_ap_acl: efs.AccessPoint,
**kwargs,
) -> None:
super().__init__(scope, "EcsAsgNestedStack", **kwargs)
Expand All @@ -60,8 +60,6 @@ def __init__(
assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"),
description="The instance's permissions (HOST of the container)",
)
## Give it root access to the EFS:
efs_file_system.grant_root_access(self.ec2_role)

## Let the instance register itself to a ecs cluster:
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/security-iam-awsmanpol.html#instance-iam-role-permissions
Expand All @@ -75,16 +73,30 @@ def __init__(
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.UserData.html
self.ec2_user_data = ec2.UserData.for_linux() # (Can also set to python, etc. Default bash)

## Mount the EFS volume:
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_efs-readme.html#mounting-the-file-system-using-user-data
# (the first few commands on that page aren't needed. Since we're a optimized ecs image, we have those packages already)
efs_mount_point = "/mnt/efs"
self.ec2_user_data.add_commands(
f'mkdir -p "{efs_mount_point}"',
### Tie all the EFS's to the host:
for efs_file_system in efs_file_systems:
# Mount on host, each has to be unique. (/mnt/efs/Efs-1, /mnt/efs/Efs-2, etc.)
efs_mount_point = f"/mnt/efs/{efs_file_system.node.id}"
### Give it root access to the EFS:
efs_file_system.grant_root_access(self.ec2_role)

### Create a access point for the host:
## Creating an access point:
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_efs.FileSystem.html#addwbraccesswbrpointid-accesspointoptions
## What it returns:
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_efs.AccessPoint.html
host_access_point = efs_file_system.add_access_point("efs-access-point-host", create_acl=efs_ap_acl, path="/")
# NOTE: The docs didn't have 'iam', but you get permission denied without it:
# (You can also mount efs directly by removing the accesspoint flag)
# https://docs.aws.amazon.com/efs/latest/ug/mounting-access-points.html
f'echo "{efs_file_system.file_system_id}:/ {efs_mount_point} efs defaults,tls,iam,_netdev,accesspoint={host_access_point.access_point_id} 0 0" >> /etc/fstab',
self.ec2_user_data.add_commands(
f'mkdir -p "{efs_mount_point}"',
f'echo "{efs_file_system.file_system_id}:/ {efs_mount_point} efs defaults,tls,iam,_netdev,accesspoint={host_access_point.access_point_id} 0 0" >> /etc/fstab',
)
## Actually mount the EFS volumes:
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_efs-readme.html#mounting-the-file-system-using-user-data
# (the first few commands on that page aren't needed. Since we're a optimized ecs image, we have those packages already)
self.ec2_user_data.add_commands(
'mount -a -t efs,nfs4 defaults',
)

Expand Down
102 changes: 0 additions & 102 deletions ContainerManager/leaf_stack/NestedStacks/Efs.py

This file was deleted.

100 changes: 100 additions & 0 deletions ContainerManager/leaf_stack/NestedStacks/Volumes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@

"""
This module contains the Volumes NestedStack class.
"""

from aws_cdk import (
NestedStack,
RemovalPolicy,
aws_ec2 as ec2,
aws_ecs as ecs,
aws_efs as efs,
)
from constructs import Construct



### Nested Stack info:
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.NestedStack.html
class Volumes(NestedStack):
"""
This sets up the persistent storage for the ECS container.
"""
def __init__(
self,
scope: Construct,
vpc: ec2.Vpc,
task_definition: ecs.Ec2TaskDefinition,
container: ecs.ContainerDefinition,
volumes_config: dict,
sg_efs_traffic: ec2.SecurityGroup,
**kwargs,
) -> None:
super().__init__(scope, "VolumesNestedStack", **kwargs)

########################
### EFS FILE SYSTEMS ###
########################

### Settings for ALL access points:
## Create ACL:
# (From the docs, if the `path` above does not exist, you must specify this)
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_efs.AccessPointOptions.html#createacl
self.efs_ap_acl = efs.Acl(owner_gid="1000", owner_uid="1000", permissions="700")
self.efs_file_systems = []
# i: each construct must have a different name inside the for-loop.
for i, volume_config in enumerate(volumes_config, start=1):
if not volume_config["Type"] == "EFS":
continue

efs_file_system = efs.FileSystem(
self,
f"Efs-{i}",
vpc=vpc,
removal_policy=volume_config["_removal_policy"],
security_group=sg_efs_traffic,
allow_anonymous_access=False,
enable_automatic_backups=volume_config["EnableBackups"],
encrypted=True,
## No need to set, only in one AZ/Subnet already. If user increases that
## number, they probably *want* more backups. There's no other reason to:
# one_zone=True,
)
self.efs_file_systems.append(efs_file_system)


## Tell the EFS side that the task can access it:
efs_file_system.grant_read_write(task_definition.task_role)
## (NOTE: There's another grant_root_access in EcsAsg.py ec2-role.
# I just didn't see a way to move it here without moving the role.)

### Create mounts and attach them to the container:
for volume_info in volume_config["Paths"]:
volume_path = volume_info["Path"]
read_only = volume_info["ReadOnly"]
## Create a UNIQUE name, ID of game + (modified) path:
# (Will be something like: `Minecraft-data` or `Valheim-opt-valheim`)
volume_id = f"{container.container_name}{volume_path.replace('/','-')}"
# Another access point, for the container (each volume gets it's own):
access_point = efs_file_system.add_access_point(volume_id, create_acl=self.efs_ap_acl, path=volume_path)
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.TaskDefinition.html#aws_cdk.aws_ecs.TaskDefinition.add_volume
task_definition.add_volume(
name=volume_id,
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.EfsVolumeConfiguration.html
efs_volume_configuration=ecs.EfsVolumeConfiguration(
file_system_id=efs_file_system.file_system_id,
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.AuthorizationConfig.html
authorization_config=ecs.AuthorizationConfig(
access_point_id=access_point.access_point_id,
iam="ENABLED",
),
transit_encryption="ENABLED",
),
)
container.add_mount_points(
ecs.MountPoint(
container_path=volume_path,
source_volume=volume_id,
read_only=read_only,
)
)
2 changes: 1 addition & 1 deletion ContainerManager/leaf_stack/NestedStacks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
from .Container import Container
from .Dashboard import Dashboard
from .EcsAsg import EcsAsg
from .Efs import Efs
from .Volumes import Volumes
from .SecurityGroups import SecurityGroups
from .Watchdog import Watchdog
14 changes: 7 additions & 7 deletions ContainerManager/leaf_stack/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_logical_id(self, element):
if "NestedStackResource" in element.node.id:
match = re.search(r'([a-zA-Z0-9]+)\.NestedStackResource', element.node.id)
if match:
# Returns "EfsNestedStack" instead of "EfsNestedStackEfsNestedStackResource..."
# Returns "VolumesNestedStack" instead of "VolumesNestedStackVolumesNestedStackResource..."
return match.group(1)
# Fail fast. If the logical_id ever changes on a existing stack, you replace everything and might loose data.
raise RuntimeError(f"Could not find 'NestedStackResource' in {element.node.id}. Did a CDK update finally fix NestedStack names?")
Expand Down Expand Up @@ -92,14 +92,14 @@ def __init__(
container_config=config["Container"],
)

### All the info for EFS Stuff
self.efs_nested_stack = NestedStacks.Efs(
### All the info for Volumes Stuff
self.volumes_nested_stack = NestedStacks.Volumes(
self,
description=f"EFS Logic for {construct_id}",
description=f"Volume Logic for {construct_id}",
vpc=base_stack.vpc,
task_definition=self.container_nested_stack.task_definition,
container=self.container_nested_stack.container,
volume_config=config["Volume"],
volumes_config=config["Volumes"],
sg_efs_traffic=self.sg_nested_stack.sg_efs_traffic,
)

Expand All @@ -117,8 +117,8 @@ def __init__(
task_definition=self.container_nested_stack.task_definition,
ec2_config=config["Ec2"],
sg_container_traffic=self.sg_nested_stack.sg_container_traffic,
efs_file_system=self.efs_nested_stack.efs_file_system,
host_access_point=self.efs_nested_stack.host_access_point,
efs_file_systems=self.volumes_nested_stack.efs_file_systems,
efs_ap_acl=self.volumes_nested_stack.efs_ap_acl,
)

### All the info for the Watchdog Stuff
Expand Down
71 changes: 44 additions & 27 deletions ContainerManager/utils/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from aws_cdk import (
Duration,
RemovalPolicy,
aws_sns as sns,
aws_ecs as ecs,
)
Expand Down Expand Up @@ -125,33 +126,49 @@ def _parse_container(config: dict) -> None:


def _parse_volume(config: dict, maturity: str) -> None:
if "Volume" not in config:
config["Volume"] = {}
assert isinstance(config["Volume"], dict)

### KeepOnDelete
if "KeepOnDelete" not in config["Volume"]:
# If the maturity is prod, default to keep the data safe:
config["Volume"]["KeepOnDelete"] = bool(maturity == "prod")
assert isinstance(config["Volume"]["KeepOnDelete"], bool)

### EnableBackups
if "EnableBackups" not in config["Volume"]:
# If the maturity is prod, default to keep the data safe:
config["Volume"]["EnableBackups"] = bool(maturity == "prod")
assert isinstance(config["Volume"]["EnableBackups"], bool)

### Paths
if "Paths" not in config["Volume"]:
config["Volume"]["Paths"] = []
assert isinstance(config["Volume"]["Paths"], list)
for path in config["Volume"]["Paths"]:
if "Path" not in path:
raise_missing_key_error("Volume.Paths[*].Path")
assert isinstance(path["Path"], str)
if "ReadOnly" not in path:
path["ReadOnly"] = False
assert isinstance(path["ReadOnly"], bool)
if "Volumes" not in config:
config["Volumes"] = []
assert isinstance(config["Volumes"], list)

### Parse Each Volume:
for volume in config["Volumes"]:
### Type
if "Type" not in volume:
volume["Type"] = "EFS" # Default to EFS
volume["Type"] = volume["Type"].upper()
assert volume["Type"] in ["EFS"] # Will support 'S3' soon!

## EnableBackups
if "EnableBackups" not in volume:
# If the maturity is prod, default to keep the data safe:
volume["EnableBackups"] = bool(maturity == "prod")
assert isinstance(volume["EnableBackups"], bool)

## KeepOnDelete
if "KeepOnDelete" not in volume:
# If the maturity is prod, default to keep the data safe:
volume["KeepOnDelete"] = bool(maturity == "prod")
assert isinstance(volume["KeepOnDelete"], bool)
# Private var, another good excuse to turn this into a class:
# https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_efs.FileSystem.html
if volume["KeepOnDelete"]:
volume["_removal_policy"] = RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE
else:
volume["_removal_policy"] = RemovalPolicy.DESTROY
del volume["KeepOnDelete"]


## Paths
if "Paths" not in volume:
raise_missing_key_error("Volumes[*].Paths")
assert isinstance(volume["Paths"], list)
for path in volume["Paths"]:
if "Path" not in path:
raise_missing_key_error("Volumes[*].Paths[*].Path")
assert isinstance(path["Path"], str)
if "ReadOnly" not in path:
path["ReadOnly"] = False
assert isinstance(path["ReadOnly"], bool)

def _parse_ec2(config: dict) -> None:
if "Ec2" not in config:
Expand Down
Loading

0 comments on commit 8d4e758

Please sign in to comment.