├── .gitignore ├── LICENSE.md ├── README.md ├── aws └── cloud-formation │ ├── template.json │ └── template.yml └── makefile /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | aws/cloud-formation/secrets.decrypted.json 3 | .log 4 | secrets.encrypted.txt 5 | .DS_Store 6 | backup/ 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2018 xilution 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Apache License 17 | ============== 18 | 19 | _Version 2.0, January 2004_ 20 | _<>_ 21 | 22 | ### Terms and Conditions for use, reproduction, and distribution 23 | 24 | #### 1. Definitions 25 | 26 | “License” shall mean the terms and conditions for use, reproduction, and 27 | distribution as defined by Sections 1 through 9 of this document. 28 | 29 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 30 | owner that is granting the License. 31 | 32 | “Legal Entity” shall mean the union of the acting entity and all other entities 33 | that control, are controlled by, or are under common control with that entity. 34 | For the purposes of this definition, “control” means **(i)** the power, direct or 35 | indirect, to cause the direction or management of such entity, whether by 36 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 37 | outstanding shares, or **(iii)** beneficial ownership of such entity. 38 | 39 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 40 | permissions granted by this License. 41 | 42 | “Source” form shall mean the preferred form for making modifications, including 43 | but not limited to software source code, documentation source, and configuration 44 | files. 45 | 46 | “Object” form shall mean any form resulting from mechanical transformation or 47 | translation of a Source form, including but not limited to compiled object code, 48 | generated documentation, and conversions to other media types. 49 | 50 | “Work” shall mean the work of authorship, whether in Source or Object form, made 51 | available under the License, as indicated by a copyright notice that is included 52 | in or attached to the work (an example is provided in the Appendix below). 53 | 54 | “Derivative Works” shall mean any work, whether in Source or Object form, that 55 | is based on (or derived from) the Work and for which the editorial revisions, 56 | annotations, elaborations, or other modifications represent, as a whole, an 57 | original work of authorship. For the purposes of this License, Derivative Works 58 | shall not include works that remain separable from, or merely link (or bind by 59 | name) to the interfaces of, the Work and Derivative Works thereof. 60 | 61 | “Contribution” shall mean any work of authorship, including the original version 62 | of the Work and any modifications or additions to that Work or Derivative Works 63 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 64 | by the copyright owner or by an individual or Legal Entity authorized to submit 65 | on behalf of the copyright owner. For the purposes of this definition, 66 | “submitted” means any form of electronic, verbal, or written communication sent 67 | to the Licensor or its representatives, including but not limited to 68 | communication on electronic mailing lists, source code control systems, and 69 | issue tracking systems that are managed by, or on behalf of, the Licensor for 70 | the purpose of discussing and improving the Work, but excluding communication 71 | that is conspicuously marked or otherwise designated in writing by the copyright 72 | owner as “Not a Contribution.” 73 | 74 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 75 | of whom a Contribution has been received by Licensor and subsequently 76 | incorporated within the Work. 77 | 78 | #### 2. Grant of Copyright License 79 | 80 | Subject to the terms and conditions of this License, each Contributor hereby 81 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 82 | irrevocable copyright license to reproduce, prepare Derivative Works of, 83 | publicly display, publicly perform, sublicense, and distribute the Work and such 84 | Derivative Works in Source or Object form. 85 | 86 | #### 3. Grant of Patent License 87 | 88 | Subject to the terms and conditions of this License, each Contributor hereby 89 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 90 | irrevocable (except as stated in this section) patent license to make, have 91 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 92 | such license applies only to those patent claims licensable by such Contributor 93 | that are necessarily infringed by their Contribution(s) alone or by combination 94 | of their Contribution(s) with the Work to which such Contribution(s) was 95 | submitted. If You institute patent litigation against any entity (including a 96 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 97 | Contribution incorporated within the Work constitutes direct or contributory 98 | patent infringement, then any patent licenses granted to You under this License 99 | for that Work shall terminate as of the date such litigation is filed. 100 | 101 | #### 4. Redistribution 102 | 103 | You may reproduce and distribute copies of the Work or Derivative Works thereof 104 | in any medium, with or without modifications, and in Source or Object form, 105 | provided that You meet the following conditions: 106 | 107 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 108 | this License; and 109 | * **(b)** You must cause any modified files to carry prominent notices stating that You 110 | changed the files; and 111 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 112 | all copyright, patent, trademark, and attribution notices from the Source form 113 | of the Work, excluding those notices that do not pertain to any part of the 114 | Derivative Works; and 115 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 116 | Derivative Works that You distribute must include a readable copy of the 117 | attribution notices contained within such NOTICE file, excluding those notices 118 | that do not pertain to any part of the Derivative Works, in at least one of the 119 | following places: within a NOTICE text file distributed as part of the 120 | Derivative Works; within the Source form or documentation, if provided along 121 | with the Derivative Works; or, within a display generated by the Derivative 122 | Works, if and wherever such third-party notices normally appear. The contents of 123 | the NOTICE file are for informational purposes only and do not modify the 124 | License. You may add Your own attribution notices within Derivative Works that 125 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 126 | provided that such additional attribution notices cannot be construed as 127 | modifying the License. 128 | 129 | You may add Your own copyright statement to Your modifications and may provide 130 | additional or different license terms and conditions for use, reproduction, or 131 | distribution of Your modifications, or for any such Derivative Works as a whole, 132 | provided Your use, reproduction, and distribution of the Work otherwise complies 133 | with the conditions stated in this License. 134 | 135 | #### 5. Submission of Contributions 136 | 137 | Unless You explicitly state otherwise, any Contribution intentionally submitted 138 | for inclusion in the Work by You to the Licensor shall be under the terms and 139 | conditions of this License, without any additional terms or conditions. 140 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 141 | any separate license agreement you may have executed with Licensor regarding 142 | such Contributions. 143 | 144 | #### 6. Trademarks 145 | 146 | This License does not grant permission to use the trade names, trademarks, 147 | service marks, or product names of the Licensor, except as required for 148 | reasonable and customary use in describing the origin of the Work and 149 | reproducing the content of the NOTICE file. 150 | 151 | #### 7. Disclaimer of Warranty 152 | 153 | Unless required by applicable law or agreed to in writing, Licensor provides the 154 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 155 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 156 | including, without limitation, any warranties or conditions of TITLE, 157 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 158 | solely responsible for determining the appropriateness of using or 159 | redistributing the Work and assume any risks associated with Your exercise of 160 | permissions under this License. 161 | 162 | #### 8. Limitation of Liability 163 | 164 | In no event and under no legal theory, whether in tort (including negligence), 165 | contract, or otherwise, unless required by applicable law (such as deliberate 166 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 167 | liable to You for damages, including any direct, indirect, special, incidental, 168 | or consequential damages of any character arising as a result of this License or 169 | out of the use or inability to use the Work (including but not limited to 170 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 171 | any and all other commercial damages or losses), even if such Contributor has 172 | been advised of the possibility of such damages. 173 | 174 | #### 9. Accepting Warranty or Additional Liability 175 | 176 | While redistributing the Work or Derivative Works thereof, You may choose to 177 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 178 | other liability obligations and/or rights consistent with this License. However, 179 | in accepting such obligations, You may act only on Your own behalf and on Your 180 | sole responsibility, not on behalf of any other Contributor, and only if You 181 | agree to indemnify, defend, and hold each Contributor harmless for any liability 182 | incurred by, or claims asserted against, such Contributor by reason of your 183 | accepting any such warranty or additional liability. 184 | 185 | _END OF TERMS AND CONDITIONS_ 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xilution-selenium-grid 2 | 3 | * Stand up a Selenium Grid in AWS ECS Fargate using Selenium Grid Host and Node Docker images. 4 | * A configurable CloudFormation template is provide along with a Makefile to simplify the provisioning and deprovisioning process. 5 | * Keeps your secrets safe with KMS. 6 | * Grid accessible through: [http://selenium-grid.your-domain.com:4444](http://selenium-grid.your-domain.com:4444) 7 | * Simply follow the instructions below and you'll have a functioning Selenium Grid stood up in no time. 8 | 9 | ## Inspiration 10 | 11 | * https://github.com/nathanpeck/aws-cloudformation-fargate 12 | * https://github.com/SeleniumHQ/docker-selenium 13 | 14 | ## Assumptions 15 | 16 | * These instructions were created for a Mac environment, but could easily be ported to Linux or Windows. 17 | 18 | ## Set Up 19 | 20 | 1. [Set up an AWS Account](https://aws.amazon.com/). 21 | 1. Install the [AWS CLI](https://aws.amazon.com/cli). 22 | 1. Use [AWS IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html) to create an administrator identity. It's generally bad practice to use your AWS account's root user. 23 | 1. Use [AWS Route53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html) to create a Hosted Zone with a custom domain name. This step is optional. 24 | 1. Use [AWS Key Management System](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html) to create a dev ops key for encrypting secrets. 25 | 1. Add `export AWS_DEV_OPS_KEY_TOKEN=your-aws-dev-ops-kms-key-token` to your `.bash_profile`. Run `source ~/.bash_profile` to add the new environment variable to your current terminal session. This only needs to be done once. 26 | 1. Create a file named `secrets.decrypted.json` in `./aws/cloud-formation/` with the following contents. Replace `your-domain.com.` with your custom domain name. If you do not set up a custom domain name, remove the `RecordSet` resource from the CloudFormation template. 27 | ``` json 28 | [ 29 | { 30 | "ParameterKey": "DomainName", 31 | "ParameterValue": "your-domain.com." 32 | } 33 | ] 34 | ``` 35 | 1. Run `make secrets-devops-encrypt` to encrypt the parameters with your key. 36 | 37 | ## Operating your Selenium Grid 38 | 39 | 1. Run `make provision` to stand up your Selenium Grid. Note that it could take up to 10 minutes to stand up the stack. 40 | * once provisioned, you can view your Selenium Grid Console at: [http://selenium-grid.your-domain.com:4444](http://selenium-grid.your-domain.com:4444) 41 | 1. Run `make deprovision` to tear down your Selenium Grid. 42 | 43 | **Warning!** Only run the grid while needed. When not in use, I highly recommend that you run the deprovision step. 44 | Why pay for your new Selenium Grid when you don't need it. Isn't elasticity great!?! 45 | 46 | ## Contributions 47 | 48 | Pull requests are welcome. If you see an opportunity to improve this repo, please share. 49 | 50 | ## Pay it Forward 51 | 52 | If you find the contents of this repo useful, please share your experiences with the boarder community. Thanks! 53 | 54 | ## Wish List 55 | 56 | [See repo Issues.](https://github.com/xilution/xilution-selenium-grid/issues) 57 | -------------------------------------------------------------------------------- /aws/cloud-formation/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Parameters": { 4 | "ContainerCpu": { 5 | "Type": "Number", 6 | "Default": 256, 7 | "Description": "How much CPU to give the container. 1024 is 1 CPU" 8 | }, 9 | "ContainerMemory": { 10 | "Type": "Number", 11 | "Default": 512, 12 | "Description": "How much memory in megabytes to give the container" 13 | }, 14 | "Path": { 15 | "Type": "String", 16 | "Default": "*", 17 | "Description": "A path on the public load balancer that this service should be connected to. Use * to send all load balancer traffic to this service." 18 | }, 19 | "Priority": { 20 | "Type": "Number", 21 | "Default": 1, 22 | "Description": "The priority for the routing rule added to the load balancer. This only applies if your have multiple services which have been assigned to different paths on the load balancer." 23 | }, 24 | "Role": { 25 | "Type": "String", 26 | "Default": "", 27 | "Description": "(Optional) An IAM role to give the service's containers if the code within needs to access other AWS resources like S3 buckets, DynamoDB tables, etc" 28 | } 29 | }, 30 | "Conditions": { 31 | "HasCustomRole": { 32 | "Fn::Not": [ 33 | { 34 | "Fn::Equals": [ 35 | { 36 | "Ref": "Role" 37 | }, 38 | "" 39 | ] 40 | } 41 | ] 42 | } 43 | }, 44 | "Mappings": { 45 | "SubnetConfig": { 46 | "VPC": { 47 | "CIDR": "10.0.0.0/16" 48 | }, 49 | "PublicOne": { 50 | "CIDR": "10.0.0.0/24" 51 | }, 52 | "PublicTwo": { 53 | "CIDR": "10.0.1.0/24" 54 | }, 55 | "PrivateOne": { 56 | "CIDR": "10.0.2.0/24" 57 | }, 58 | "PrivateTwo": { 59 | "CIDR": "10.0.3.0/24" 60 | } 61 | } 62 | }, 63 | "Resources": { 64 | "VPC": { 65 | "Type": "AWS::EC2::VPC", 66 | "Properties": { 67 | "EnableDnsSupport": true, 68 | "EnableDnsHostnames": true, 69 | "CidrBlock": { 70 | "Fn::FindInMap": [ 71 | "SubnetConfig", 72 | "VPC", 73 | "CIDR" 74 | ] 75 | } 76 | } 77 | }, 78 | "PublicSubnetOne": { 79 | "Type": "AWS::EC2::Subnet", 80 | "Properties": { 81 | "AvailabilityZone": { 82 | "Fn::Select": [ 83 | 0, 84 | { 85 | "Fn::GetAZs": { 86 | "Ref": "AWS::Region" 87 | } 88 | } 89 | ] 90 | }, 91 | "VpcId": { 92 | "Ref": "VPC" 93 | }, 94 | "CidrBlock": { 95 | "Fn::FindInMap": [ 96 | "SubnetConfig", 97 | "PublicOne", 98 | "CIDR" 99 | ] 100 | }, 101 | "MapPublicIpOnLaunch": true 102 | } 103 | }, 104 | "PublicSubnetTwo": { 105 | "Type": "AWS::EC2::Subnet", 106 | "Properties": { 107 | "AvailabilityZone": { 108 | "Fn::Select": [ 109 | 1, 110 | { 111 | "Fn::GetAZs": { 112 | "Ref": "AWS::Region" 113 | } 114 | } 115 | ] 116 | }, 117 | "VpcId": { 118 | "Ref": "VPC" 119 | }, 120 | "CidrBlock": { 121 | "Fn::FindInMap": [ 122 | "SubnetConfig", 123 | "PublicTwo", 124 | "CIDR" 125 | ] 126 | }, 127 | "MapPublicIpOnLaunch": true 128 | } 129 | }, 130 | "PrivateSubnetOne": { 131 | "Type": "AWS::EC2::Subnet", 132 | "Properties": { 133 | "AvailabilityZone": { 134 | "Fn::Select": [ 135 | 0, 136 | { 137 | "Fn::GetAZs": { 138 | "Ref": "AWS::Region" 139 | } 140 | } 141 | ] 142 | }, 143 | "VpcId": { 144 | "Ref": "VPC" 145 | }, 146 | "CidrBlock": { 147 | "Fn::FindInMap": [ 148 | "SubnetConfig", 149 | "PrivateOne", 150 | "CIDR" 151 | ] 152 | } 153 | } 154 | }, 155 | "PrivateSubnetTwo": { 156 | "Type": "AWS::EC2::Subnet", 157 | "Properties": { 158 | "AvailabilityZone": { 159 | "Fn::Select": [ 160 | 1, 161 | { 162 | "Fn::GetAZs": { 163 | "Ref": "AWS::Region" 164 | } 165 | } 166 | ] 167 | }, 168 | "VpcId": { 169 | "Ref": "VPC" 170 | }, 171 | "CidrBlock": { 172 | "Fn::FindInMap": [ 173 | "SubnetConfig", 174 | "PrivateTwo", 175 | "CIDR" 176 | ] 177 | } 178 | } 179 | }, 180 | "InternetGateway": { 181 | "Type": "AWS::EC2::InternetGateway" 182 | }, 183 | "GatewayAttachement": { 184 | "Type": "AWS::EC2::VPCGatewayAttachment", 185 | "Properties": { 186 | "VpcId": { 187 | "Ref": "VPC" 188 | }, 189 | "InternetGatewayId": { 190 | "Ref": "InternetGateway" 191 | } 192 | } 193 | }, 194 | "PublicRouteTable": { 195 | "Type": "AWS::EC2::RouteTable", 196 | "Properties": { 197 | "VpcId": { 198 | "Ref": "VPC" 199 | } 200 | } 201 | }, 202 | "PublicRoute": { 203 | "Type": "AWS::EC2::Route", 204 | "DependsOn": "GatewayAttachement", 205 | "Properties": { 206 | "RouteTableId": { 207 | "Ref": "PublicRouteTable" 208 | }, 209 | "DestinationCidrBlock": "0.0.0.0/0", 210 | "GatewayId": { 211 | "Ref": "InternetGateway" 212 | } 213 | } 214 | }, 215 | "PublicSubnetOneRouteTableAssociation": { 216 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 217 | "Properties": { 218 | "SubnetId": { 219 | "Ref": "PublicSubnetOne" 220 | }, 221 | "RouteTableId": { 222 | "Ref": "PublicRouteTable" 223 | } 224 | } 225 | }, 226 | "PublicSubnetTwoRouteTableAssociation": { 227 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 228 | "Properties": { 229 | "SubnetId": { 230 | "Ref": "PublicSubnetTwo" 231 | }, 232 | "RouteTableId": { 233 | "Ref": "PublicRouteTable" 234 | } 235 | } 236 | }, 237 | "NatGatewayOneAttachment": { 238 | "Type": "AWS::EC2::EIP", 239 | "DependsOn": "GatewayAttachement", 240 | "Properties": { 241 | "Domain": "vpc" 242 | } 243 | }, 244 | "NatGatewayTwoAttachment": { 245 | "Type": "AWS::EC2::EIP", 246 | "DependsOn": "GatewayAttachement", 247 | "Properties": { 248 | "Domain": "vpc" 249 | } 250 | }, 251 | "NatGatewayOne": { 252 | "Type": "AWS::EC2::NatGateway", 253 | "Properties": { 254 | "AllocationId": { 255 | "Fn::GetAtt": [ 256 | "NatGatewayOneAttachment", 257 | "AllocationId" 258 | ] 259 | }, 260 | "SubnetId": { 261 | "Ref": "PublicSubnetOne" 262 | } 263 | } 264 | }, 265 | "NatGatewayTwo": { 266 | "Type": "AWS::EC2::NatGateway", 267 | "Properties": { 268 | "AllocationId": { 269 | "Fn::GetAtt": [ 270 | "NatGatewayTwoAttachment", 271 | "AllocationId" 272 | ] 273 | }, 274 | "SubnetId": { 275 | "Ref": "PublicSubnetTwo" 276 | } 277 | } 278 | }, 279 | "PrivateRouteTableOne": { 280 | "Type": "AWS::EC2::RouteTable", 281 | "Properties": { 282 | "VpcId": { 283 | "Ref": "VPC" 284 | } 285 | } 286 | }, 287 | "PrivateRouteOne": { 288 | "Type": "AWS::EC2::Route", 289 | "Properties": { 290 | "RouteTableId": { 291 | "Ref": "PrivateRouteTableOne" 292 | }, 293 | "DestinationCidrBlock": "0.0.0.0/0", 294 | "NatGatewayId": { 295 | "Ref": "NatGatewayOne" 296 | } 297 | } 298 | }, 299 | "PrivateRouteTableOneAssociation": { 300 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 301 | "Properties": { 302 | "RouteTableId": { 303 | "Ref": "PrivateRouteTableOne" 304 | }, 305 | "SubnetId": { 306 | "Ref": "PrivateSubnetOne" 307 | } 308 | } 309 | }, 310 | "PrivateRouteTableTwo": { 311 | "Type": "AWS::EC2::RouteTable", 312 | "Properties": { 313 | "VpcId": { 314 | "Ref": "VPC" 315 | } 316 | } 317 | }, 318 | "PrivateRouteTwo": { 319 | "Type": "AWS::EC2::Route", 320 | "Properties": { 321 | "RouteTableId": { 322 | "Ref": "PrivateRouteTableTwo" 323 | }, 324 | "DestinationCidrBlock": "0.0.0.0/0", 325 | "NatGatewayId": { 326 | "Ref": "NatGatewayTwo" 327 | } 328 | } 329 | }, 330 | "PrivateRouteTableTwoAssociation": { 331 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 332 | "Properties": { 333 | "RouteTableId": { 334 | "Ref": "PrivateRouteTableTwo" 335 | }, 336 | "SubnetId": { 337 | "Ref": "PrivateSubnetTwo" 338 | } 339 | } 340 | }, 341 | "ECSCluster": { 342 | "Type": "AWS::ECS::Cluster", 343 | "Properties": { 344 | "ClusterName": "selenium-grid" 345 | } 346 | }, 347 | "FargateContainerSecurityGroup": { 348 | "Type": "AWS::EC2::SecurityGroup", 349 | "Properties": { 350 | "GroupDescription": "Access to the Fargate containers", 351 | "VpcId": { 352 | "Ref": "VPC" 353 | } 354 | } 355 | }, 356 | "EcsSecurityGroupIngressFromPublicALB": { 357 | "Type": "AWS::EC2::SecurityGroupIngress", 358 | "Properties": { 359 | "Description": "Ingress from the public ALB", 360 | "GroupId": { 361 | "Ref": "FargateContainerSecurityGroup" 362 | }, 363 | "IpProtocol": -1, 364 | "SourceSecurityGroupId": { 365 | "Ref": "PublicLoadBalancerSG" 366 | } 367 | } 368 | }, 369 | "EcsSecurityGroupIngressFromSelf": { 370 | "Type": "AWS::EC2::SecurityGroupIngress", 371 | "Properties": { 372 | "Description": "Ingress from other containers in the same security group", 373 | "GroupId": { 374 | "Ref": "FargateContainerSecurityGroup" 375 | }, 376 | "IpProtocol": -1, 377 | "SourceSecurityGroupId": { 378 | "Ref": "FargateContainerSecurityGroup" 379 | } 380 | } 381 | }, 382 | "PublicLoadBalancerSG": { 383 | "Type": "AWS::EC2::SecurityGroup", 384 | "Properties": { 385 | "GroupDescription": "Access to the public facing load balancer", 386 | "VpcId": { 387 | "Ref": "VPC" 388 | }, 389 | "SecurityGroupIngress": [ 390 | { 391 | "CidrIp": "0.0.0.0/0", 392 | "IpProtocol": -1 393 | } 394 | ] 395 | } 396 | }, 397 | "PublicLoadBalancer": { 398 | "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", 399 | "Properties": { 400 | "Scheme": "internet-facing", 401 | "LoadBalancerAttributes": [ 402 | { 403 | "Key": "idle_timeout.timeout_seconds", 404 | "Value": "30" 405 | } 406 | ], 407 | "Subnets": [ 408 | { 409 | "Ref": "PublicSubnetOne" 410 | }, 411 | { 412 | "Ref": "PublicSubnetTwo" 413 | } 414 | ], 415 | "SecurityGroups": [ 416 | { 417 | "Ref": "PublicLoadBalancerSG" 418 | } 419 | ] 420 | } 421 | }, 422 | "DummyTargetGroupPublic": { 423 | "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", 424 | "Properties": { 425 | "HealthCheckIntervalSeconds": 6, 426 | "HealthCheckPath": "/", 427 | "HealthCheckProtocol": "HTTP", 428 | "HealthCheckTimeoutSeconds": 5, 429 | "HealthyThresholdCount": 2, 430 | "Name": { 431 | "Fn::Join": [ 432 | "-", 433 | [ 434 | { 435 | "Ref": "AWS::StackName" 436 | }, 437 | "drop-1" 438 | ] 439 | ] 440 | }, 441 | "Port": 4444, 442 | "Protocol": "HTTP", 443 | "UnhealthyThresholdCount": 2, 444 | "VpcId": { 445 | "Ref": "VPC" 446 | } 447 | } 448 | }, 449 | "PublicLoadBalancerListener": { 450 | "Type": "AWS::ElasticLoadBalancingV2::Listener", 451 | "DependsOn": "PublicLoadBalancer", 452 | "Properties": { 453 | "DefaultActions": [ 454 | { 455 | "TargetGroupArn": { 456 | "Ref": "DummyTargetGroupPublic" 457 | }, 458 | "Type": "forward" 459 | } 460 | ], 461 | "LoadBalancerArn": { 462 | "Ref": "PublicLoadBalancer" 463 | }, 464 | "Port": 4444, 465 | "Protocol": "HTTP" 466 | } 467 | }, 468 | "ECSRole": { 469 | "Type": "AWS::IAM::Role", 470 | "Properties": { 471 | "AssumeRolePolicyDocument": { 472 | "Statement": [ 473 | { 474 | "Effect": "Allow", 475 | "Principal": { 476 | "Service": [ 477 | "ecs.amazonaws.com" 478 | ] 479 | }, 480 | "Action": [ 481 | "sts:AssumeRole" 482 | ] 483 | } 484 | ] 485 | }, 486 | "Path": "/", 487 | "Policies": [ 488 | { 489 | "PolicyName": "ecs-service", 490 | "PolicyDocument": { 491 | "Statement": [ 492 | { 493 | "Effect": "Allow", 494 | "Action": [ 495 | "ec2:AttachNetworkInterface", 496 | "ec2:CreateNetworkInterface", 497 | "ec2:CreateNetworkInterfacePermission", 498 | "ec2:DeleteNetworkInterface", 499 | "ec2:DeleteNetworkInterfacePermission", 500 | "ec2:Describe*", 501 | "ec2:DetachNetworkInterface", 502 | "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", 503 | "elasticloadbalancing:DeregisterTargets", 504 | "elasticloadbalancing:Describe*", 505 | "elasticloadbalancing:RegisterInstancesWithLoadBalancer", 506 | "elasticloadbalancing:RegisterTargets" 507 | ], 508 | "Resource": "*" 509 | } 510 | ] 511 | } 512 | } 513 | ] 514 | } 515 | }, 516 | "ECSTaskExecutionRole": { 517 | "Type": "AWS::IAM::Role", 518 | "Properties": { 519 | "AssumeRolePolicyDocument": { 520 | "Statement": [ 521 | { 522 | "Effect": "Allow", 523 | "Principal": { 524 | "Service": [ 525 | "ecs-tasks.amazonaws.com" 526 | ] 527 | }, 528 | "Action": [ 529 | "sts:AssumeRole" 530 | ] 531 | } 532 | ] 533 | }, 534 | "Path": "/", 535 | "Policies": [ 536 | { 537 | "PolicyName": "AmazonECSTaskExecutionRolePolicy", 538 | "PolicyDocument": { 539 | "Statement": [ 540 | { 541 | "Effect": "Allow", 542 | "Action": [ 543 | "ecr:GetAuthorizationToken", 544 | "ecr:BatchCheckLayerAvailability", 545 | "ecr:GetDownloadUrlForLayer", 546 | "ecr:BatchGetImage", 547 | "logs:CreateLogStream", 548 | "logs:PutLogEvents" 549 | ], 550 | "Resource": "*" 551 | } 552 | ] 553 | } 554 | } 555 | ] 556 | } 557 | }, 558 | "LogGroup": { 559 | "Type": "AWS::Logs::LogGroup", 560 | "Properties": { 561 | "LogGroupName": "selenium-grid", 562 | "RetentionInDays": 30 563 | } 564 | }, 565 | "TaskDefinitionHub": { 566 | "Type": "AWS::ECS::TaskDefinition", 567 | "Properties": { 568 | "Cpu": { 569 | "Ref": "ContainerCpu" 570 | }, 571 | "Memory": { 572 | "Ref": "ContainerMemory" 573 | }, 574 | "NetworkMode": "awsvpc", 575 | "RequiresCompatibilities": [ 576 | "FARGATE" 577 | ], 578 | "ExecutionRoleArn": { 579 | "Fn::GetAtt": [ 580 | "ECSTaskExecutionRole", 581 | "Arn" 582 | ] 583 | }, 584 | "ContainerDefinitions": [ 585 | { 586 | "Name": "hub", 587 | "Cpu": { 588 | "Ref": "ContainerCpu" 589 | }, 590 | "Memory": { 591 | "Ref": "ContainerMemory" 592 | }, 593 | "Image": "selenium/hub:3.9.1-actinium", 594 | "LogConfiguration": { 595 | "LogDriver": "awslogs", 596 | "Options": { 597 | "awslogs-group": { 598 | "Ref": "LogGroup" 599 | }, 600 | "awslogs-region": { 601 | "Ref": "AWS::Region" 602 | }, 603 | "awslogs-stream-prefix": "selenium" 604 | } 605 | }, 606 | "PortMappings": [ 607 | { 608 | "ContainerPort": 4444 609 | } 610 | ], 611 | "EntryPoint": [ 612 | "sh", 613 | "-c" 614 | ], 615 | "Command": [ 616 | "export GRID_HUB_HOST=`networkctl status eth0 | grep -oP \" Address: \\K([0-9]+\\.){3}[0-9]+\"`; /opt/bin/entry_point.sh;" 617 | ] 618 | } 619 | ] 620 | } 621 | }, 622 | "TaskDefinitionNodeChrome": { 623 | "Type": "AWS::ECS::TaskDefinition", 624 | "Properties": { 625 | "Cpu": { 626 | "Ref": "ContainerCpu" 627 | }, 628 | "Memory": { 629 | "Ref": "ContainerMemory" 630 | }, 631 | "NetworkMode": "awsvpc", 632 | "RequiresCompatibilities": [ 633 | "FARGATE" 634 | ], 635 | "ExecutionRoleArn": { 636 | "Fn::GetAtt": [ 637 | "ECSTaskExecutionRole", 638 | "Arn" 639 | ] 640 | }, 641 | "ContainerDefinitions": [ 642 | { 643 | "Name": "node-chrome", 644 | "Cpu": { 645 | "Ref": "ContainerCpu" 646 | }, 647 | "Memory": { 648 | "Ref": "ContainerMemory" 649 | }, 650 | "Image": "selenium/node-chrome:3.9.1-actinium", 651 | "Environment": [ 652 | { 653 | "Name": "HUB_PORT_4444_TCP_ADDR", 654 | "Value": { 655 | "Fn::GetAtt": [ 656 | "PublicLoadBalancer", 657 | "DNSName" 658 | ] 659 | } 660 | }, 661 | { 662 | "Name": "HUB_PORT_4444_TCP_PORT", 663 | "Value": "4444" 664 | } 665 | ], 666 | "LogConfiguration": { 667 | "LogDriver": "awslogs", 668 | "Options": { 669 | "awslogs-group": { 670 | "Ref": "LogGroup" 671 | }, 672 | "awslogs-region": { 673 | "Ref": "AWS::Region" 674 | }, 675 | "awslogs-stream-prefix": "selenium" 676 | } 677 | }, 678 | "PortMappings": [ 679 | { 680 | "ContainerPort": 5555 681 | } 682 | ] , 683 | "EntryPoint": [ 684 | "sh", 685 | "-c" 686 | ], 687 | "Command": [ 688 | "export REMOTE_HOST=http://`ip addr show eth0 | grep -oP \"inet \\K\\S([0-9]+\\.){3}[0-9]+\"`:$NODE_PORT ; printenv | grep REMOTE ; /opt/bin/entry_point.sh;" 689 | ] 690 | 691 | 692 | } 693 | ] 694 | } 695 | }, 696 | "ServiceHub": { 697 | "Type": "AWS::ECS::Service", 698 | "DependsOn": "LoadBalancerRule", 699 | "Properties": { 700 | "ServiceName": "hub", 701 | "Cluster": { 702 | "Ref": "ECSCluster" 703 | }, 704 | "LaunchType": "FARGATE", 705 | "DeploymentConfiguration": { 706 | "MaximumPercent": 200, 707 | "MinimumHealthyPercent": 75 708 | }, 709 | "DesiredCount": 2, 710 | "NetworkConfiguration": { 711 | "AwsvpcConfiguration": { 712 | "AssignPublicIp": "ENABLED", 713 | "SecurityGroups": [ 714 | { 715 | "Ref": "FargateContainerSecurityGroup" 716 | } 717 | ], 718 | "Subnets": [ 719 | { 720 | "Ref": "PrivateSubnetOne" 721 | }, 722 | { 723 | "Ref": "PrivateSubnetTwo" 724 | } 725 | ] 726 | } 727 | }, 728 | "TaskDefinition": { 729 | "Ref": "TaskDefinitionHub" 730 | }, 731 | "Role": { 732 | "Fn::If": [ 733 | "HasCustomRole", 734 | { 735 | "Ref": "Role" 736 | }, 737 | { 738 | "Ref": "AWS::NoValue" 739 | } 740 | ] 741 | }, 742 | "LoadBalancers": [ 743 | { 744 | "ContainerName": "hub", 745 | "ContainerPort": 4444, 746 | "TargetGroupArn": { 747 | "Ref": "TargetGroup" 748 | } 749 | } 750 | ] 751 | } 752 | }, 753 | "ServiceNodeChrome": { 754 | "Type": "AWS::ECS::Service", 755 | "DependsOn": "ServiceHub", 756 | "Properties": { 757 | "ServiceName": "node-chrome", 758 | "Cluster": { 759 | "Ref": "ECSCluster" 760 | }, 761 | "LaunchType": "FARGATE", 762 | "DeploymentConfiguration": { 763 | "MaximumPercent": 200, 764 | "MinimumHealthyPercent": 75 765 | }, 766 | "DesiredCount": 2, 767 | "NetworkConfiguration": { 768 | "AwsvpcConfiguration": { 769 | "AssignPublicIp": "ENABLED", 770 | "SecurityGroups": [ 771 | { 772 | "Ref": "FargateContainerSecurityGroup" 773 | } 774 | ], 775 | "Subnets": [ 776 | { 777 | "Ref": "PrivateSubnetOne" 778 | }, 779 | { 780 | "Ref": "PrivateSubnetTwo" 781 | } 782 | ] 783 | } 784 | }, 785 | "TaskDefinition": { 786 | "Ref": "TaskDefinitionNodeChrome" 787 | } 788 | } 789 | }, 790 | "TargetGroup": { 791 | "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", 792 | "Properties": { 793 | "HealthCheckIntervalSeconds": 6, 794 | "HealthCheckPath": "/", 795 | "HealthCheckProtocol": "HTTP", 796 | "HealthCheckTimeoutSeconds": 5, 797 | "HealthyThresholdCount": 2, 798 | "TargetType": "ip", 799 | "Name": "selenium-grid", 800 | "Port": 4444, 801 | "Protocol": "HTTP", 802 | "UnhealthyThresholdCount": 2, 803 | "VpcId": { 804 | "Ref": "VPC" 805 | } 806 | } 807 | }, 808 | "LoadBalancerRule": { 809 | "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", 810 | "DependsOn": "PublicLoadBalancerListener", 811 | "Properties": { 812 | "Actions": [ 813 | { 814 | "TargetGroupArn": { 815 | "Ref": "TargetGroup" 816 | }, 817 | "Type": "forward" 818 | } 819 | ], 820 | "Conditions": [ 821 | { 822 | "Field": "path-pattern", 823 | "Values": [ 824 | { 825 | "Ref": "Path" 826 | } 827 | ] 828 | } 829 | ], 830 | "ListenerArn": { 831 | "Ref": "PublicLoadBalancerListener" 832 | }, 833 | "Priority": { 834 | "Ref": "Priority" 835 | } 836 | } 837 | } 838 | }, 839 | "Outputs": { 840 | "ExternalUrl": { 841 | "Description": "The url of the external load balancer", 842 | "Value": { 843 | "Fn::Join": [ 844 | "", 845 | [ 846 | "http://", 847 | { 848 | "Fn::GetAtt": [ 849 | "PublicLoadBalancer", 850 | "DNSName" 851 | ] 852 | } 853 | ] 854 | ] 855 | }, 856 | "Export": { 857 | "Name": { 858 | "Fn::Join": [ 859 | ":", 860 | [ 861 | { 862 | "Ref": "AWS::StackName" 863 | }, 864 | "ExternalUrl" 865 | ] 866 | ] 867 | } 868 | } 869 | } 870 | } 871 | } 872 | -------------------------------------------------------------------------------- /aws/cloud-formation/template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Parameters: 4 | ElasticIpOne: 5 | Type: String 6 | Default: 'eipalloc-0236872aadcb9b4b8' 7 | Description: The allocationId of an already reserved ElasticIP 8 | ElasticIpTwo: 9 | Type: String 10 | Default: 'eipalloc-01c9d1963b3751183' 11 | Description: The allocationId of an already reserved ElasticIP 12 | NumberOfHub: 13 | Type: String 14 | Default: '1' 15 | Description: The number of Selenium Hub's instances to allocate. 16 | NumberOfFirefox: 17 | Type: String 18 | Default: '0' 19 | Description: The number of firefox's instances to allocate. 20 | NumberOfChrome: 21 | Type: String 22 | Default: '2' 23 | Description: The number of chrome's instances to allocate. 24 | 25 | 26 | # DomainName: 27 | # Type: String 28 | ContainerCpu: 29 | Type: Number 30 | Default: 256 31 | Description: How much CPU to give the container. 1024 is 1 CPU 32 | ContainerMemory: 33 | Type: Number 34 | Default: 512 35 | Description: How much memory in megabytes to give the container 36 | Path: 37 | Type: String 38 | Default: '*' 39 | Description: A path on the public load balancer that this service 40 | should be connected to. Use * to send all load balancer 41 | traffic to this service. 42 | Priority: 43 | Type: Number 44 | Default: 1 45 | Description: The priority for the routing rule added to the load balancer. 46 | This only applies if your have multiple services which have been 47 | assigned to different paths on the load balancer. 48 | Role: 49 | Type: String 50 | Default: '' 51 | Description: (Optional) An IAM role to give the service's containers if the code within needs to 52 | access other AWS resources like S3 buckets, DynamoDB tables, etc 53 | 54 | Conditions: 55 | HasCustomRole: !Not [ !Equals [!Ref 'Role', ''] ] 56 | 57 | Mappings: 58 | SubnetConfig: 59 | VPC: 60 | CIDR: '10.0.0.0/16' 61 | PublicOne: 62 | CIDR: '10.0.0.0/24' 63 | PublicTwo: 64 | CIDR: '10.0.1.0/24' 65 | PrivateOne: 66 | CIDR: '10.0.2.0/24' 67 | PrivateTwo: 68 | CIDR: '10.0.3.0/24' 69 | 70 | Resources: 71 | VPC: 72 | Type: AWS::EC2::VPC 73 | Properties: 74 | EnableDnsSupport: true 75 | EnableDnsHostnames: true 76 | CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] 77 | PublicSubnetOne: 78 | Type: AWS::EC2::Subnet 79 | Properties: 80 | AvailabilityZone: 81 | Fn::Select: 82 | - 0 83 | - Fn::GetAZs: {Ref: 'AWS::Region'} 84 | VpcId: !Ref 'VPC' 85 | CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR'] 86 | MapPublicIpOnLaunch: true 87 | PublicSubnetTwo: 88 | Type: AWS::EC2::Subnet 89 | Properties: 90 | AvailabilityZone: 91 | Fn::Select: 92 | - 1 93 | - Fn::GetAZs: {Ref: 'AWS::Region'} 94 | VpcId: !Ref 'VPC' 95 | CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR'] 96 | MapPublicIpOnLaunch: true 97 | PrivateSubnetOne: 98 | Type: AWS::EC2::Subnet 99 | Properties: 100 | AvailabilityZone: 101 | Fn::Select: 102 | - 0 103 | - Fn::GetAZs: {Ref: 'AWS::Region'} 104 | VpcId: !Ref 'VPC' 105 | CidrBlock: !FindInMap ['SubnetConfig', 'PrivateOne', 'CIDR'] 106 | PrivateSubnetTwo: 107 | Type: AWS::EC2::Subnet 108 | Properties: 109 | AvailabilityZone: 110 | Fn::Select: 111 | - 1 112 | - Fn::GetAZs: {Ref: 'AWS::Region'} 113 | VpcId: !Ref 'VPC' 114 | CidrBlock: !FindInMap ['SubnetConfig', 'PrivateTwo', 'CIDR'] 115 | InternetGateway: 116 | Type: AWS::EC2::InternetGateway 117 | GatewayAttachement: 118 | Type: AWS::EC2::VPCGatewayAttachment 119 | Properties: 120 | VpcId: !Ref 'VPC' 121 | InternetGatewayId: !Ref 'InternetGateway' 122 | PublicRouteTable: 123 | Type: AWS::EC2::RouteTable 124 | Properties: 125 | VpcId: !Ref 'VPC' 126 | PublicRoute: 127 | Type: AWS::EC2::Route 128 | DependsOn: GatewayAttachement 129 | Properties: 130 | RouteTableId: !Ref 'PublicRouteTable' 131 | DestinationCidrBlock: '0.0.0.0/0' 132 | GatewayId: !Ref 'InternetGateway' 133 | PublicSubnetOneRouteTableAssociation: 134 | Type: AWS::EC2::SubnetRouteTableAssociation 135 | Properties: 136 | SubnetId: !Ref PublicSubnetOne 137 | RouteTableId: !Ref PublicRouteTable 138 | PublicSubnetTwoRouteTableAssociation: 139 | Type: AWS::EC2::SubnetRouteTableAssociation 140 | Properties: 141 | SubnetId: !Ref PublicSubnetTwo 142 | RouteTableId: !Ref PublicRouteTable 143 | # NatGatewayOneAttachment: 144 | # Type: AWS::EC2::EIP 145 | # DependsOn: GatewayAttachement 146 | # Properties: 147 | # Domain: vpc 148 | # NatGatewayTwoAttachment: 149 | # Type: AWS::EC2::EIP 150 | # DependsOn: GatewayAttachement 151 | # Properties: 152 | # Domain: vpc 153 | NatGatewayOne: 154 | Type: AWS::EC2::NatGateway 155 | Properties: 156 | # AllocationId: !GetAtt NatGatewayOneAttachment.AllocationId 157 | AllocationId: 158 | Ref: 'ElasticIpOne' 159 | SubnetId: !Ref PublicSubnetOne 160 | NatGatewayTwo: 161 | Type: AWS::EC2::NatGateway 162 | Properties: 163 | AllocationId: 164 | Ref: 'ElasticIpTwo' 165 | # AllocationId: !GetAtt NatGatewayTwoAttachment.AllocationId 166 | SubnetId: !Ref PublicSubnetTwo 167 | PrivateRouteTableOne: 168 | Type: AWS::EC2::RouteTable 169 | Properties: 170 | VpcId: !Ref 'VPC' 171 | PrivateRouteOne: 172 | Type: AWS::EC2::Route 173 | Properties: 174 | RouteTableId: !Ref PrivateRouteTableOne 175 | DestinationCidrBlock: 0.0.0.0/0 176 | NatGatewayId: !Ref NatGatewayOne 177 | PrivateRouteTableOneAssociation: 178 | Type: AWS::EC2::SubnetRouteTableAssociation 179 | Properties: 180 | RouteTableId: !Ref PrivateRouteTableOne 181 | SubnetId: !Ref PrivateSubnetOne 182 | PrivateRouteTableTwo: 183 | Type: AWS::EC2::RouteTable 184 | Properties: 185 | VpcId: !Ref 'VPC' 186 | PrivateRouteTwo: 187 | Type: AWS::EC2::Route 188 | Properties: 189 | RouteTableId: !Ref PrivateRouteTableTwo 190 | DestinationCidrBlock: 0.0.0.0/0 191 | NatGatewayId: !Ref NatGatewayTwo 192 | PrivateRouteTableTwoAssociation: 193 | Type: AWS::EC2::SubnetRouteTableAssociation 194 | Properties: 195 | RouteTableId: !Ref PrivateRouteTableTwo 196 | SubnetId: !Ref PrivateSubnetTwo 197 | ECSCluster: 198 | Type: AWS::ECS::Cluster 199 | Properties: 200 | ClusterName: 'selenium-grid' 201 | FargateContainerSecurityGroup: 202 | Type: AWS::EC2::SecurityGroup 203 | Properties: 204 | GroupDescription: Access to the Fargate containers 205 | VpcId: !Ref 'VPC' 206 | EcsSecurityGroupIngressFromPublicALB: 207 | Type: AWS::EC2::SecurityGroupIngress 208 | Properties: 209 | Description: Ingress from the public ALB 210 | GroupId: !Ref 'FargateContainerSecurityGroup' 211 | IpProtocol: -1 212 | SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG' 213 | EcsSecurityGroupIngressFromSelf: 214 | Type: AWS::EC2::SecurityGroupIngress 215 | Properties: 216 | Description: Ingress from other containers in the same security group 217 | GroupId: !Ref 'FargateContainerSecurityGroup' 218 | IpProtocol: -1 219 | SourceSecurityGroupId: !Ref 'FargateContainerSecurityGroup' 220 | PublicLoadBalancerSG: 221 | Type: AWS::EC2::SecurityGroup 222 | Properties: 223 | GroupDescription: Access to the public facing load balancer 224 | VpcId: !Ref 'VPC' 225 | SecurityGroupIngress: 226 | - CidrIp: 0.0.0.0/0 227 | IpProtocol: -1 228 | PublicLoadBalancer: 229 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 230 | Properties: 231 | Scheme: internet-facing 232 | LoadBalancerAttributes: 233 | - Key: idle_timeout.timeout_seconds 234 | Value: '30' 235 | Subnets: 236 | - !Ref PublicSubnetOne 237 | - !Ref PublicSubnetTwo 238 | SecurityGroups: [!Ref 'PublicLoadBalancerSG'] 239 | # This is an optional resource. If you don't want to use a custom domain, just 240 | # remove the RecordSet resource an use the URL of the public load balancer which can 241 | # be found in CloudFormation. 242 | # RecordSet: 243 | # Type: "AWS::Route53::RecordSet" 244 | # DependsOn: PublicLoadBalancer 245 | # Properties: 246 | # Name: !Join ['.', ['selenium-grid', !Ref 'DomainName']] 247 | # AliasTarget: 248 | # DNSName: !GetAtt 'PublicLoadBalancer.DNSName' 249 | # HostedZoneId: !GetAtt 'PublicLoadBalancer.CanonicalHostedZoneID' 250 | # HostedZoneName: !Ref 'DomainName' 251 | # Type: A 252 | DummyTargetGroupPublic: 253 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 254 | Properties: 255 | HealthCheckIntervalSeconds: 6 256 | HealthCheckPath: / 257 | HealthCheckProtocol: HTTP 258 | HealthCheckTimeoutSeconds: 5 259 | HealthyThresholdCount: 2 260 | Name: !Join ['-', [!Ref 'AWS::StackName', 'drop-1']] 261 | Port: 4444 262 | Protocol: HTTP 263 | UnhealthyThresholdCount: 2 264 | VpcId: !Ref 'VPC' 265 | PublicLoadBalancerListener: 266 | Type: AWS::ElasticLoadBalancingV2::Listener 267 | DependsOn: PublicLoadBalancer 268 | Properties: 269 | DefaultActions: 270 | - TargetGroupArn: !Ref 'DummyTargetGroupPublic' 271 | Type: 'forward' 272 | LoadBalancerArn: !Ref 'PublicLoadBalancer' 273 | Port: 4444 274 | Protocol: HTTP 275 | ECSRole: 276 | Type: AWS::IAM::Role 277 | Properties: 278 | AssumeRolePolicyDocument: 279 | Statement: 280 | - Effect: Allow 281 | Principal: 282 | Service: [ecs.amazonaws.com] 283 | Action: ['sts:AssumeRole'] 284 | Path: / 285 | Policies: 286 | - PolicyName: ecs-service 287 | PolicyDocument: 288 | Statement: 289 | - Effect: Allow 290 | Action: 291 | - 'ec2:AttachNetworkInterface' 292 | - 'ec2:CreateNetworkInterface' 293 | - 'ec2:CreateNetworkInterfacePermission' 294 | - 'ec2:DeleteNetworkInterface' 295 | - 'ec2:DeleteNetworkInterfacePermission' 296 | - 'ec2:Describe*' 297 | - 'ec2:DetachNetworkInterface' 298 | - 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer' 299 | - 'elasticloadbalancing:DeregisterTargets' 300 | - 'elasticloadbalancing:Describe*' 301 | - 'elasticloadbalancing:RegisterInstancesWithLoadBalancer' 302 | - 'elasticloadbalancing:RegisterTargets' 303 | Resource: '*' 304 | ECSTaskExecutionRole: 305 | Type: AWS::IAM::Role 306 | Properties: 307 | AssumeRolePolicyDocument: 308 | Statement: 309 | - Effect: Allow 310 | Principal: 311 | Service: [ecs-tasks.amazonaws.com] 312 | Action: ['sts:AssumeRole'] 313 | Path: / 314 | Policies: 315 | - PolicyName: AmazonECSTaskExecutionRolePolicy 316 | PolicyDocument: 317 | Statement: 318 | - Effect: Allow 319 | Action: 320 | - 'ecr:GetAuthorizationToken' 321 | - 'ecr:BatchCheckLayerAvailability' 322 | - 'ecr:GetDownloadUrlForLayer' 323 | - 'ecr:BatchGetImage' 324 | - 'logs:CreateLogStream' 325 | - 'logs:PutLogEvents' 326 | Resource: '*' 327 | LogGroup: 328 | Type: 'AWS::Logs::LogGroup' 329 | Properties: 330 | LogGroupName: selenium-grid 331 | RetentionInDays: 30 332 | TaskDefinitionHub: 333 | Type: AWS::ECS::TaskDefinition 334 | Properties: 335 | Cpu: !Ref 'ContainerCpu' 336 | Memory: !Ref 'ContainerMemory' 337 | NetworkMode: awsvpc 338 | RequiresCompatibilities: 339 | - FARGATE 340 | ExecutionRoleArn: !GetAtt 'ECSTaskExecutionRole.Arn' 341 | ContainerDefinitions: 342 | - Name: 'hub' 343 | Cpu: !Ref 'ContainerCpu' 344 | Memory: !Ref 'ContainerMemory' 345 | Image: 'selenium/hub:3.9.1-actinium' 346 | LogConfiguration: 347 | LogDriver: awslogs 348 | Options: 349 | awslogs-group: !Ref LogGroup 350 | awslogs-region: !Ref 'AWS::Region' 351 | awslogs-stream-prefix: 'selenium' 352 | PortMappings: 353 | - ContainerPort: 4444 354 | EntryPoint: 355 | - 'sh' 356 | - '-c' 357 | Command: 358 | - "export GRID_HUB_HOST=`networkctl status eth0 | grep -oP \" Address: \\K([0-9]+\\.){3}[0-9]+\"`; /opt/bin/entry_point.sh;" 359 | TaskDefinitionNodeChrome: 360 | Type: AWS::ECS::TaskDefinition 361 | Properties: 362 | Cpu: !Ref 'ContainerCpu' 363 | Memory: !Ref 'ContainerMemory' 364 | NetworkMode: awsvpc 365 | RequiresCompatibilities: 366 | - FARGATE 367 | ExecutionRoleArn: !GetAtt 'ECSTaskExecutionRole.Arn' 368 | ContainerDefinitions: 369 | - Name: 'node-chrome' 370 | Cpu: !Ref 'ContainerCpu' 371 | Memory: !Ref 'ContainerMemory' 372 | Image: 'selenium/node-chrome:3.9.1-actinium' 373 | Environment: 374 | - Name: 'HUB_PORT_4444_TCP_ADDR' 375 | Value: !GetAtt 'PublicLoadBalancer.DNSName' 376 | - Name: 'HUB_PORT_4444_TCP_PORT' 377 | Value: '4444' 378 | LogConfiguration: 379 | LogDriver: awslogs 380 | Options: 381 | awslogs-group: !Ref LogGroup 382 | awslogs-region: !Ref 'AWS::Region' 383 | awslogs-stream-prefix: 'selenium' 384 | PortMappings: 385 | - ContainerPort: 5555 386 | EntryPoint: 387 | - 'sh' 388 | - '-c' 389 | Command: 390 | - "export REMOTE_HOST=http://`ip addr show eth0 | grep -oP \"inet \\K\\S([0-9]+\\.){3}[0-9]+\"`:$NODE_PORT ; printenv | grep REMOTE ; /opt/bin/entry_point.sh;" 391 | 392 | # TaskDefinitionNodeFirefox: 393 | # Type: AWS::ECS::TaskDefinition 394 | # Properties: 395 | # Cpu: !Ref 'ContainerCpu' 396 | # Memory: !Ref 'ContainerMemory' 397 | # NetworkMode: awsvpc 398 | # RequiresCompatibilities: 399 | # - FARGATE 400 | # ExecutionRoleArn: !GetAtt 'ECSTaskExecutionRole.Arn' 401 | # ContainerDefinitions: 402 | # - Name: 'node-firefox' 403 | # Cpu: !Ref 'ContainerCpu' 404 | # Memory: !Ref 'ContainerMemory' 405 | # Image: 'selenium/node-firefox:3.9.1-actinium' 406 | # Environment: 407 | # - Name: 'HUB_PORT_4444_TCP_ADDR' 408 | # Value: !GetAtt 'PublicLoadBalancer.DNSName' 409 | # - Name: 'HUB_PORT_4444_TCP_PORT' 410 | # Value: '4444' 411 | # LogConfiguration: 412 | # LogDriver: awslogs 413 | # Options: 414 | # awslogs-group: !Ref LogGroup 415 | # awslogs-region: !Ref 'AWS::Region' 416 | # awslogs-stream-prefix: 'selenium' 417 | # PortMappings: 418 | # - ContainerPort: 5555 419 | ServiceHub: 420 | Type: AWS::ECS::Service 421 | DependsOn: LoadBalancerRule 422 | Properties: 423 | ServiceName: 'hub' 424 | Cluster: !Ref 'ECSCluster' 425 | LaunchType: FARGATE 426 | DeploymentConfiguration: 427 | MaximumPercent: 200 428 | MinimumHealthyPercent: 75 429 | DesiredCount: !Ref 'NumberOfHub' 430 | NetworkConfiguration: 431 | AwsvpcConfiguration: 432 | AssignPublicIp: ENABLED 433 | SecurityGroups: 434 | - !Ref 'FargateContainerSecurityGroup' 435 | Subnets: 436 | - !Ref 'PrivateSubnetOne' 437 | - !Ref 'PrivateSubnetTwo' 438 | TaskDefinition: !Ref 'TaskDefinitionHub' 439 | Role: 440 | Fn::If: 441 | - 'HasCustomRole' 442 | - !Ref 'Role' 443 | - !Ref 'AWS::NoValue' 444 | LoadBalancers: 445 | - ContainerName: 'hub' 446 | ContainerPort: 4444 447 | TargetGroupArn: !Ref 'TargetGroup' 448 | ServiceNodeChrome: 449 | Type: AWS::ECS::Service 450 | DependsOn: ServiceHub 451 | Properties: 452 | ServiceName: 'node-chrome' 453 | Cluster: !Ref 'ECSCluster' 454 | LaunchType: FARGATE 455 | DeploymentConfiguration: 456 | MaximumPercent: 200 457 | MinimumHealthyPercent: 75 458 | DesiredCount: !Ref 'NumberOfChrome' 459 | NetworkConfiguration: 460 | AwsvpcConfiguration: 461 | AssignPublicIp: ENABLED 462 | SecurityGroups: 463 | - !Ref 'FargateContainerSecurityGroup' 464 | Subnets: 465 | - !Ref 'PrivateSubnetOne' 466 | - !Ref 'PrivateSubnetTwo' 467 | TaskDefinition: !Ref 'TaskDefinitionNodeChrome' 468 | # ServiceNodeFirefox: 469 | # Type: AWS::ECS::Service 470 | # DependsOn: ServiceHub 471 | # Properties: 472 | # ServiceName: 'node-firefox' 473 | # Cluster: !Ref 'ECSCluster' 474 | # LaunchType: FARGATE 475 | # DeploymentConfiguration: 476 | # MaximumPercent: 200 477 | # MinimumHealthyPercent: 75 478 | # DesiredCount: 2 479 | # NetworkConfiguration: 480 | # AwsvpcConfiguration: 481 | # AssignPublicIp: ENABLED 482 | # SecurityGroups: 483 | # - !Ref 'FargateContainerSecurityGroup' 484 | # Subnets: 485 | # - !Ref 'PrivateSubnetOne' 486 | # - !Ref 'PrivateSubnetTwo' 487 | # TaskDefinition: !Ref 'TaskDefinitionNodeFirefox' 488 | TargetGroup: 489 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 490 | Properties: 491 | HealthCheckIntervalSeconds: 6 492 | HealthCheckPath: / 493 | HealthCheckProtocol: HTTP 494 | HealthCheckTimeoutSeconds: 5 495 | HealthyThresholdCount: 2 496 | TargetType: ip 497 | Name: 'selenium-grid' 498 | Port: 4444 499 | Protocol: HTTP 500 | UnhealthyThresholdCount: 2 501 | VpcId: !Ref 'VPC' 502 | LoadBalancerRule: 503 | Type: AWS::ElasticLoadBalancingV2::ListenerRule 504 | DependsOn: PublicLoadBalancerListener 505 | Properties: 506 | Actions: 507 | - TargetGroupArn: !Ref 'TargetGroup' 508 | Type: 'forward' 509 | Conditions: 510 | - Field: path-pattern 511 | Values: [!Ref 'Path'] 512 | ListenerArn: !Ref 'PublicLoadBalancerListener' 513 | Priority: !Ref 'Priority' 514 | 515 | Outputs: 516 | ExternalUrl: 517 | Description: The url of the Selenium console 518 | Value: !Join ['', ['http://', !GetAtt 'PublicLoadBalancer.DNSName', ':4444/grid/console']] 519 | Export: 520 | Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ExternalUrl' ] ] 521 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/sh 2 | STACK-NAME=selenium-grid 3 | TEMPLATE=file://./aws/cloud-formation/template.yml 4 | AWS_PROFILE=boso 5 | AWS_DEV_OPS_KEY_TOKEN=87648098-8dce-4df8-a0d6-0238af3e6270 6 | 7 | 8 | deprovision: 9 | aws --profile $(AWS_PROFILE) cloudformation delete-stack --stack-name $(STACK-NAME) 10 | 11 | estimate-cost: secrets-devops-decrypt 12 | aws --profile $(AWS_PROFILE) cloudformation estimate-template-cost \ 13 | --template-body $(TEMPLATE) \ 14 | --parameters file://./aws/cloud-formation/secrets.decrypted.json 15 | make secrets-devops-clean-up 16 | 17 | provision: secrets-devops-decrypt 18 | aws --profile $(AWS_PROFILE) cloudformation create-stack --stack-name $(STACK-NAME) \ 19 | --capabilities CAPABILITY_IAM \ 20 | --template-body $(TEMPLATE) \ 21 | --parameters file://./aws/cloud-formation/secrets.decrypted.json 22 | make secrets-devops-clean-up 23 | 24 | reprovision: secrets-devops-decrypt 25 | aws --profile $(AWS_PROFILE) cloudformation update-stack --stack-name $(STACK-NAME) \ 26 | --capabilities CAPABILITY_IAM \ 27 | --template-body $(TEMPLATE) \ 28 | --parameters file://./aws/cloud-formation/secrets.decrypted.json 29 | make secrets-devops-clean-up 30 | 31 | secrets-devops-clean-up: 32 | rm -rf ./aws/cloud-formation/secrets.decrypted.json 33 | 34 | secrets-devops-decrypt: 35 | aws --profile $(AWS_PROFILE) kms decrypt --ciphertext-blob fileb://./aws/cloud-formation/secrets.encrypted.txt --output text --query Plaintext | base64 --decode > ./aws/cloud-formation/secrets.decrypted.json 36 | 37 | secrets-devops-encrypt: 38 | aws --profile $(AWS_PROFILE) kms encrypt --key-id $(AWS_DEV_OPS_KEY_TOKEN) --plaintext fileb://./aws/cloud-formation/secrets.decrypted.json --output text --query CiphertextBlob | base64 --decode > ./aws/cloud-formation/secrets.encrypted.txt 39 | 40 | info: 41 | aws --profile $(AWS_PROFILE) cloudformation list-stacks --stack-status-filter CREATE_IN_PROGRESS \ 42 | CREATE_FAILED\ 43 | CREATE_COMPLETE\ 44 | ROLLBACK_IN_PROGRESS\ 45 | ROLLBACK_FAILED\ 46 | ROLLBACK_COMPLETE\ 47 | DELETE_IN_PROGRESS\ 48 | DELETE_FAILED\ 49 | UPDATE_IN_PROGRESS\ 50 | UPDATE_COMPLETE_CLEANUP_IN_PROGRESS\ 51 | UPDATE_COMPLETE\ 52 | UPDATE_ROLLBACK_IN_PROGRESS\ 53 | UPDATE_ROLLBACK_FAILED\ 54 | UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS\ 55 | UPDATE_ROLLBACK_COMPLETE\ 56 | REVIEW_IN_PROGRESS 57 | # DELETE_COMPLETE 58 | 59 | list-instances: 60 | aws --profile $(AWS_PROFILE) cloudformation list-stack-instances --stack-name $(STACK-NAME) 61 | --------------------------------------------------------------------------------