├── README.md ├── images └── architecture_overview.png ├── infrastructure ├── service.yaml ├── storage.yaml ├── vpc.yaml └── web.yaml └── master.yaml /README.md: -------------------------------------------------------------------------------- 1 | # Deploying Laravel with Amazon ECS, AWS CloudFormation and an Application Load Balancer 2 | 3 | This reference architecture provides a set of YAML templates for deploying a Laravel application (with its crons and workers) to [Amazon EC2 Container Service (Amazon ECS)](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html) with [AWS CloudFormation](https://aws.amazon.com/cloudformation/). 4 | 5 | The architecture features high security and high availability, and can be adjusted based on your hosting budget. All details on Medium [here](https://medium.com) 6 | 7 | ## Overview 8 | 9 | ![infrastructure-overview](images/architecture_overview.png) 10 | 11 | The repository consists of a set of nested templates that deploy the following: 12 | 13 | - A tiered [VPC](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Introduction.html) with public and private subnets, spanning an AWS region. 14 | - A highly available ECS cluster deployed across two [Availability Zones](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html) in an [Auto Scaling](https://aws.amazon.com/autoscaling/) group. 15 | - (optional) A pair of [NAT gateways](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-nat-gateway.html) (one in each zone) to handle outbound traffic. 16 | - The Laravel application and its crons and workers deployed as [ECS services](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html) (Dockerfiles for Laravel and Nginx provided). 17 | - An [Application Load Balancer (ALB)](https://aws.amazon.com/elasticloadbalancing/applicationloadbalancer/) to the public subnets to handle inbound traffic. 18 | - A S3 Bucket and a corresponding CloudFront distribution 19 | - SSL certificates for the CloudFront Distribution and the ALB 20 | - SSL offloading on the ALB so you don't have to manage certificates in your Nginx containers 21 | - All traffic is forced to HTTPS, and traffic to the www subdomain is redirected to the apex domain. 22 | - Centralized container logging with [Amazon CloudWatch Logs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html). 23 | - CloudWatch monitoring alarms for the database and web instances -------------------------------------------------------------------------------- /images/architecture_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/li0nel/laravelaws/1b66b658ba36638a4f2075003b0a3b83f4f444a4/images/architecture_overview.png -------------------------------------------------------------------------------- /infrastructure/service.yaml: -------------------------------------------------------------------------------- 1 | Description: > 2 | This is an example of a long running ECS service that needs to connect to another ECS service (product-service) 3 | via it's load balancer. We use environment variables to pass the URL of the product-service to this one's container(s). 4 | 5 | Parameters: 6 | 7 | VPC: 8 | Description: The VPC that the ECS cluster is deployed to 9 | Type: AWS::EC2::VPC::Id 10 | 11 | Cluster: 12 | Description: Please provide the ECS Cluster ID that this service should run on 13 | Type: String 14 | 15 | DesiredCount: 16 | Description: How many instances of this task should we run across our cluster? 17 | Type: Number 18 | Default: 2 19 | 20 | ListenerHTTP: 21 | Description: The Application Load Balancer listener to register with 22 | Type: String 23 | 24 | ListenerHTTPS: 25 | Description: The Application Load Balancer listener to register with 26 | Type: String 27 | 28 | Path: 29 | Description: The path to register with the Application Load Balancer 30 | Type: String 31 | Default: / 32 | 33 | ECR: 34 | Description: ECR 35 | Type: String 36 | 37 | APPURL: 38 | Description: Laravel environment variable 39 | Type: String 40 | 41 | DBCONNECTION: 42 | Description: Laravel environment variable 43 | Type: String 44 | 45 | DBHOST: 46 | Description: Laravel environment variable 47 | Type: String 48 | 49 | DBPORT: 50 | Description: Laravel environment variable 51 | Type: String 52 | 53 | DBDATABASE: 54 | Description: Laravel environment variable 55 | Type: String 56 | 57 | DBUSERNAME: 58 | Description: Laravel environment variable 59 | Type: String 60 | 61 | DBPASSWORD: 62 | Description: Laravel environment variable 63 | Type: String 64 | 65 | MAILDRIVER: 66 | Description: Laravel environment variable 67 | Type: String 68 | 69 | MAILHOST: 70 | Description: Laravel environment variable 71 | Type: String 72 | 73 | MAILPORT: 74 | Description: Laravel environment variable 75 | Type: String 76 | 77 | MAILUSERNAME: 78 | Description: Laravel environment variable 79 | Type: String 80 | 81 | MAILPASSWORD: 82 | Description: Laravel environment variable 83 | Type: String 84 | 85 | MAILFROMADDRESS: 86 | Description: Laravel environment variable 87 | Type: String 88 | 89 | MAILFROMNAME: 90 | Description: Laravel environment variable 91 | Type: String 92 | 93 | # ELASTICSEARCHHOST: 94 | # Description: Laravel environment variable 95 | # Type: String 96 | # 97 | # ELASTICSEARCHPORT: 98 | # Description: Laravel environment variable 99 | # Type: String 100 | 101 | FILESYSTEMDRIVER: 102 | Description: Laravel environment variable 103 | Type: String 104 | 105 | AWSBUCKET: 106 | Description: Laravel environment variable 107 | Type: String 108 | 109 | QUEUEDRIVER: 110 | Description: Laravel environment variable 111 | Type: String 112 | 113 | QUEUENAME: 114 | Description: Laravel environment variable 115 | Type: String 116 | 117 | Resources: 118 | 119 | Service: 120 | Type: AWS::ECS::Service 121 | DependsOn: 122 | - ListenerRuleHTTPS 123 | Properties: 124 | Cluster: !Ref Cluster 125 | Role: !Ref ServiceRole 126 | DesiredCount: !Ref DesiredCount 127 | TaskDefinition: !Ref TaskDefinition 128 | LoadBalancers: 129 | - ContainerName: nginx 130 | ContainerPort: 80 131 | TargetGroupArn: !Ref TargetGroup 132 | 133 | ServiceWorkers: 134 | Type: AWS::ECS::Service 135 | Properties: 136 | Cluster: !Ref Cluster 137 | DesiredCount: 1 138 | TaskDefinition: !Ref TaskDefinitionWorker 139 | 140 | ServiceCron: 141 | Type: AWS::ECS::Service 142 | Properties: 143 | Cluster: !Ref Cluster 144 | DesiredCount: 1 145 | TaskDefinition: !Ref TaskDefinitionCron 146 | 147 | ServiceRedirect: 148 | Type: AWS::ECS::Service 149 | DependsOn: 150 | - ListenerRuleHTTP 151 | Properties: 152 | Cluster: !Ref Cluster 153 | Role: !Ref ServiceRole 154 | DesiredCount: 1 155 | TaskDefinition: !Ref TaskDefinitionRedirectHTTPtoHTTPS 156 | LoadBalancers: 157 | - ContainerName: nginx-to-https 158 | ContainerPort: 80 159 | TargetGroupArn: !Ref TargetGroupRedirectHTTPSToHTTP 160 | 161 | TaskDefinitionRedirectHTTPtoHTTPS: 162 | Type: AWS::ECS::TaskDefinition 163 | Properties: 164 | Family: nginx-to-https 165 | ContainerDefinitions: 166 | - Name: nginx-to-https 167 | Essential: true 168 | Image: getlionel/nginx-to-https 169 | Memory: 128 170 | PortMappings: 171 | - ContainerPort: 80 172 | 173 | TaskDefinition: 174 | Type: AWS::ECS::TaskDefinition 175 | Properties: 176 | Family: laravel-nginx 177 | ContainerDefinitions: 178 | - Name: nginx 179 | Essential: true 180 | Image: !Join [ ".", [ !Ref "AWS::AccountId", "dkr.ecr", !Ref "AWS::Region", !Join [ ":", [ !Join [ "/", [ "amazonaws.com", !Ref ECR ] ], "nginx" ] ] ] ] 181 | Memory: 128 182 | PortMappings: 183 | - ContainerPort: 80 184 | Links: 185 | - app 186 | LogConfiguration: 187 | LogDriver: awslogs 188 | Options: 189 | awslogs-group: !Ref AWS::StackName 190 | awslogs-region: !Ref AWS::Region 191 | - Name: app 192 | Essential: true 193 | Image: !Join [ ".", [ !Ref "AWS::AccountId", "dkr.ecr", !Ref "AWS::Region", !Join [ ":", [ !Join [ "/", [ "amazonaws.com", !Ref ECR ] ], "laravel" ] ] ] ] 194 | Memory: 128 195 | LogConfiguration: 196 | LogDriver: awslogs 197 | Options: 198 | awslogs-group: !Ref AWS::StackName 199 | awslogs-region: !Ref AWS::Region 200 | Environment: 201 | - Name: APP_NAME 202 | Value: Laravel 203 | - Name: APP_ENV 204 | Value: production 205 | - Name: APP_DEBUG 206 | Value: false 207 | - Name: APP_LOG_LEVEL 208 | Value: error 209 | - Name: APP_KEY 210 | Value: base64:h2ASblVGbCXbC1buJ8KToZkKIEY69GSiutkAeGo77B0= 211 | - Name: APP_URL 212 | Value: !Ref APPURL 213 | - Name: DB_CONNECTION 214 | Value: !Ref DBCONNECTION 215 | - Name: DB_HOST 216 | Value: !Ref DBHOST 217 | - Name: DB_PORT 218 | Value: !Ref DBPORT 219 | - Name: DB_DATABASE 220 | Value: !Ref DBDATABASE 221 | - Name: DB_USERNAME 222 | Value: !Ref DBUSERNAME 223 | - Name: DB_PASSWORD 224 | Value: !Ref DBPASSWORD 225 | - Name: CACHE_DRIVER 226 | Value: file 227 | - Name: SESSION_DRIVER 228 | Value: database 229 | - Name: MAIL_DRIVER 230 | Value: !Ref MAILDRIVER 231 | - Name: MAIL_HOST 232 | Value: !Ref MAILHOST 233 | - Name: MAIL_PORT 234 | Value: !Ref MAILPORT 235 | - Name: MAIL_USERNAME 236 | Value: !Ref MAILUSERNAME 237 | - Name: MAIL_PASSWORD 238 | Value: !Ref MAILPASSWORD 239 | - Name: MAIL_FROM_ADDRESS 240 | Value: !Ref MAILFROMADDRESS 241 | - Name: MAIL_FROM_NAME 242 | Value: !Ref MAILFROMNAME 243 | # - Name: ELASTICSEARCH_HOST 244 | # Value: !Ref ELASTICSEARCHHOST 245 | # - Name: ELASTICSEARCH_PORT 246 | # Value: !Ref ELASTICSEARCHPORT 247 | - Name: FILESYSTEM_DRIVER 248 | Value: !Ref FILESYSTEMDRIVER 249 | - Name: AWS_REGION 250 | Value: !Sub ${AWS::Region} 251 | - Name: AWS_BUCKET 252 | Value: !Ref AWSBUCKET 253 | - Name: QUEUE_DRIVER 254 | Value: !Ref QUEUEDRIVER 255 | - Name: AWS_ACCOUNT_ID 256 | Value: !Ref AWS::AccountId 257 | - Name: QUEUE_NAME 258 | Value: !Ref QUEUENAME 259 | 260 | TaskDefinitionWorker: 261 | Type: AWS::ECS::TaskDefinition 262 | Properties: 263 | Family: laravel-workers 264 | ContainerDefinitions: 265 | - Name: app 266 | Essential: true 267 | Image: !Join [ ".", [ !Ref "AWS::AccountId", "dkr.ecr", !Ref "AWS::Region", !Join [ ":", [ !Join [ "/", [ "amazonaws.com", !Ref ECR ] ], "laravel" ] ] ] ] 268 | Command: 269 | - "/bin/sh" 270 | - "-c" 271 | - "php artisan queue:work" 272 | Memory: 128 273 | LogConfiguration: 274 | LogDriver: awslogs 275 | Options: 276 | awslogs-group: !Ref AWS::StackName 277 | awslogs-region: !Ref AWS::Region 278 | Environment: 279 | - Name: APP_NAME 280 | Value: Laravel 281 | - Name: APP_ENV 282 | Value: production 283 | - Name: APP_DEBUG 284 | Value: false 285 | - Name: APP_LOG_LEVEL 286 | Value: error 287 | - Name: APP_KEY 288 | Value: base64:h2ASblVGbCXbC1buJ8KToZkKIEY69GSiutkAeGo77B0= 289 | - Name: APP_URL 290 | Value: !Ref APPURL 291 | - Name: DB_CONNECTION 292 | Value: !Ref DBCONNECTION 293 | - Name: DB_HOST 294 | Value: !Ref DBHOST 295 | - Name: DB_PORT 296 | Value: !Ref DBPORT 297 | - Name: DB_DATABASE 298 | Value: !Ref DBDATABASE 299 | - Name: DB_USERNAME 300 | Value: !Ref DBUSERNAME 301 | - Name: DB_PASSWORD 302 | Value: !Ref DBPASSWORD 303 | - Name: CACHE_DRIVER 304 | Value: file 305 | - Name: SESSION_DRIVER 306 | Value: database 307 | - Name: MAIL_DRIVER 308 | Value: !Ref MAILDRIVER 309 | - Name: MAIL_HOST 310 | Value: !Ref MAILHOST 311 | - Name: MAIL_PORT 312 | Value: !Ref MAILPORT 313 | - Name: MAIL_USERNAME 314 | Value: !Ref MAILUSERNAME 315 | - Name: MAIL_PASSWORD 316 | Value: !Ref MAILPASSWORD 317 | - Name: MAIL_FROM_ADDRESS 318 | Value: !Ref MAILFROMADDRESS 319 | - Name: MAIL_FROM_NAME 320 | Value: !Ref MAILFROMNAME 321 | # - Name: ELASTICSEARCH_HOST 322 | # Value: !Ref ELASTICSEARCHHOST 323 | # - Name: ELASTICSEARCH_PORT 324 | # Value: !Ref ELASTICSEARCHPORT 325 | - Name: FILESYSTEM_DRIVER 326 | Value: !Ref FILESYSTEMDRIVER 327 | - Name: AWS_REGION 328 | Value: !Sub ${AWS::Region} 329 | - Name: AWS_BUCKET 330 | Value: !Ref AWSBUCKET 331 | - Name: QUEUE_DRIVER 332 | Value: !Ref QUEUEDRIVER 333 | - Name: AWS_ACCOUNT_ID 334 | Value: !Ref AWS::AccountId 335 | - Name: QUEUE_NAME 336 | Value: !Ref QUEUENAME 337 | 338 | TaskDefinitionCron: 339 | Type: AWS::ECS::TaskDefinition 340 | Properties: 341 | Family: laravel-cron 342 | ContainerDefinitions: 343 | - Name: app 344 | Essential: true 345 | Image: !Join [ ".", [ !Ref "AWS::AccountId", "dkr.ecr", !Ref "AWS::Region", !Join [ ":", [ !Join [ "/", [ "amazonaws.com", !Ref ECR ] ], "laravel" ] ] ] ] 346 | EntryPoint: 347 | - /bin/bash 348 | - -c 349 | Command: 350 | - env /bin/bash -o posix -c 'export -p' > /etc/cron.d/project_env.sh && chmod +x /etc/cron.d/project_env.sh && crontab /etc/cron.d/artisan-schedule-run && cron && tail -f /var/log/cron.log 351 | Memory: 128 352 | LogConfiguration: 353 | LogDriver: awslogs 354 | Options: 355 | awslogs-group: !Ref AWS::StackName 356 | awslogs-region: !Ref AWS::Region 357 | Environment: 358 | - Name: APP_NAME 359 | Value: Laravel 360 | - Name: APP_ENV 361 | Value: production 362 | - Name: APP_DEBUG 363 | Value: false 364 | - Name: APP_LOG_LEVEL 365 | Value: error 366 | - Name: APP_KEY 367 | Value: base64:h2ASblVGbCXbC1buJ8KToZkKIEY69GSiutkAeGo77B0= 368 | - Name: APP_URL 369 | Value: !Ref APPURL 370 | - Name: DB_CONNECTION 371 | Value: !Ref DBCONNECTION 372 | - Name: DB_HOST 373 | Value: !Ref DBHOST 374 | - Name: DB_PORT 375 | Value: !Ref DBPORT 376 | - Name: DB_DATABASE 377 | Value: !Ref DBDATABASE 378 | - Name: DB_USERNAME 379 | Value: !Ref DBUSERNAME 380 | - Name: DB_PASSWORD 381 | Value: !Ref DBPASSWORD 382 | - Name: CACHE_DRIVER 383 | Value: file 384 | - Name: SESSION_DRIVER 385 | Value: database 386 | - Name: MAIL_DRIVER 387 | Value: !Ref MAILDRIVER 388 | - Name: MAIL_HOST 389 | Value: !Ref MAILHOST 390 | - Name: MAIL_PORT 391 | Value: !Ref MAILPORT 392 | - Name: MAIL_USERNAME 393 | Value: !Ref MAILUSERNAME 394 | - Name: MAIL_PASSWORD 395 | Value: !Ref MAILPASSWORD 396 | - Name: MAIL_FROM_ADDRESS 397 | Value: !Ref MAILFROMADDRESS 398 | - Name: MAIL_FROM_NAME 399 | Value: !Ref MAILFROMNAME 400 | # - Name: ELASTICSEARCH_HOST 401 | # Value: !Ref ELASTICSEARCHHOST 402 | # - Name: ELASTICSEARCH_PORT 403 | # Value: !Ref ELASTICSEARCHPORT 404 | - Name: FILESYSTEM_DRIVER 405 | Value: !Ref FILESYSTEMDRIVER 406 | - Name: AWS_REGION 407 | Value: !Sub ${AWS::Region} 408 | - Name: AWS_BUCKET 409 | Value: !Ref AWSBUCKET 410 | - Name: QUEUE_DRIVER 411 | Value: !Ref QUEUEDRIVER 412 | - Name: QUEUE_NAME 413 | Value: !Ref QUEUENAME 414 | 415 | CloudWatchLogsGroup: 416 | Type: AWS::Logs::LogGroup 417 | Properties: 418 | LogGroupName: !Ref AWS::StackName 419 | RetentionInDays: 365 420 | 421 | TargetGroup: 422 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 423 | Properties: 424 | VpcId: !Ref VPC 425 | Port: 80 426 | Protocol: HTTP 427 | Matcher: 428 | HttpCode: 200-301 429 | HealthCheckIntervalSeconds: 10 430 | HealthCheckPath: / 431 | HealthCheckProtocol: HTTP 432 | HealthCheckTimeoutSeconds: 5 433 | HealthyThresholdCount: 2 434 | 435 | TargetGroupRedirectHTTPSToHTTP: 436 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 437 | Properties: 438 | VpcId: !Ref VPC 439 | Port: 80 440 | Protocol: HTTP 441 | Matcher: 442 | HttpCode: 200-301 443 | HealthCheckIntervalSeconds: 10 444 | HealthCheckPath: / 445 | HealthCheckProtocol: HTTP 446 | HealthCheckTimeoutSeconds: 5 447 | HealthyThresholdCount: 2 448 | 449 | ListenerRuleHTTP: 450 | Type: AWS::ElasticLoadBalancingV2::ListenerRule 451 | Properties: 452 | ListenerArn: !Ref ListenerHTTP 453 | Priority: 1 454 | Conditions: 455 | - Field: path-pattern 456 | Values: 457 | - !Ref Path 458 | Actions: 459 | - TargetGroupArn: !Ref TargetGroupRedirectHTTPSToHTTP 460 | Type: forward 461 | 462 | ListenerRuleHTTPS: 463 | Type: AWS::ElasticLoadBalancingV2::ListenerRule 464 | Properties: 465 | ListenerArn: !Ref ListenerHTTPS 466 | Priority: 1 467 | Conditions: 468 | - Field: path-pattern 469 | Values: 470 | - !Ref Path 471 | Actions: 472 | - TargetGroupArn: !Ref TargetGroup 473 | Type: forward 474 | 475 | # This IAM Role grants the service access to register/unregister with the 476 | # Application Load Balancer (ALB). It is based on the default documented here: 477 | # http://docs.aws.amazon.com/AmazonECS/latest/developerguide/service_IAM_role.html 478 | ServiceRole: 479 | Type: AWS::IAM::Role 480 | Properties: 481 | RoleName: !Sub ecs-service-${AWS::StackName} 482 | Path: / 483 | AssumeRolePolicyDocument: | 484 | { 485 | "Statement": [{ 486 | "Effect": "Allow", 487 | "Principal": { "Service": [ "ecs.amazonaws.com" ]}, 488 | "Action": [ "sts:AssumeRole" ] 489 | }] 490 | } 491 | Policies: 492 | - PolicyName: !Sub ecs-service-${AWS::StackName} 493 | PolicyDocument: 494 | { 495 | "Version": "2012-10-17", 496 | "Statement": [{ 497 | "Effect": "Allow", 498 | "Action": [ 499 | "ec2:AuthorizeSecurityGroupIngress", 500 | "ec2:Describe*", 501 | "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", 502 | "elasticloadbalancing:Describe*", 503 | "elasticloadbalancing:RegisterInstancesWithLoadBalancer", 504 | "elasticloadbalancing:DeregisterTargets", 505 | "elasticloadbalancing:DescribeTargetGroups", 506 | "elasticloadbalancing:DescribeTargetHealth", 507 | "elasticloadbalancing:RegisterTargets" 508 | ], 509 | "Resource": "*" 510 | }] 511 | } 512 | 513 | # ElasticSearch: 514 | # Type: AWS::Elasticsearch::Domain 515 | # Properties: 516 | # DomainName: !Sub ${AWS::StackName}-es 517 | # ElasticsearchVersion: 5.5 518 | # ElasticsearchClusterConfig: 519 | # InstanceType: t2.small.elasticsearch 520 | # ZoneAwarenessEnabled: false 521 | # InstanceCount: 1 522 | # EBSOptions: 523 | # EBSEnabled: true 524 | # VolumeSize: 10 525 | # AccessPolicies: 526 | # Version: 2012-10-17 527 | # Statement: 528 | # - Effect: Allow 529 | # Principal: 530 | # AWS: "*" 531 | # Action: 532 | # - es:ESHttpDelete 533 | # - es:ESHttpGet 534 | # - es:ESHttpHead 535 | # - es:ESHttpPost 536 | # - es:ESHttpPut 537 | # Resource: !Sub arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${AWS::StackName}-es/* 538 | # Condition: 539 | # IpAddress: 540 | # aws:SourceIp: 541 | # - !GetAtt VPC.Outputs.NatGateway1EIP 542 | # - !GetAtt VPC.Outputs.NatGateway2EIP 543 | 544 | -------------------------------------------------------------------------------- /infrastructure/storage.yaml: -------------------------------------------------------------------------------- 1 | Description: A basic CloudFormation template for an RDS Aurora cluster. 2 | 3 | Parameters: 4 | 5 | DatabaseInstanceType: 6 | Default: db.t2.small 7 | Description: The instance type to use for the database. 8 | Type: String 9 | 10 | DatabasePassword: 11 | AllowedPattern: "[a-zA-Z0-9]+" 12 | ConstraintDescription: must contain only alphanumeric characters. 13 | Description: The database admin account password. 14 | MaxLength: '41' 15 | MinLength: '8' 16 | NoEcho: 'true' 17 | Type: String 18 | 19 | DatabaseUsername: 20 | AllowedPattern: "[a-zA-Z0-9]+" 21 | ConstraintDescription: must contain only alphanumeric characters. 22 | Description: The database admin account user name. 23 | MaxLength: '16' 24 | MinLength: '1' 25 | Type: String 26 | 27 | DatabaseBackupRetentionPeriod: 28 | Type: String 29 | Default: 14 30 | Description: The database backup retention period in days. 31 | 32 | DatabaseSubnets: 33 | Description: The subnets to place database instances in. 34 | Type: CommaDelimitedList 35 | 36 | DatabaseSecurityGroup: 37 | Type: String 38 | Description: Security groups to apply to the RDS cluster. 39 | 40 | DatabaseName: 41 | Type: String 42 | Description: Database name 43 | 44 | Resources: 45 | KmsKey: 46 | Type: AWS::KMS::Key 47 | Properties: 48 | Description: !Sub KMS Key for our ${AWS::StackName} DB 49 | KeyPolicy: 50 | Id: !Ref AWS::StackName 51 | Version: "2012-10-17" 52 | Statement: 53 | - 54 | Sid: "Allow administration of the key" 55 | Effect: "Allow" 56 | Action: 57 | - kms:Create* 58 | - kms:Describe* 59 | - kms:Enable* 60 | - kms:List* 61 | - kms:Put* 62 | - kms:Update* 63 | - kms:Revoke* 64 | - kms:Disable* 65 | - kms:Get* 66 | - kms:Delete* 67 | - kms:ScheduleKeyDeletion 68 | - kms:CancelKeyDeletion 69 | Principal: 70 | AWS: !Ref AWS::AccountId 71 | Resource: '*' 72 | - 73 | Sid: "Allow use of the key" 74 | Effect: "Allow" 75 | Principal: 76 | AWS: !Ref AWS::AccountId 77 | Action: 78 | - "kms:Encrypt" 79 | - "kms:Decrypt" 80 | - "kms:ReEncrypt*" 81 | - "kms:GenerateDataKey*" 82 | - "kms:DescribeKey" 83 | Resource: "*" 84 | 85 | DatabaseSubnetGroup: 86 | Type: AWS::RDS::DBSubnetGroup 87 | Properties: 88 | DBSubnetGroupDescription: CloudFormation managed DB subnet group. 89 | SubnetIds: !Ref DatabaseSubnets 90 | 91 | DatabaseCluster: 92 | Type: AWS::RDS::DBCluster 93 | Properties: 94 | Engine: aurora 95 | DatabaseName: !Ref DatabaseName 96 | MasterUsername: !Ref DatabaseUsername 97 | MasterUserPassword: !Ref DatabasePassword 98 | BackupRetentionPeriod: 7 99 | PreferredBackupWindow: 01:00-02:30 100 | PreferredMaintenanceWindow: mon:03:00-mon:04:00 101 | DBSubnetGroupName: !Ref DatabaseSubnetGroup 102 | KmsKeyId: !GetAtt KmsKey.Arn 103 | StorageEncrypted: true 104 | VpcSecurityGroupIds: 105 | - !Ref DatabaseSecurityGroup 106 | 107 | DatabasePrimaryInstance: 108 | Type: AWS::RDS::DBInstance 109 | Properties: 110 | Engine: aurora 111 | DBClusterIdentifier: !Ref DatabaseCluster 112 | DBInstanceClass: !Ref DatabaseInstanceType 113 | DBSubnetGroupName: !Ref DatabaseSubnetGroup 114 | 115 | # DatabaseReplicaInstance: 116 | # Type: AWS::RDS::DBInstance 117 | # DependsOn: DatabasePrimaryInstance 118 | # Properties: 119 | # Engine: aurora 120 | # DBClusterIdentifier: !Ref DatabaseCluster 121 | # DBInstanceClass: !Ref DatabaseInstanceType 122 | # DBSubnetGroupName: !Ref DatabaseSubnetGroup 123 | 124 | AlarmTopic: 125 | Type: AWS::SNS::Topic 126 | Properties: 127 | Subscription: 128 | - Endpoint: hi@getlionel.com 129 | Protocol: email 130 | 131 | DatabasePrimaryCPUAlarm: 132 | Type: AWS::CloudWatch::Alarm 133 | Properties: 134 | AlarmDescription: Primary database CPU utilization is over 80%. 135 | Namespace: AWS/RDS 136 | MetricName: CPUUtilization 137 | Unit: Percent 138 | Statistic: Average 139 | Period: 300 140 | EvaluationPeriods: 2 141 | Threshold: 80 142 | ComparisonOperator: GreaterThanOrEqualToThreshold 143 | Dimensions: 144 | - Name: DBInstanceIdentifier 145 | Value: 146 | Ref: DatabasePrimaryInstance 147 | AlarmActions: 148 | - Ref: AlarmTopic 149 | InsufficientDataActions: 150 | - Ref: AlarmTopic 151 | 152 | # DatabaseReplicaCPUAlarm: 153 | # Type: AWS::CloudWatch::Alarm 154 | # Properties: 155 | # AlarmDescription: Replica database CPU utilization is over 80%. 156 | # Namespace: AWS/RDS 157 | # MetricName: CPUUtilization 158 | # Unit: Percent 159 | # Statistic: Average 160 | # Period: 300 161 | # EvaluationPeriods: 2 162 | # Threshold: 80 163 | # ComparisonOperator: GreaterThanOrEqualToThreshold 164 | # Dimensions: 165 | # - Name: DBInstanceIdentifier 166 | # Value: 167 | # Ref: DatabaseReplicaInstance 168 | # AlarmActions: 169 | # - Ref: AlarmTopic 170 | # InsufficientDataActions: 171 | # - Ref: AlarmTopic 172 | 173 | DatabasePrimaryMemoryAlarm: 174 | Type: AWS::CloudWatch::Alarm 175 | Properties: 176 | AlarmDescription: Primary database freeable memory is under 700MB. 177 | Namespace: AWS/RDS 178 | MetricName: FreeableMemory 179 | Unit: Bytes 180 | Statistic: Average 181 | Period: 300 182 | EvaluationPeriods: 2 183 | Threshold: 700000000 184 | ComparisonOperator: LessThanOrEqualToThreshold 185 | Dimensions: 186 | - Name: DBInstanceIdentifier 187 | Value: 188 | Ref: DatabasePrimaryInstance 189 | AlarmActions: 190 | - Ref: AlarmTopic 191 | InsufficientDataActions: 192 | - Ref: AlarmTopic 193 | 194 | # DatabaseReplicaReplicationAlarm: 195 | # Type: AWS::CloudWatch::Alarm 196 | # Properties: 197 | # AlarmDescription: Database replication latency is over 200ms. 198 | # Namespace: AWS/RDS 199 | # MetricName: AuroraReplicaLag 200 | # Unit: Milliseconds 201 | # Statistic: Average 202 | # Period: 300 203 | # EvaluationPeriods: 2 204 | # Threshold: 200 205 | # ComparisonOperator: GreaterThanOrEqualToThreshold 206 | # Dimensions: 207 | # - Name: DBInstanceIdentifier 208 | # Value: 209 | # Ref: DatabaseReplicaInstance 210 | # AlarmActions: 211 | # - Ref: AlarmTopic 212 | 213 | Bucket: 214 | Type: AWS::S3::Bucket 215 | Properties: 216 | AccessControl: PublicRead 217 | 218 | Outputs: 219 | 220 | EndpointAddress: 221 | Description: DB cluster endpoint address 222 | Value: !GetAtt DatabaseCluster.Endpoint.Address 223 | 224 | EndpointPort: 225 | Description: DB cluster endpoint port 226 | Value: !GetAtt DatabaseCluster.Endpoint.Port 227 | 228 | S3BucketName: 229 | Description: S3 Bucket name 230 | Value: !Ref Bucket 231 | -------------------------------------------------------------------------------- /infrastructure/vpc.yaml: -------------------------------------------------------------------------------- 1 | Description: > 2 | This template deploys a VPC, with a pair of public and private subnets spread 3 | across two Availabilty Zones. It deploys an Internet Gateway, with a default 4 | route on the public subnets. It deploys a pair of NAT Gateways (one in each AZ), 5 | and default routes for them in the private subnets. 6 | 7 | Parameters: 8 | 9 | EnvironmentName: 10 | Description: An environment name that will be prefixed to resource names 11 | Type: String 12 | 13 | VpcCIDR: 14 | Description: Please enter the IP range (CIDR notation) for this VPC 15 | Type: String 16 | Default: 10.192.0.0/16 17 | 18 | PublicSubnet1CIDR: 19 | Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone 20 | Type: String 21 | Default: 10.192.10.0/24 22 | 23 | PublicSubnet2CIDR: 24 | Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone 25 | Type: String 26 | Default: 10.192.11.0/24 27 | 28 | PrivateSubnet1CIDR: 29 | Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone 30 | Type: String 31 | Default: 10.192.20.0/24 32 | 33 | PrivateSubnet2CIDR: 34 | Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone 35 | Type: String 36 | Default: 10.192.21.0/24 37 | 38 | Resources: 39 | 40 | VPC: 41 | Type: AWS::EC2::VPC 42 | Properties: 43 | CidrBlock: !Ref VpcCIDR 44 | Tags: 45 | - Key: Name 46 | Value: !Ref EnvironmentName 47 | 48 | InternetGateway: 49 | Type: AWS::EC2::InternetGateway 50 | Properties: 51 | Tags: 52 | - Key: Name 53 | Value: !Ref EnvironmentName 54 | 55 | InternetGatewayAttachment: 56 | Type: AWS::EC2::VPCGatewayAttachment 57 | Properties: 58 | InternetGatewayId: !Ref InternetGateway 59 | VpcId: !Ref VPC 60 | 61 | PublicSubnet1: 62 | Type: AWS::EC2::Subnet 63 | Properties: 64 | VpcId: !Ref VPC 65 | AvailabilityZone: !Select [ 0, !GetAZs ] 66 | CidrBlock: !Ref PublicSubnet1CIDR 67 | MapPublicIpOnLaunch: true 68 | Tags: 69 | - Key: Name 70 | Value: !Sub ${EnvironmentName} Public Subnet (AZ1) 71 | 72 | PublicSubnet2: 73 | Type: AWS::EC2::Subnet 74 | Properties: 75 | VpcId: !Ref VPC 76 | AvailabilityZone: !Select [ 1, !GetAZs ] 77 | CidrBlock: !Ref PublicSubnet2CIDR 78 | MapPublicIpOnLaunch: true 79 | Tags: 80 | - Key: Name 81 | Value: !Sub ${EnvironmentName} Public Subnet (AZ2) 82 | 83 | PrivateSubnet1: 84 | Type: AWS::EC2::Subnet 85 | Properties: 86 | VpcId: !Ref VPC 87 | AvailabilityZone: !Select [ 0, !GetAZs ] 88 | CidrBlock: !Ref PrivateSubnet1CIDR 89 | MapPublicIpOnLaunch: false 90 | Tags: 91 | - Key: Name 92 | Value: !Sub ${EnvironmentName} Private Subnet (AZ1) 93 | 94 | PrivateSubnet2: 95 | Type: AWS::EC2::Subnet 96 | Properties: 97 | VpcId: !Ref VPC 98 | AvailabilityZone: !Select [ 1, !GetAZs ] 99 | CidrBlock: !Ref PrivateSubnet2CIDR 100 | MapPublicIpOnLaunch: false 101 | Tags: 102 | - Key: Name 103 | Value: !Sub ${EnvironmentName} Private Subnet (AZ2) 104 | 105 | NatGateway1EIP: 106 | Type: AWS::EC2::EIP 107 | DependsOn: InternetGatewayAttachment 108 | Properties: 109 | Domain: vpc 110 | 111 | NatGateway2EIP: 112 | Type: AWS::EC2::EIP 113 | DependsOn: InternetGatewayAttachment 114 | Properties: 115 | Domain: vpc 116 | 117 | NatGateway1: 118 | Type: AWS::EC2::NatGateway 119 | Properties: 120 | AllocationId: !GetAtt NatGateway1EIP.AllocationId 121 | SubnetId: !Ref PublicSubnet1 122 | 123 | NatGateway2: 124 | Type: AWS::EC2::NatGateway 125 | Properties: 126 | AllocationId: !GetAtt NatGateway2EIP.AllocationId 127 | SubnetId: !Ref PublicSubnet2 128 | 129 | PublicRouteTable: 130 | Type: AWS::EC2::RouteTable 131 | Properties: 132 | VpcId: !Ref VPC 133 | Tags: 134 | - Key: Name 135 | Value: !Sub ${EnvironmentName} Public Routes 136 | 137 | DefaultPublicRoute: 138 | Type: AWS::EC2::Route 139 | DependsOn: InternetGatewayAttachment 140 | Properties: 141 | RouteTableId: !Ref PublicRouteTable 142 | DestinationCidrBlock: 0.0.0.0/0 143 | GatewayId: !Ref InternetGateway 144 | 145 | PublicSubnet1RouteTableAssociation: 146 | Type: AWS::EC2::SubnetRouteTableAssociation 147 | Properties: 148 | RouteTableId: !Ref PublicRouteTable 149 | SubnetId: !Ref PublicSubnet1 150 | 151 | PublicSubnet2RouteTableAssociation: 152 | Type: AWS::EC2::SubnetRouteTableAssociation 153 | Properties: 154 | RouteTableId: !Ref PublicRouteTable 155 | SubnetId: !Ref PublicSubnet2 156 | 157 | PrivateRouteTable1: 158 | Type: AWS::EC2::RouteTable 159 | Properties: 160 | VpcId: !Ref VPC 161 | Tags: 162 | - Key: Name 163 | Value: !Sub ${EnvironmentName} Private Routes (AZ1) 164 | 165 | DefaultPrivateRoute1: 166 | Type: AWS::EC2::Route 167 | Properties: 168 | RouteTableId: !Ref PrivateRouteTable1 169 | DestinationCidrBlock: 0.0.0.0/0 170 | NatGatewayId: !Ref NatGateway1 171 | 172 | PrivateSubnet1RouteTableAssociation: 173 | Type: AWS::EC2::SubnetRouteTableAssociation 174 | Properties: 175 | RouteTableId: !Ref PrivateRouteTable1 176 | SubnetId: !Ref PrivateSubnet1 177 | 178 | PrivateRouteTable2: 179 | Type: AWS::EC2::RouteTable 180 | Properties: 181 | VpcId: !Ref VPC 182 | Tags: 183 | - Key: Name 184 | Value: !Sub ${EnvironmentName} Private Routes (AZ2) 185 | 186 | DefaultPrivateRoute2: 187 | Type: AWS::EC2::Route 188 | Properties: 189 | RouteTableId: !Ref PrivateRouteTable2 190 | DestinationCidrBlock: 0.0.0.0/0 191 | NatGatewayId: !Ref NatGateway2 192 | 193 | PrivateSubnet2RouteTableAssociation: 194 | Type: AWS::EC2::SubnetRouteTableAssociation 195 | Properties: 196 | RouteTableId: !Ref PrivateRouteTable2 197 | SubnetId: !Ref PrivateSubnet2 198 | 199 | # This security group defines who/where is allowed to access the ECS hosts directly. 200 | # By default we're just allowing access from the load balancer. If you want to SSH 201 | # into the hosts, or expose non-load balanced services you can open their ports here. 202 | ECSSecurityGroup: 203 | Type: AWS::EC2::SecurityGroup 204 | Properties: 205 | VpcId: !Ref VPC 206 | GroupDescription: Access to the ECS hosts and the tasks/containers that run on them 207 | SecurityGroupIngress: 208 | # Only allow inbound access to ECS from the ELB 209 | - SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup 210 | IpProtocol: -1 211 | - IpProtocol: tcp 212 | CidrIp: 0.0.0.0/0 213 | FromPort: '22' 214 | ToPort: '22' 215 | Tags: 216 | - Key: Name 217 | Value: !Sub ${EnvironmentName}-ECS-Hosts 218 | 219 | # This security group defines who/where is allowed to access the Application Load Balancer. 220 | # By default, we've opened this up to the public internet (0.0.0.0/0) but can you restrict 221 | # it further if you want. 222 | LoadBalancerSecurityGroup: 223 | Type: AWS::EC2::SecurityGroup 224 | Properties: 225 | VpcId: !Ref VPC 226 | GroupDescription: Access to the load balancer that sits in front of ECS 227 | SecurityGroupIngress: 228 | # Allow access from anywhere to our ECS services 229 | - CidrIp: 0.0.0.0/0 230 | IpProtocol: -1 231 | Tags: 232 | - Key: Name 233 | Value: !Sub ${EnvironmentName}-LoadBalancers 234 | 235 | DBSecurityGroup: 236 | Type: AWS::EC2::SecurityGroup 237 | Properties: 238 | GroupDescription: Open database for access 239 | VpcId: !Ref VPC 240 | SecurityGroupIngress: 241 | - IpProtocol: tcp 242 | FromPort: '3306' 243 | ToPort: '3306' 244 | SourceSecurityGroupId: !Ref ECSSecurityGroup 245 | Tags: 246 | - Key: Name 247 | Value: !Sub ${EnvironmentName}-DB-Host 248 | 249 | Outputs: 250 | 251 | VPC: 252 | Description: A reference to the created VPC 253 | Value: !Ref VPC 254 | 255 | PublicSubnets: 256 | Description: A list of the public subnets 257 | Value: !Join [ ",", [ !Ref PublicSubnet1, !Ref PublicSubnet2 ]] 258 | 259 | PrivateSubnets: 260 | Description: A list of the private subnets 261 | Value: !Join [ ",", [ !Ref PrivateSubnet1, !Ref PrivateSubnet2 ]] 262 | 263 | PublicSubnet1: 264 | Description: A reference to the public subnet in the 1st Availability Zone 265 | Value: !Ref PublicSubnet1 266 | 267 | PublicSubnet2: 268 | Description: A reference to the public subnet in the 2nd Availability Zone 269 | Value: !Ref PublicSubnet2 270 | 271 | PrivateSubnet1: 272 | Description: A reference to the private subnet in the 1st Availability Zone 273 | Value: !Ref PrivateSubnet1 274 | 275 | PrivateSubnet2: 276 | Description: A reference to the private subnet in the 2nd Availability Zone 277 | Value: !Ref PrivateSubnet2 278 | 279 | NatGateway1EIP: 280 | Description: NAT Gateway 1 IP address 281 | Value: !Ref NatGateway1EIP 282 | 283 | NatGateway2EIP: 284 | Description: NAT Gateway 2 IP address 285 | Value: !Ref NatGateway2EIP 286 | 287 | ECSSecurityGroup: 288 | Description: A reference to the security group for EC2 hosts 289 | Value: !Ref ECSSecurityGroup 290 | 291 | LoadBalancerSecurityGroup: 292 | Description: A reference to the security group for load balancers 293 | Value: !Ref LoadBalancerSecurityGroup 294 | 295 | DBSecurityGroup: 296 | Description: A reference to the security group for the database 297 | Value: !Ref DBSecurityGroup -------------------------------------------------------------------------------- /infrastructure/web.yaml: -------------------------------------------------------------------------------- 1 | Description: > 2 | This template deploys an ECS cluster to the provided VPC and subnets 3 | using an Auto Scaling Group 4 | 5 | Parameters: 6 | 7 | EnvironmentName: 8 | Description: An environment name that will be prefixed to resource names 9 | Type: String 10 | 11 | InstanceType: 12 | Description: Which instance type should we use to build the ECS cluster? 13 | Type: String 14 | Default: c4.large 15 | 16 | ClusterSize: 17 | Description: How many ECS hosts do you want to initially deploy? 18 | Type: Number 19 | Default: 4 20 | 21 | VPC: 22 | Description: Choose which VPC this ECS cluster should be deployed to 23 | Type: AWS::EC2::VPC::Id 24 | 25 | PublicSubnets: 26 | Description: Choose which subnets the Application Load Balancer should be deployed to 27 | Type: List 28 | 29 | LBSecurityGroup: 30 | Description: Select the Security Group to apply to the Application Load Balancer 31 | Type: AWS::EC2::SecurityGroup::Id 32 | 33 | LBCertificateArn: 34 | Description: AWS ARN of the SSL certificate to be used by the ALB 35 | Type: String 36 | 37 | PrivateSubnets: 38 | Description: Choose which subnets this ECS cluster should be deployed to 39 | Type: List 40 | 41 | ECSSecurityGroup: 42 | Description: Select the Security Group to use for the ECS cluster hosts 43 | Type: AWS::EC2::SecurityGroup::Id 44 | 45 | S3BucketName: 46 | Description: Name of a S3 Bucket to be granted access to 47 | Type: String 48 | 49 | CloudFrontOAI: 50 | Description: Origin Access Identity to create via the CLI 51 | Type: String 52 | 53 | CFCertificateArn: 54 | Description: CertificateArn to create via the CLI. Must be in region us-east-1 to work with CloudFront 55 | Type: String 56 | 57 | CDNAlias: 58 | Description: DNS Alias for your CloudFront distribution (for example files.laravelaws.com) 59 | Type: String 60 | 61 | Mappings: 62 | 63 | # These are the latest ECS optimized AMIs as of August 2017: 64 | # 65 | # amzn-ami-2017.03.f-amazon-ecs-optimized 66 | # ECS agent: 1.14.4 67 | # Docker: 17.03.2-ce 68 | # ecs-init: 1.14.4-1 69 | # 70 | # You can find the latest available on this page of our documentation: 71 | # http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html 72 | # (note the AMI identifier is region specific) 73 | 74 | AWSRegionToAMI: 75 | us-east-2: 76 | AMI: ami-1c002379 77 | us-east-1: 78 | AMI: ami-9eb4b1e5 79 | us-west-2: 80 | AMI: ami-1d668865 81 | us-west-1: 82 | AMI: ami-4a2c192a 83 | eu-west-2: 84 | AMI: ami-cb1101af 85 | eu-west-1: 86 | AMI: ami-8fcc32f6 87 | eu-central-1: 88 | AMI: ami-0460cb6b 89 | ap-northeast-1: 90 | AMI: ami-b743bed1 91 | ap-southeast-2: 92 | AMI: ami-c1a6bda2 93 | ap-southeast-1: 94 | AMI: ami-9d1f7efe 95 | ca-central-1: 96 | AMI: ami-b677c9d2 97 | 98 | Resources: 99 | 100 | ECSCluster: 101 | Type: AWS::ECS::Cluster 102 | Properties: 103 | ClusterName: !Ref EnvironmentName 104 | 105 | ECSAutoScalingGroup: 106 | Type: AWS::AutoScaling::AutoScalingGroup 107 | Properties: 108 | VPCZoneIdentifier: !Ref PrivateSubnets 109 | LaunchConfigurationName: !Ref ECSLaunchConfiguration 110 | MinSize: !Ref ClusterSize 111 | MaxSize: !Ref ClusterSize 112 | DesiredCapacity: !Ref ClusterSize 113 | Tags: 114 | - Key: Name 115 | Value: !Sub ${EnvironmentName} ECS host 116 | PropagateAtLaunch: true 117 | CreationPolicy: 118 | ResourceSignal: 119 | Timeout: PT15M 120 | UpdatePolicy: 121 | AutoScalingReplacingUpdate: 122 | WillReplace: true 123 | AutoScalingRollingUpdate: 124 | MinInstancesInService: 1 125 | MaxBatchSize: 1 126 | PauseTime: PT15M 127 | SuspendProcesses: 128 | - HealthCheck 129 | - ReplaceUnhealthy 130 | - AZRebalance 131 | - AlarmNotification 132 | - ScheduledActions 133 | WaitOnResourceSignals: true 134 | 135 | ECSLaunchConfiguration: 136 | Type: AWS::AutoScaling::LaunchConfiguration 137 | Properties: 138 | ImageId: !FindInMap [AWSRegionToAMI, !Ref "AWS::Region", AMI] 139 | InstanceType: !Ref InstanceType 140 | SecurityGroups: 141 | - !Ref ECSSecurityGroup 142 | IamInstanceProfile: !Ref ECSInstanceProfile 143 | KeyName: laravelaws 144 | UserData: 145 | "Fn::Base64": !Sub | 146 | #!/bin/bash 147 | yum update -y 148 | yum install -y aws-cfn-bootstrap aws-cli go 149 | echo '{ "credsStore": "ecr-login" }' > ~/.docker/config.json 150 | go get -u github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login 151 | cd /home/ec2-user/go/src/github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login 152 | go build 153 | export PATH=$PATH:/home/ec2-user/go/bin 154 | /opt/aws/bin/cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource ECSLaunchConfiguration 155 | /opt/aws/bin/cfn-signal -e $? --region ${AWS::Region} --stack ${AWS::StackName} --resource ECSAutoScalingGroup 156 | Metadata: 157 | AWS::CloudFormation::Init: 158 | config: 159 | commands: 160 | 01_add_instance_to_cluster: 161 | command: !Sub echo ECS_CLUSTER=${ECSCluster} >> /etc/ecs/ecs.config 162 | files: 163 | "/etc/cfn/cfn-hup.conf": 164 | mode: 000400 165 | owner: root 166 | group: root 167 | content: !Sub | 168 | [main] 169 | stack=${AWS::StackId} 170 | region=${AWS::Region} 171 | 172 | "/etc/cfn/hooks.d/cfn-auto-reloader.conf": 173 | content: !Sub | 174 | [cfn-auto-reloader-hook] 175 | triggers=post.update 176 | path=Resources.ECSLaunchConfiguration.Metadata.AWS::CloudFormation::Init 177 | action=/opt/aws/bin/cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource ECSLaunchConfiguration 178 | 179 | services: 180 | sysvinit: 181 | cfn-hup: 182 | enabled: true 183 | ensureRunning: true 184 | files: 185 | - /etc/cfn/cfn-hup.conf 186 | - /etc/cfn/hooks.d/cfn-auto-reloader.conf 187 | 188 | # This IAM Role is attached to all of the ECS hosts. It is based on the default role 189 | # published here: 190 | # http://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html 191 | # 192 | # You can add other IAM policy statements here to allow access from your ECS hosts 193 | # to other AWS services. Please note that this role will be used by ALL containers 194 | # running on the ECS host. 195 | ECSRole: 196 | Type: AWS::IAM::Role 197 | Properties: 198 | Path: / 199 | RoleName: !Sub ${EnvironmentName}-ECSRole-${AWS::Region} 200 | AssumeRolePolicyDocument: | 201 | { 202 | "Statement": [{ 203 | "Action": "sts:AssumeRole", 204 | "Effect": "Allow", 205 | "Principal": { 206 | "Service": "ec2.amazonaws.com" 207 | } 208 | }] 209 | } 210 | ManagedPolicyArns: 211 | - "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role" 212 | Policies: 213 | - PolicyName: ecs-service 214 | PolicyDocument: | 215 | { 216 | "Statement": [{ 217 | "Effect": "Allow", 218 | "Action": [ 219 | "ecs:CreateCluster", 220 | "ecs:DeregisterContainerInstance", 221 | "ecs:DiscoverPollEndpoint", 222 | "ecs:Poll", 223 | "ecs:RegisterContainerInstance", 224 | "ecs:StartTelemetrySession", 225 | "ecs:Submit*", 226 | "logs:CreateLogStream", 227 | "logs:PutLogEvents", 228 | "ecr:BatchCheckLayerAvailability", 229 | "ecr:BatchGetImage", 230 | "ecr:GetDownloadUrlForLayer", 231 | "ecr:GetAuthorizationToken" 232 | ], 233 | "Resource": "*" 234 | }] 235 | } 236 | - PolicyName: ec2-s3-write-access 237 | PolicyDocument: 238 | Statement: 239 | - Effect: Allow 240 | Action: 241 | - s3:PutObject 242 | - s3:GetBucketAcl 243 | - s3:PutObjectTagging 244 | - s3:ListBucket 245 | - s3:PutObjectAcl 246 | Resource: !Sub arn:aws:s3:::${S3BucketName}/* 247 | - PolicyName: ec2-cloudwatch-write-access 248 | PolicyDocument: 249 | Statement: 250 | - Effect: Allow 251 | Action: 252 | - logs:CreateLogStream 253 | - logs:PutLogEvents 254 | - logs:CreateLogGroup 255 | Resource: "*" 256 | - PolicyName: sqs-read-write-access 257 | PolicyDocument: 258 | Statement: 259 | - Effect: Allow 260 | Action: 261 | - sqs:* 262 | Resource: !GetAtt Queue.Arn 263 | 264 | ECSInstanceProfile: 265 | Type: AWS::IAM::InstanceProfile 266 | Properties: 267 | Path: / 268 | Roles: 269 | - !Ref ECSRole 270 | 271 | ECR: 272 | Type: AWS::ECR::Repository 273 | Properties: 274 | # RepositoryName: !Sub ${AWS::StackName}-nginx 275 | RepositoryPolicyText: 276 | Version: "2012-10-17" 277 | Statement: 278 | - 279 | Sid: AllowPushPull 280 | Effect: Allow 281 | Principal: 282 | AWS: 283 | - !Sub arn:aws:iam::${AWS::AccountId}:role/${ECSRole} 284 | Action: 285 | - "ecr:GetDownloadUrlForLayer" 286 | - "ecr:BatchGetImage" 287 | - "ecr:BatchCheckLayerAvailability" 288 | - "ecr:PutImage" 289 | - "ecr:InitiateLayerUpload" 290 | - "ecr:UploadLayerPart" 291 | - "ecr:CompleteLayerUpload" 292 | 293 | LoadBalancer: 294 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 295 | Properties: 296 | Name: !Ref EnvironmentName 297 | Subnets: !Ref PublicSubnets 298 | SecurityGroups: 299 | - !Ref LBSecurityGroup 300 | Tags: 301 | - Key: Name 302 | Value: !Ref EnvironmentName 303 | 304 | LoadBalancerListenerHTTP: 305 | Type: AWS::ElasticLoadBalancingV2::Listener 306 | Properties: 307 | LoadBalancerArn: !Ref LoadBalancer 308 | Port: 80 309 | Protocol: HTTP 310 | DefaultActions: 311 | - Type: forward 312 | TargetGroupArn: !Ref DefaultTargetGroup 313 | 314 | LoadBalancerListenerHTTPS: 315 | Type: AWS::ElasticLoadBalancingV2::Listener 316 | Properties: 317 | LoadBalancerArn: !Ref LoadBalancer 318 | Port: 443 319 | Protocol: HTTPS 320 | Certificates: 321 | - CertificateArn: !Ref LBCertificateArn 322 | DefaultActions: 323 | - Type: forward 324 | TargetGroupArn: !Ref DefaultTargetGroup 325 | 326 | # We define a default target group here, as this is a mandatory Parameters 327 | # when creating an Application Load Balancer Listener. This is not used, instead 328 | # a target group is created per-service in each service template (../services/*) 329 | DefaultTargetGroup: 330 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 331 | Properties: 332 | Name: !Sub ${EnvironmentName}-default 333 | VpcId: !Ref VPC 334 | Port: 80 335 | Protocol: HTTP 336 | 337 | HTTPCodeTarget5XXTooHighAlarm: 338 | Type: AWS::CloudWatch::Alarm 339 | Properties: 340 | AlarmDescription: Application load balancer receives 5XX HTTP status codes from targets 341 | Namespace: AWS/ApplicationELB 342 | MetricName: HTTPCode_Target_5XX_Count 343 | Statistic: Sum 344 | Period: 60 345 | EvaluationPeriods: 1 346 | ComparisonOperator: GreaterThanThreshold 347 | Threshold: 0 348 | AlarmActions: 349 | - Ref: AlarmTopic 350 | Dimensions: 351 | - Name: LoadBalancer 352 | Value: !GetAtt LoadBalancer.LoadBalancerFullName 353 | 354 | # CloudFrontDistribution: 355 | # Type: AWS::CloudFront::Distribution 356 | # Properties: 357 | # DistributionConfig: 358 | # Origins: 359 | # - DomainName: !Ref S3BucketDNSName 360 | # Id: myS3Origin 361 | # S3OriginConfig: 362 | # OriginAccessIdentity: !Ref CloudFrontOAI 363 | # Enabled: 'true' 364 | # Aliases: 365 | # - !Ref CDNAlias 366 | # DefaultCacheBehavior: 367 | # Compress: 'true' 368 | # AllowedMethods: 369 | # - GET 370 | # - HEAD 371 | # - OPTIONS 372 | # TargetOriginId: myS3Origin 373 | # ForwardedValues: 374 | # QueryString: 'false' 375 | # Cookies: 376 | # Forward: none 377 | # ViewerProtocolPolicy: redirect-to-https 378 | # ViewerCertificate: 379 | # AcmCertificateArn: !Ref CFCertificateArn 380 | # SslSupportMethod: 'sni-only' 381 | 382 | Queue: 383 | Type: AWS::SQS::Queue 384 | 385 | AlarmTopic: 386 | Type: AWS::SNS::Topic 387 | Properties: 388 | Subscription: 389 | - Endpoint: hi@getlionel.com 390 | Protocol: email 391 | 392 | QueueDepthAlarm: 393 | Type: AWS::CloudWatch::Alarm 394 | Properties: 395 | AlarmDescription: Alarm if queue depth grows beyond 10 messages 396 | Namespace: AWS/SQS 397 | MetricName: ApproximateNumberOfMessagesVisible 398 | Dimensions: 399 | - Name: QueueName 400 | Value: !GetAtt Queue.QueueName 401 | Statistic: Sum 402 | Period: 300 403 | EvaluationPeriods: 1 404 | Threshold: 10 405 | ComparisonOperator: GreaterThanThreshold 406 | AlarmActions: 407 | - Ref: AlarmTopic 408 | InsufficientDataActions: 409 | - Ref: AlarmTopic 410 | 411 | # ScaleUpPolicy: 412 | # Type: AWS::AutoScaling::ScalingPolicy 413 | # Properties: 414 | # AdjustmentType: ChangeInCapacity 415 | # AutoScalingGroupName: !Ref ECSAutoScalingGroup 416 | # Cooldown: '1' 417 | # ScalingAdjustment: '1' 418 | 419 | CPUAlarmHigh: 420 | Type: AWS::CloudWatch::Alarm 421 | Properties: 422 | EvaluationPeriods: '1' 423 | Statistic: Average 424 | Threshold: '50' 425 | AlarmDescription: Alarm if CPU too high or metric disappears indicating instance is down 426 | Period: '60' 427 | # AlarmActions: 428 | # - Ref: ScaleUpPolicy 429 | AlarmActions: 430 | - Ref: AlarmTopic 431 | Namespace: AWS/EC2 432 | Dimensions: 433 | - Name: AutoScalingGroupName 434 | Value: !Ref ECSAutoScalingGroup 435 | ComparisonOperator: GreaterThanThreshold 436 | MetricName: CPUUtilization 437 | 438 | # CPUUtilizationLowAlarm: 439 | # Type: AWS::CloudWatch::Alarm 440 | # Properties: 441 | # AlarmDescription: Service is wasting CPU 442 | # Namespace: 'AWS/ECS' 443 | # Dimensions: 444 | # - Name: ClusterName 445 | # Value: 446 | # 'Fn::ImportValue': !Sub '${ParentClusterStack}-Cluster' 447 | # - Name: ServiceName 448 | # Value: !GetAtt 'Service.Name' 449 | # MetricName: CPUUtilization 450 | # ComparisonOperator: LessThanThreshold 451 | # Statistic: Average 452 | # Period: 300 453 | # EvaluationPeriods: 3 454 | # Threshold: 30 455 | # AlarmActions: 456 | # - !Ref ScaleDownPolicy 457 | 458 | Outputs: 459 | 460 | Cluster: 461 | Description: A reference to the ECS cluster 462 | Value: !Ref ECSCluster 463 | 464 | ECR: 465 | Description: ECR 466 | Value: !Ref ECR 467 | 468 | QueueName: 469 | Description: SQS Queue name 470 | Value: !GetAtt Queue.QueueName 471 | 472 | LoadBalancer: 473 | Description: A reference to the Application Load Balancer 474 | Value: !Ref LoadBalancer 475 | 476 | LoadBalancerUrl: 477 | Description: The URL of the ALB 478 | Value: !GetAtt LoadBalancer.DNSName 479 | 480 | ListenerHTTP: 481 | Description: A reference to a port 80 listener 482 | Value: !Ref LoadBalancerListenerHTTP 483 | 484 | ListenerHTTPS: 485 | Description: A reference to a port 443 listener 486 | Value: !Ref LoadBalancerListenerHTTPS 487 | -------------------------------------------------------------------------------- /master.yaml: -------------------------------------------------------------------------------- 1 | Description: > 2 | 3 | This template deploys a VPC, with a pair of public and private subnets spread 4 | across two Availability Zones. It deploys an Internet Gateway, with a default 5 | route on the public subnets, plus two NAT Gateways for the private ones. 6 | It then deploys an Aurora cluster, a public S3 bucket and a corresponding 7 | CloudFront distribution. 8 | Finally, it deploys an ECS cluster with an Application Load Balancer at the front 9 | and one ECR Docker registry to push your Laravel and Nginx images to. 10 | 11 | This template uses the public Docker image getlionel/nginx-to-https to handle 12 | redirection from HTTP to HTTPS within the cluster. 13 | 14 | Full article on Medium here: https://medium.com 15 | 16 | Author: Lionel Martin 17 | 18 | Parameters: 19 | 20 | CertificateArn: 21 | Description: The AWS ARN of the SSL certificate to be used by the load-balancer 22 | Type: String 23 | 24 | CloudFrontOAI: 25 | Description: CloudFront Origin Access Identity 26 | Type: String 27 | 28 | CertificateArnCF: 29 | Description: AWS ARN of the us-east-1 SSL certificate to be used by CloudFront 30 | Type: String 31 | 32 | InstanceType: 33 | Description: EC2 instance types to use in our cluster 34 | Type: String 35 | Default: t2.micro 36 | 37 | ClusterSize: 38 | Description: Number of EC2 instances in our ECS cluster 39 | Type: String 40 | Default: 1 41 | 42 | BaseUrl: 43 | Description: Second level domain name for your application (for example laravelaws.com) 44 | Type: String 45 | 46 | DBMasterPwd: 47 | Description: Postgresql master password 48 | Type: String 49 | 50 | DBInstanceSize: 51 | Description: DB instance size 52 | Type: String 53 | Default: db.t2.small 54 | 55 | MailDriver: 56 | Description: Mail driver (smtp or log) 57 | Type: String 58 | Default: log 59 | 60 | MailHost: 61 | Description: SMTP host 62 | Type: String 63 | Default: "" 64 | 65 | MailPort: 66 | Description: SMTP port 67 | Type: String 68 | Default: "" 69 | 70 | MailUsername: 71 | Description: SMTP password 72 | Type: String 73 | Default: "" 74 | 75 | MailPassword: 76 | Description: SMTP password 77 | Type: String 78 | Default: "" 79 | 80 | Resources: 81 | 82 | VPC: 83 | Type: AWS::CloudFormation::Stack 84 | Properties: 85 | TemplateURL: https://s3.amazonaws.com/laravelaws/infrastructure/vpc.yaml 86 | Parameters: 87 | EnvironmentName: !Ref AWS::StackName 88 | VpcCIDR: 10.180.0.0/16 89 | PublicSubnet1CIDR: 10.180.8.0/21 90 | PublicSubnet2CIDR: 10.180.16.0/21 91 | PrivateSubnet1CIDR: 10.180.24.0/21 92 | PrivateSubnet2CIDR: 10.180.32.0/21 93 | 94 | Storage: 95 | Type: AWS::CloudFormation::Stack 96 | Properties: 97 | TemplateURL: https://s3.amazonaws.com/laravelaws/infrastructure/storage.yaml 98 | Parameters: 99 | DatabaseInstanceType: !Ref DBInstanceSize 100 | DatabasePassword: !Ref DBMasterPwd 101 | DatabaseUsername: laravel 102 | DatabaseSubnets: !GetAtt VPC.Outputs.PrivateSubnets 103 | DatabaseSecurityGroup: !GetAtt VPC.Outputs.DBSecurityGroup 104 | DatabaseName: !Ref AWS::StackName 105 | 106 | Web: 107 | Type: AWS::CloudFormation::Stack 108 | Properties: 109 | TemplateURL: https://s3.amazonaws.com/laravelaws/infrastructure/web.yaml 110 | Parameters: 111 | EnvironmentName: !Ref AWS::StackName 112 | VPC: !GetAtt VPC.Outputs.VPC 113 | PrivateSubnets: !GetAtt VPC.Outputs.PrivateSubnets 114 | PublicSubnets: !GetAtt VPC.Outputs.PublicSubnets 115 | LBCertificateArn: !Ref CertificateArn 116 | InstanceType: !Ref InstanceType 117 | ClusterSize: !Ref ClusterSize 118 | LBSecurityGroup: !GetAtt VPC.Outputs.LoadBalancerSecurityGroup 119 | ECSSecurityGroup: !GetAtt VPC.Outputs.ECSSecurityGroup 120 | S3BucketName: !GetAtt Storage.Outputs.S3BucketName 121 | CloudFrontOAI: !Ref CloudFrontOAI 122 | CFCertificateArn: !Ref CertificateArnCF 123 | CDNAlias: !Join [ ".", [ "files", !Ref BaseUrl ] ] 124 | 125 | WebService: 126 | Type: AWS::CloudFormation::Stack 127 | Properties: 128 | TemplateURL: https://s3.amazonaws.com/laravelaws/infrastructure/service.yaml 129 | Parameters: 130 | VPC: !GetAtt VPC.Outputs.VPC 131 | Cluster: !GetAtt Web.Outputs.Cluster 132 | ECR: !GetAtt Web.Outputs.ECR 133 | ListenerHTTP: !GetAtt Web.Outputs.ListenerHTTP 134 | ListenerHTTPS: !GetAtt Web.Outputs.ListenerHTTPS 135 | DesiredCount: 1 136 | Path: "*" 137 | APPURL: !Join [ "", [ "https://", !Ref BaseUrl ] ] 138 | DBCONNECTION: mysql 139 | DBHOST: !GetAtt Storage.Outputs.EndpointAddress 140 | DBPORT: !GetAtt Storage.Outputs.EndpointPort 141 | DBDATABASE: !Ref AWS::StackName 142 | DBUSERNAME: laravel 143 | DBPASSWORD: !Ref DBMasterPwd 144 | MAILDRIVER: !Ref MailDriver 145 | MAILHOST: !Ref MailHost 146 | MAILPORT: !Ref MailPort 147 | MAILUSERNAME: !Ref MailUsername 148 | MAILPASSWORD: !Ref MailPassword 149 | MAILFROMADDRESS: !Join [ "@", [ "admin", !Ref BaseUrl ] ] 150 | MAILFROMNAME: !Join [ " ", [ "Admin from", !Ref BaseUrl ] ] 151 | FILESYSTEMDRIVER: s3 152 | AWSBUCKET: !GetAtt Storage.Outputs.S3BucketName 153 | QUEUEDRIVER: sqs 154 | QUEUENAME: !GetAtt Web.Outputs.QueueName 155 | 156 | Outputs: 157 | 158 | BucketName: 159 | Description: S3 Bucket Name with public read access 160 | Value: !GetAtt Storage.Outputs.S3BucketName 161 | 162 | QueueName: 163 | Description: SQS queue name 164 | Value: !GetAtt Web.Outputs.QueueName 165 | 166 | DatabaseInstance: 167 | Description: Database instance 168 | Value: !Join [ ":", [ !GetAtt Storage.Outputs.EndpointAddress, !GetAtt Storage.Outputs.EndpointPort ] ] 169 | 170 | ECR: 171 | Description: The ECR where to push your Docker images 172 | Value: !Join [ ".", [ !Ref "AWS::AccountId", "dkr.ecr", !Ref "AWS::Region", !Join [ "/", [ "amazonaws.com", !GetAtt Web.Outputs.ECR ] ] ] ] 173 | 174 | ServiceUrl: 175 | Description: The URL endpoint for the website service 176 | Value: !Join ["", [ !GetAtt Web.Outputs.LoadBalancerUrl, "/" ]] 177 | 178 | # CloudFrontDistribution: 179 | # Description: Domain name for the CloudFront distribution 180 | # Value: !GetAtt CDN.Outputs.CFDistributionDomainName 181 | --------------------------------------------------------------------------------