# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # AWSTemplateFormatVersion: 2010-09-09 Description: >- Amazon ECS Service for Nextcloud with Serverless RDS, EFS & S3 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: 'Deployment Identifiers' Parameters: - DeploymentName - Label: default: 'Nextcloud Configuration' Parameters: - NextCloudAdminUser - NextCloudAdminPassword - NextCloudVersion - Label: default: 'Database Configuration' Parameters: - DbUserName - DbPassword - NextCloudDbName - DbMinCapacity - DbMaxCapacity - Label: default: 'AWS ECS Configuration' Parameters: - EcsTaskCpu - EcsTaskMem - EcsCapacityProvider - EcsMinCapacity - EcsInitialDesiredCapacity - EcsMaxCapacity - EcsTargetCpuUtilization - SuspendAutoScaling - Label: default: 'ElastiCache Redis Configuration' Parameters: - RedisTshirtSize - Label: default: 'VPC Configuration' Parameters: - IsolationLevel - Label: default: 'Custom Domain Configuration (optional)' Parameters: - Domain - Route53Zone Parameters: DeploymentName: Description: Deployment Name (Unique name, no punctuation) Type: String Default: nc-serverless Route53Zone: Description: Route53 Public Hosted Zone ID to configure custom domain Type: String Default: "" Domain: Description: Speficy a full quallified domain name to use with this Nextcloud deployment Type: String Default: "" DbUserName: Description: Database Master Username Type: String AllowedPattern: ^[a-zA-Z0-9]{6,32} Default: nextcloud DbPassword: Description: Database Master Password Type: String NoEcho: True DbMinCapacity: Description: Minimal Aurora Capacity Units Provisioned Type: Number MinValue: 2 MaxValue: 384 Default: 2 DbMaxCapacity: Description: Maxmimal Aurora Capacity Units Provisioned Type: Number MinValue: 2 MaxValue: 384 Default: 8 NextCloudAdminUser: Description: Initial Nextcloud Admin User Type: String AllowedPattern: ^[a-zA-Z0-9]{6,32} Default: ncadmin NextCloudAdminPassword: Description: Initial Nextcloud Admin Password (change after first login) Type: String NoEcho: True NextCloudDbName: Description: Nextcloud Database Name Type: String AllowedPattern: ^[a-zA-Z0-9]{6,32} Default: nextcloud NextCloudVersion: Description: Nextcloud Version to be deployed (no downgrade possible) Type: String Default: 21.0.1 S3SecretRotationSerial: Description: In order to rotate long-term S3 credentials increase by 1 Type: Number Default: 1 IsolationLevel: Description: VPC Isolation Level (see README.md) Type: String Default: Private AllowedValues: - PrivateHA - Private - Public EcsCapacityProvider: Description: Optional select Spot Fargate Tasks for Development Environments Type: String Default: FARGATE AllowedValues: - FARGATE_SPOT - FARGATE EcsTaskCpu: Description: CPU Units to provision per ECS Container Type: Number Default: 1024 AllowedValues: - 256 - 512 - 1024 - 2048 - 4096 EcsTaskMem: Description: Memory to provision per ECS Container (MB) Type: Number Default: 2048 EcsMinCapacity: Description: Minimum number of ECS Containers to provision Type: Number Default: 1 EcsInitialDesiredCapacity: Description: Initial Desired number of ECS Containers to provision Type: Number Default: 1 EcsMaxCapacity: Description: Maximum number of ECS Containers to provision Type: Number Default: 25 EcsTargetCpuUtilization: Description: Threshold to be reached for scaling in/out Type: Number Default: 50 SuspendAutoScaling: Description: Suspend Auto Scaling for Maintanence Type: String Default: false RedisTshirtSize: Description: ElasticCache Redis Node Size Type: String Default: cache.t3.small AllowedValues: - cache.t3.micro - cache.t3.small - cache.t3.medium - cache.m5.large - cache.m5.xlarge - cache.m5.2xlarge - cache.m5.4xlarge - cache.m5.12xlarge - cache.m5.24xlarge - cache.r5.large - cache.r5.xlarge - cache.r5.2xlarge - cache.r5.4xlarge - cache.r5.12xlarge - cache.r5.24xlarge Mappings: Config: Container: Uid: 33 Gid: 0 Permission: "0777" Conditions: CustomDomain: !Not [!And [!Equals [!Ref Domain, ""], !Equals [!Ref Route53Zone, ""]]] PrivateSubnets: !Not [!Equals [!Ref IsolationLevel, Public]] Resources: VpcStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: https://f7o-quickstart.s3.eu-west-1.amazonaws.com/aws-serverless-nextcloud/simplevpc.yaml TimeoutInMinutes: 60 Parameters: EnvironmentName: !Ref DeploymentName IsolationLevel: !Ref IsolationLevel RdsStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: https://f7o-quickstart.s3.eu-west-1.amazonaws.com/aws-serverless-nextcloud/db-cluster.yaml TimeoutInMinutes: 60 Parameters: DbUserName: !Ref DbUserName DbPassword: !Ref DbPassword DbName: nextcloud DbMinCapacity: !Ref DbMinCapacity DbMaxCapacity: !Ref DbMaxCapacity PrivateSubnets: !GetAtt VpcStack.Outputs.PrivateSubnets VpcId: !GetAtt VpcStack.Outputs.VPC FargateSecGroupId: !Ref EcsSecurityGroup EcsCluster: Type: 'AWS::ECS::Cluster' Properties: CapacityProviders: - FARGATE - FARGATE_SPOT Configuration: ExecuteCommandConfiguration: Logging: DEFAULT EcsSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: ECS Security Group VpcId: !GetAtt VpcStack.Outputs.VPC EcsSecurityGroupHTTPinbound: Type: 'AWS::EC2::SecurityGroupIngress' Properties: GroupId: !Ref EcsSecurityGroup IpProtocol: tcp FromPort: '80' ToPort: '80' SourceSecurityGroupId: !Ref ElbSecurityGroup CloudwatchLogsGroup: Type: 'AWS::Logs::LogGroup' Properties: LogGroupName: !Join - '-' - - !Sub ${DeploymentName} - !Ref 'AWS::StackName' RetentionInDays: 14 DataBucket: DeletionPolicy: Retain Type: 'AWS::S3::Bucket' Properties: VersioningConfiguration: Status: Enabled BucketEncryption: ServerSideEncryptionConfiguration: - BucketKeyEnabled: true ServerSideEncryptionByDefault: SSEAlgorithm: AES256 BucketUser: Type: AWS::IAM::User Properties: Policies: - PolicyName: s3-access PolicyDocument: Statement: - Effect: Allow Action: - s3:* Resource: !Sub arn:aws:s3:::${DataBucket} - Effect: Allow Action: - s3:* Resource: !Sub arn:aws:s3:::${DataBucket}/* - Effect: Deny Action: - s3:DeleteBucket* - s3:PutBucketPolicy - s3:PutEncryptionConfiguration Resource: "*" - Effect: Allow Action: - s3:GetBucketLocation Resource: arn:aws:s3:::* BucketUserCredentials: Type: AWS::IAM::AccessKey Properties: Serial: !Ref S3SecretRotationSerial Status: Active UserName: !Ref BucketUser Efs: Type: AWS::EFS::FileSystem Properties: Encrypted: true EfsMountTarget1: Type: AWS::EFS::MountTarget Properties: FileSystemId: Ref: Efs SubnetId: !Select [0, !Split [ ',', !GetAtt VpcStack.Outputs.PrivateSubnets ]] SecurityGroups: - Ref: "EfsSecurityGroup" EfsMountTarget2: Type: AWS::EFS::MountTarget Properties: FileSystemId: Ref: Efs SubnetId: !Select [ 1, !Split [ ',', !GetAtt VpcStack.Outputs.PrivateSubnets ] ] SecurityGroups: - Ref: "EfsSecurityGroup" EfsAPNextcloud: Type: AWS::EFS::AccessPoint Properties: FileSystemId: !Ref Efs RootDirectory: Path: !Sub /${DeploymentName}/nextcloud CreationInfo: OwnerUid: !FindInMap [Config, Container, Uid] OwnerGid: !FindInMap [Config, Container, Gid] Permissions: !FindInMap [Config, Container, Permission] EfsAPConfig: Type: AWS::EFS::AccessPoint Properties: FileSystemId: !Ref Efs RootDirectory: Path: !Sub /${DeploymentName}/config CreationInfo: OwnerUid: !FindInMap [Config, Container, Uid] OwnerGid: !FindInMap [Config, Container, Gid] Permissions: !FindInMap [Config, Container, Permission] EfsAPApps: Type: AWS::EFS::AccessPoint Properties: FileSystemId: !Ref Efs RootDirectory: Path: !Sub /${DeploymentName}/apps CreationInfo: OwnerUid: !FindInMap [Config, Container, Uid] OwnerGid: !FindInMap [Config, Container, Gid] Permissions: !FindInMap [Config, Container, Permission] EfsAPData: Type: AWS::EFS::AccessPoint Properties: FileSystemId: !Ref Efs RootDirectory: Path: !Sub /${DeploymentName}/data CreationInfo: OwnerUid: !FindInMap [Config, Container, Uid] OwnerGid: !FindInMap [Config, Container, Gid] Permissions: !FindInMap [Config, Container, Permission] EfsSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: ECS Security Group VpcId: !GetAtt VpcStack.Outputs.VPC EfsSecurityGroupNFSinbound: Type: 'AWS::EC2::SecurityGroupIngress' Properties: GroupId: !Ref EfsSecurityGroup IpProtocol: tcp FromPort: '2049' ToPort: '2049' SourceSecurityGroupId: !Ref EcsSecurityGroup # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/bind-mounts.html EcsTaskDefinition: Type: 'AWS::ECS::TaskDefinition' Properties: Family: !Join - '' - - !Ref 'AWS::StackName' - '-ecs-nextcloud' NetworkMode: awsvpc RequiresCompatibilities: - "FARGATE" ExecutionRoleArn: !GetAtt ECSTaskExecRole.Arn TaskRoleArn: !GetAtt ECSTaskRole.Arn Cpu: !Ref EcsTaskCpu Memory: !Ref EcsTaskMem ContainerDefinitions: - Name: nextcloud LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref CloudwatchLogsGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: nextcloud Environment: - Name: POSTGRES_DB Value: !Ref NextCloudDbName - Name: POSTGRES_USER Value: !Ref DbUserName - Name: POSTGRES_PASSWORD Value: !Ref DbPassword - Name: POSTGRES_HOST Value: !GetAtt RdsStack.Outputs.EndpointUrl - Name: NEXTCLOUD_TRUSTED_DOMAINS Value: Fn::Sub: - ${Domain} ${ElbDomain} - ElbDomain: !GetAtt ElasticLoadBalancer.DNSName - Name: NEXTCLOUD_ADMIN_USER Value: !Ref NextCloudAdminUser - Name: NEXTCLOUD_ADMIN_PASSWORD Value: !Ref NextCloudAdminPassword - Name: OBJECTSTORE_S3_BUCKET Value: !Ref DataBucket - Name: OBJECTSTORE_S3_REGION Value: !Ref AWS::Region - Name: OBJECTSTORE_S3_KEY Value: !Ref BucketUserCredentials - Name: OBJECTSTORE_S3_SECRET Value: !GetAtt BucketUserCredentials.SecretAccessKey - Name: OVERWRITEPROTOCOL Value: !If [CustomDomain, https, http] - Name: REDIS_HOST Value: !GetAtt RedisReplicationGroup.PrimaryEndPoint.Address - Name: REDIS_PORT Value: !GetAtt RedisReplicationGroup.PrimaryEndPoint.Port PortMappings: - HostPort: 80 Protocol: tcp ContainerPort: 80 MountPoints: - ContainerPath: "/var/www/html" SourceVolume: nextcloud - ContainerPath: "/var/www/html/custom_apps" SourceVolume: apps - ContainerPath: "/var/www/html/config" SourceVolume: config - ContainerPath: "/var/www/html/data" SourceVolume: data Image: !Sub nextcloud:${NextCloudVersion}-apache Essential: true Volumes: - Name: nextcloud EFSVolumeConfiguration: FilesystemId: !Ref Efs AuthorizationConfig: AccessPointId: !Ref EfsAPNextcloud IAM: ENABLED TransitEncryption: ENABLED - Name: apps EFSVolumeConfiguration: FilesystemId: !Ref Efs AuthorizationConfig: AccessPointId: !Ref EfsAPApps IAM: ENABLED TransitEncryption: ENABLED - Name: config EFSVolumeConfiguration: FilesystemId: !Ref Efs AuthorizationConfig: AccessPointId: !Ref EfsAPConfig IAM: ENABLED TransitEncryption: ENABLED - Name: data EFSVolumeConfiguration: FilesystemId: !Ref Efs AuthorizationConfig: AccessPointId: !Ref EfsAPData IAM: ENABLED TransitEncryption: ENABLED AlbCertificate: Condition: CustomDomain Type: AWS::CertificateManager::Certificate Properties: DomainName: !Ref Domain ValidationMethod: DNS DomainValidationOptions: - DomainName: !Ref Domain HostedZoneId: !Ref Route53Zone ElbSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: ELB Security Group VpcId: !GetAtt VpcStack.Outputs.VPC ElbSecurityGroupHTTPSinbound: Type: 'AWS::EC2::SecurityGroupIngress' Properties: GroupId: !Ref ElbSecurityGroup IpProtocol: tcp FromPort: '443' ToPort: '443' CidrIp: 0.0.0.0/0 ElbSecurityGroupHTTPinbound: Type: 'AWS::EC2::SecurityGroupIngress' Properties: GroupId: !Ref ElbSecurityGroup IpProtocol: tcp FromPort: '80' ToPort: '80' CidrIp: 0.0.0.0/0 ElasticLoadBalancer: Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' Properties: Scheme: internet-facing LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds Value: '30' Subnets: !Split [ ',', !GetAtt VpcStack.Outputs.PublicSubnets ] SecurityGroups: - !Ref ElbSecurityGroup Route53AliasRecord: Condition: CustomDomain Type: AWS::Route53::RecordSet Properties: AliasTarget: DNSName: !GetAtt ElasticLoadBalancer.DNSName EvaluateTargetHealth: true HostedZoneId: !GetAtt ElasticLoadBalancer.CanonicalHostedZoneID Comment: Sso Api Gateway HostedZoneId: !Ref Route53Zone Name: !Ref Domain Type: A LoadBalancerListener: Type: 'AWS::ElasticLoadBalancingV2::Listener' Properties: DefaultActions: Fn::If: - CustomDomain - - Type: "redirect" RedirectConfig: Protocol: "HTTPS" Port: 443 Host: "#{host}" Path: "/#{path}" Query: "#{query}" StatusCode: "HTTP_301" - - Type: forward TargetGroupArn: !Ref LoadBalancerTargetGroup LoadBalancerArn: !Ref ElasticLoadBalancer Port: '80' Protocol: HTTP HttpsLoadBalancerListener: Condition: CustomDomain Type: 'AWS::ElasticLoadBalancingV2::Listener' Properties: Certificates: - CertificateArn: !Ref AlbCertificate DefaultActions: - Type: forward TargetGroupArn: !Ref LoadBalancerTargetGroup LoadBalancerArn: !Ref ElasticLoadBalancer Port: '443' Protocol: HTTPS LoadBalancerListenerRule: Type: 'AWS::ElasticLoadBalancingV2::ListenerRule' DependsOn: LoadBalancerListener Properties: Actions: - Type: forward TargetGroupArn: !Ref LoadBalancerTargetGroup Conditions: - Field: "path-pattern" Values: - "/" ListenerArn: !Ref LoadBalancerListener Priority: 1 LoadBalancerTargetGroup: Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' DependsOn: ElasticLoadBalancer Properties: HealthCheckIntervalSeconds: 10 # This might be keeping the Serverless RDS awake HealthCheckPath: /status.php HealthCheckProtocol: HTTP HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 Port: 80 Protocol: HTTP Matcher: HttpCode: 200,400 UnhealthyThresholdCount: 2 VpcId: !GetAtt VpcStack.Outputs.VPC TargetType: ip TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 - Key: stickiness.enabled Value: false EcsService: DependsOn: - LoadBalancerListener - EfsMountTarget1 - EfsMountTarget2 Type: 'AWS::ECS::Service' Properties: Cluster: !Ref EcsCluster DesiredCount: !Ref EcsInitialDesiredCapacity CapacityProviderStrategy: - Base: 1 CapacityProvider: !Ref EcsCapacityProvider Weight: 1 DeploymentConfiguration: MaximumPercent: 100 MinimumHealthyPercent: 0 # Allows service interruption rather than scaling up NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: !If [PrivateSubnets, DISABLED, ENABLED] SecurityGroups: - !Ref EcsSecurityGroup Subnets: !Split [ ',', !If [PrivateSubnets, !GetAtt VpcStack.Outputs.PrivateSubnets, !GetAtt VpcStack.Outputs.PublicSubnets] ] HealthCheckGracePeriodSeconds: 1200 LoadBalancers: - ContainerName: 'nextcloud' ContainerPort: '80' TargetGroupArn: !Ref LoadBalancerTargetGroup SchedulingStrategy: REPLICA TaskDefinition: !Ref EcsTaskDefinition PropagateTags: SERVICE ECSTaskRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - ecs-tasks.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole' Policies: - PolicyName: ecs-service PolicyDocument: Statement: - Effect: Allow Action: - 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer' - 'elasticloadbalancing:DeregisterTargets' - 'elasticloadbalancing:Describe*' - 'elasticloadbalancing:RegisterInstancesWithLoadBalancer' - 'elasticloadbalancing:RegisterTargets' # TODO: understand permissions needed Resource: '*' - Effect: Allow Action: - 'ec2:Describe*' - 'ec2:AuthorizeSecurityGroupIngress' - "ec2:AttachNetworkInterface" - "ec2:CreateNetworkInterface" - "ec2:CreateNetworkInterfacePermission" - "ec2:DeleteNetworkInterface" - "ec2:DeleteNetworkInterfacePermission" - "ec2:Describe*" - "ec2:DetachNetworkInterface" Resource: "*" - Effect: Allow Action: - elasticfilesystem:* Resource: - !Sub arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/${Efs} - !Sub arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:access-point/${EfsAPNextcloud} - !Sub arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:access-point/${EfsAPConfig} - !Sub arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:access-point/${EfsAPApps} - !Sub arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:access-point/${EfsAPData} ECSTaskExecRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - ecs-tasks.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy Policies: - PolicyName: ecs-service PolicyDocument: Statement: - Effect: Allow Action: - elasticfilesystem:* Resource: - !Sub arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/${Efs} - !Sub arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:access-point/${EfsAPNextcloud} - !Sub arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:access-point/${EfsAPConfig} - !Sub arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:access-point/${EfsAPApps} - !Sub arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:access-point/${EfsAPData} - Effect: Allow Action: - "ecr:GetAuthorizationToken" - "ecr:BatchCheckLayerAvailability" - "ecr:GetDownloadUrlForLayer" - "ecr:BatchGetImage" Resource: "*" - Effect: Allow Action: - s3:* Resource: !Sub arn:aws:s3:::${DataBucket} - Effect: Allow Action: - s3:* Resource: !Sub arn:aws:s3:::${DataBucket}/* - Effect: Deny Action: - s3:DeleteBucket* - s3:PutBucket* - s3:PutEncryptionConfiguration - s3:CreateBucket Resource: "*" - Effect: Allow Action: - s3:GetBucketLocation Resource: arn:aws:s3:::* Ec2NcRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore Policies: - PolicyName: ecs-service PolicyDocument: Statement: - Effect: Allow Action: - logs:* - elasticfilesystem:* Resource: '*' InstanceProfile: Type: "AWS::IAM::InstanceProfile" Properties: Path: "/" Roles: - Ref: Ec2NcRole RedisSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: Elasticache Security Group VpcId: !GetAtt VpcStack.Outputs.VPC RedisSecurityGroupinbound: Type: 'AWS::EC2::SecurityGroupIngress' Properties: GroupId: !Ref RedisSecurityGroup IpProtocol: tcp FromPort: '6379' ToPort: '6379' SourceSecurityGroupId: !Ref EcsSecurityGroup RedisSubnetGroup: Type: 'AWS::ElastiCache::SubnetGroup' Properties: Description: Redis Subnet Group SubnetIds: !Split [ ',', !GetAtt VpcStack.Outputs.PrivateSubnets] RedisParameterGroup: Type: AWS::ElastiCache::ParameterGroup Properties: Description: nextcloud param group CacheParameterGroupFamily: redis6.x Properties: cluster-enabled: 'no' RedisReplicationGroup: DeletionPolicy: Snapshot UpdateReplacePolicy: Snapshot Type: AWS::ElastiCache::ReplicationGroup Properties: ReplicationGroupDescription: 'redis cache for nextcloud' AutomaticFailoverEnabled: false NumCacheClusters: 1 MultiAZEnabled: false CacheNodeType: !Ref RedisTshirtSize CacheParameterGroupName: !Ref RedisParameterGroup CacheSubnetGroupName: !Ref RedisSubnetGroup Engine: redis EngineVersion: 6.x # NumNodeGroups: 1 # ReplicasPerNodeGroup: 1 # NodeGroupConfiguration: # - ReplicaCount: 1 PreferredMaintenanceWindow: 'sat:07:00-sat:08:00' SecurityGroupIds: - !GetAtt - RedisSecurityGroup - GroupId SnapshotRetentionLimit: 35 SnapshotWindow: '00:00-03:00' AtRestEncryptionEnabled: true UpdatePolicy: UseOnlineResharding: true NextcloudAutoScalingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: 'sts:AssumeRole' ManagedPolicyArns: - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole' NextcloudAutoScalingTarget: Type: AWS::ApplicationAutoScaling::ScalableTarget Properties: MinCapacity: !Ref EcsMinCapacity MaxCapacity: !Ref EcsMaxCapacity ResourceId: !Join [ '/', [ service, !Ref EcsCluster, !GetAtt EcsService.Name ] ] ScalableDimension: ecs:service:DesiredCount ServiceNamespace: ecs RoleARN: !GetAtt NextcloudAutoScalingRole.Arn SuspendedState: DynamicScalingInSuspended: !Ref SuspendAutoScaling DynamicScalingOutSuspended: !Ref SuspendAutoScaling ScheduledScalingSuspended: !Ref SuspendAutoScaling NextcloudAutoScalingPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: PolicyName: !GetAtt EcsService.Name PolicyType: TargetTrackingScaling ScalingTargetId: !Ref NextcloudAutoScalingTarget TargetTrackingScalingPolicyConfiguration: PredefinedMetricSpecification: PredefinedMetricType: ECSServiceAverageCPUUtilization ScaleInCooldown: 10 ScaleOutCooldown: 10 # Keep things at or lower than a target CPU utilization, for example TargetValue: !Ref EcsTargetCpuUtilization NextcloudCWDashbard: Type: AWS::CloudWatch::Dashboard Properties: DashboardName: !Sub ${DeploymentName}-nextcloud DashboardBody: !Sub | { "widgets": [ { "height": 6, "width": 12, "y": 0, "x": 0, "type": "metric", "properties": { "view": "timeSeries", "stacked": false, "metrics": [ [ "AWS/ECS", "CPUUtilization", "ServiceName", "${EcsService.Name}", "ClusterName", "${EcsCluster}" ] ], "region": "${AWS::Region}", "title": "ECS-CPUUtilization" } }, { "height": 6, "width": 12, "y": 0, "x": 12, "type": "metric", "properties": { "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "stat": "Average", "period": 300, "metrics": [ [ "AWS/ECS", "MemoryUtilization", "ServiceName", "${EcsService.Name}", "ClusterName", "${EcsCluster}" ] ], "title": "ECS-MemoryUtilization" } }, { "height": 6, "width": 12, "y": 6, "x": 0, "type": "metric", "properties": { "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "stat": "Average", "period": 300, "metrics": [ [ "AWS/RDS", "CPUUtilization", "DBClusterIdentifier", "${RdsStack.Outputs.DBIdentifier}" ] ], "title": "RDS-CPUUtilization" } }, { "height": 6, "width": 12, "y": 6, "x": 12, "type": "metric", "properties": { "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "stat": "Average", "period": 300, "metrics": [ [ "AWS/RDS", "DatabaseConnections", "DBClusterIdentifier", "${RdsStack.Outputs.DBIdentifier}" ] ], "title": "DatabaseConnections" } }, { "height": 6, "width": 12, "y": 12, "x": 0, "type": "metric", "properties": { "metrics": [ [ "AWS/EFS", "StorageBytes", "StorageClass", "Standard", "FileSystemId", "${Efs}" ], [ "...", "IA", ".", "." ], [ "...", "Total", ".", "." ] ], "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "stat": "Average", "period": 300, "title": "EFS-StorageBytes" } }, { "height": 6, "width": 12, "y": 12, "x": 12, "type": "metric", "properties": { "metrics": [ [ { "expression": "(m2*100)/m1", "label": "Data write", "id": "e2", "region": "${AWS::Region}" } ], [ { "expression": "(m3*100)/m1", "label": "Data read", "id": "e3", "region": "${AWS::Region}" } ], [ { "expression": "(m4*100)/m1", "label": "Metadata", "id": "e4", "region": "${AWS::Region}" } ], [ "AWS/EFS", "TotalIOBytes", "FileSystemId", "${Efs}", { "id": "m1", "visible": false, "region": "${AWS::Region}" } ], [ "AWS/EFS", "DataWriteIOBytes", "FileSystemId", "${Efs}", { "id": "m2", "visible": false, "region": "${AWS::Region}" } ], [ "AWS/EFS", "DataReadIOBytes", "FileSystemId", "${Efs}", { "id": "m3", "visible": false, "region": "${AWS::Region}" } ], [ "AWS/EFS", "MetadataIOBytes", "FileSystemId", "${Efs}", { "id": "m4", "visible": false, "region": "${AWS::Region}" } ] ], "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "stat": "Sum", "period": 60, "title": "EFS - Throughput by type" } }, { "height": 6, "width": 12, "y": 18, "x": 0, "type": "metric", "properties": { "view": "timeSeries", "stacked": false, "start": "-P14D", "end": "P0D", "region": "${AWS::Region}", "metrics": [ [ "AWS/S3", "BucketSizeBytes", "StorageType", "StandardStorage", "BucketName", "${DataBucket}" ] ], "period": 86400, "stat": "Average", "title": "Data-BucketSizeBytes" } }, { "height": 6, "width": 12, "y": 18, "x": 12, "type": "metric", "properties": { "view": "timeSeries", "stacked": false, "start": "-P14D", "end": "P0D", "region": "${AWS::Region}", "metrics": [ [ "AWS/S3", "NumberOfObjects", "StorageType", "AllStorageTypes", "BucketName", "${DataBucket}", { "period": 86400 } ] ], "period": 86400, "stat": "Average", "title": "DataBucket-NumberOfObjects" } }, { "type": "metric", "x": 0, "y": 24, "width": 12, "height": 6, "properties": { "view": "timeSeries", "stacked": false, "metrics": [ [ "AWS/ElastiCache", "CPUUtilization", "CacheClusterId", "${RedisReplicationGroup}-001", "CacheNodeId", "0001" ] ], "region": "${AWS::Region}", "title": "Redis_CPUUtilization" } }, { "type": "metric", "x": 12, "y": 24, "width": 12, "height": 6, "properties": { "view": "timeSeries", "stacked": false, "metrics": [ [ "AWS/ElastiCache", "DatabaseMemoryUsagePercentage", "CacheClusterId", "${RedisReplicationGroup}-001", "CacheNodeId", "0001" ] ], "region": "${AWS::Region}", "title": "Redis_DatabaseMemoryUsagePercentage" } } ] } Outputs: CloudWatchDashboardUrl: Value: !Sub https://${AWS::Region}.console.aws.amazon.com/cloudwatch/home?region=${AWS::Region}#dashboards:name=${NextcloudCWDashbard} LoadBalancerUrl: Value: !GetAtt ElasticLoadBalancer.DNSName CustomUrl: Condition: CustomDomain Value: !Sub https://${Domain}