├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── arch.png └── cdk ├── app.py ├── cdk.json ├── config.ini ├── docker ├── leader │ ├── Dockerfile │ ├── config-as-code.j2 │ ├── entrypoint.sh │ ├── modify_casc.py │ ├── plugins.txt │ └── test-env.sh └── worker │ └── Dockerfile ├── envs.sh ├── jenkins ├── __init__.py ├── ecs.py ├── jenkins_leader.py ├── jenkins_worker.py └── network.py ├── requirements.txt ├── setup.py ├── setup_env └── tests ├── __init__.py └── unit ├── __init__.py └── test_hello_construct.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | cdk/.env 4 | cdk/__pycache__* 5 | cdk/cdk.out 6 | cdk/cdk/cdk.egg* 7 | 8 | # Python Virtual Env 9 | .env 10 | 11 | # Visual Studio 12 | .vs/ 13 | .vscode/ 14 | 15 | # JetBrains IDEs 16 | .idea 17 | 18 | # Node Modules 19 | node_modules/ 20 | 21 | # CDK output 22 | cdk/cdk.out 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/abc123), or [recently closed](https://github.com/aws-samples/abc123/issues?utf8=), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/abc123) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/abc123/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jenkins on AWS 2 | 3 | ## What is this? 4 | This project will build and deploy an immutable, fault tolerant, and cost effective Jenkins environment in AWS using ECS. All Jenkins images are managed within the repository (pulled from upstream) and fully configurable as code. Plugin installation is automated, including versioning, as well as configured through the Configuration as Code [plugin](https://jenkins.io/projects/jcasc/). 5 | 6 | ## Why 7 | Managing Jenkins is hard. From plugin management and configuration details, to ensuring that the nodes are up to date and running, it's a lot of work and susceptible to issues. 8 | 9 | Please note, this is an **example** and **not production ready**. You are still responsible for securing Jenkins (enabling TLS at the ALB, adding authentication, etc). This intent of this project is to provide an example of running Jenkins on ECS. 10 | 11 | ## Architecture 12 | 13 | ![ArchDiagram](./arch.png) 14 | 15 | ## Project Structure 16 | 17 | ``` 18 | ./ 19 | 20 | ./cdk/ <-- Deployment configuration 21 | 22 | ./cdk/docker/ <-- Dockerfiles and associated config files for Leader and Worker images. The cdk will build these images on a cdk deploy. 23 | 24 | ./cdk/app.py <-- cdk application file where all stacks are instantiated and built 25 | 26 | ./cdk/requirements.txt <-- Python module requirements 27 | 28 | ./cdk/jenkins/ <-- cdk stacks to deploy Jenkins environment 29 | ``` 30 | 31 | 32 | ## Requirements 33 | 34 | To deploy this environment, we will use the [aws-cdk](https://github.com/aws/aws-cdk) 35 | - Please follow the requirements to install from the cdk github repo 36 | - Tested with the following version: `1.53.0 (build 6c326cb)` 37 | 38 | ## Fargate Jenkins (Leader and Workers) 39 | 40 | Set config.ini 41 | ```bash 42 | fargate_enabled = yes 43 | ``` 44 | 45 | ## EC2 Backed Leader and Fargate Workers 46 | Set config.ini 47 | ```bash 48 | ec2_enabled = yes 49 | ``` 50 | 51 | ## Validate configs and deploy 52 | 53 | Navigate to the cdk directory, and run: 54 | 55 | ```bash 56 | cdk synth 57 | ``` 58 | 59 | Output should look something like: 60 | 61 | ```console 62 | [user@computer cdk (cdk)]$ cdk synth 63 | Successfully synthesized to jenkins-on-aws/cdk/cdk.out 64 | Supply a stack name (JenkinsOnAWSNetwork, JenkinsOnAWSECS, JenkinsOnAWSWorker, JenkinsOnAWSJenkinsLeader) to display its template. 65 | ``` 66 | 67 | Feel free to check out the [CloudFormation](https://aws.amazon.com/cloudformation/) templates created by the cdk in the `cdk.out` directory 68 | 69 | Let's deploy the environment! The below command will deploy all of the stacks required to get the environment up and running: 70 | 71 | ```bash 72 | cdk deploy Jenkins* 73 | ``` 74 | 75 | _Note:_ You will be prompted for approval during the stages of the deploy. Follow the instructions on the prompt when asked. 76 | 77 | 78 | That's it! You now have a fully automated Jenkins implementation running on AWS Fargate with worker nodes automatically configured to run on an as needed basis. 79 | 80 | 81 | -------------------------------------------------------------------------------- /arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jenkins-on-aws/4eb20872954c83b9b5cfc9d1853f9815ecc3000c/arch.png -------------------------------------------------------------------------------- /cdk/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from aws_cdk import core 4 | from os import getenv 5 | from configparser import ConfigParser 6 | from jenkins.network import Network 7 | from jenkins.ecs import ECSCluster 8 | from jenkins.jenkins_leader import JenkinsLeader 9 | from jenkins.jenkins_worker import JenkinsWorker 10 | 11 | config = ConfigParser() 12 | config.read('config.ini') 13 | 14 | stack_name = config['DEFAULT']['stack_name'] 15 | account = getenv('CDK_DEFAULT_ACCOUNT') 16 | region = getenv('CDK_DEFAULT_REGION') 17 | 18 | service_discovery_namespace = 'jenkins' 19 | 20 | app = core.App() 21 | network = Network(app, stack_name + 'Network') 22 | ecs_cluster = ECSCluster(app, stack_name + 'ECS', vpc=network.vpc, service_discovery_namespace=service_discovery_namespace) 23 | jenkins_workers = JenkinsWorker(app, stack_name + "Worker", vpc=network.vpc, cluster=ecs_cluster) 24 | jenkins_leader_service = JenkinsLeader(app, stack_name + 'JenkinsLeader', cluster=ecs_cluster, vpc=network, worker=jenkins_workers) 25 | 26 | app.synth() 27 | -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py" 3 | } 4 | -------------------------------------------------------------------------------- /cdk/config.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | region = us-west-2 3 | stack_name = JenkinsOnAWS 4 | 5 | # Network settings 6 | cidr = 10.0.0.0/24 7 | 8 | # ECS w/EC2 Jenkins Leader 9 | ec2_enabled = no 10 | instance_type = t3.xlarge 11 | ec2_cpu = 4096 12 | ec2_memory_limit_mib = 8192 13 | 14 | # Fargate Jenkins Leader 15 | fargate_enabled = yes 16 | fargate_cpu = 4096 17 | fargate_memory_limit_mib = 8192 18 | 19 | jenkins_url = http://leader.jenkins:8080 -------------------------------------------------------------------------------- /cdk/docker/leader/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jenkins/jenkins:2.235.5-lts 2 | 3 | # Install custom plugins 4 | COPY plugins.txt /usr/share/jenkins/ref/plugins.txt 5 | RUN /usr/local/bin/install-plugins.sh < /usr/share/jenkins/ref/plugins.txt 6 | 7 | COPY modify_casc.py /modify_casc.py 8 | COPY config-as-code.j2 /config-as-code.j2 9 | 10 | USER root 11 | 12 | RUN apt-get update &&\ 13 | apt-get install -y python-pip &&\ 14 | pip install jinja2 dnspython &&\ 15 | rm -rf /var/lib/apt/lists/* &&\ 16 | touch /config-as-code.yaml &&\ 17 | chown jenkins: /config-as-code.yaml &&\ 18 | sed -i '/\/bin\/bash*/a \\n\/modify_casc.py' /usr/local/bin/jenkins.sh 19 | 20 | # User back to jenkins 21 | USER jenkins 22 | -------------------------------------------------------------------------------- /cdk/docker/leader/config-as-code.j2: -------------------------------------------------------------------------------- 1 | jenkins: 2 | clouds: 3 | - ecs: 4 | cluster: {{ECS_CLUSTER_ARN}} 5 | # Credentials should not be set locally. Use IAM roles assigned to the container. 6 | credentialsId: False 7 | regionName: {{AWS_REGION}} 8 | name: fargate-workers 9 | jenkinsUrl: {{JENKINS_URL}} 10 | templates: 11 | - label: fargate-workers 12 | templateName: fargate-worker-nodes 13 | # Build image and store in ECR. Benefits: fast download time, control image on your own 14 | image: jenkins/jnlp-slave 15 | launchType: FARGATE 16 | networkMode: awsvpc 17 | # Soft memory limit 18 | memoryReservation: 2048 19 | cpu: 1024 20 | subnets: {{SUBNET_IDS}} 21 | securityGroups: {{SECURITY_GROUP_IDS}} 22 | executionRole: {{EXECUTION_ROLE_ARN}} 23 | taskrole: {{TASK_ROLE_ARN}} 24 | logDriver: awslogs 25 | logDriverOptions: 26 | - name: awslogs-region 27 | value: {{AWS_REGION}} 28 | - name: awslogs-group 29 | value: {{LOG_GROUP}} 30 | - name: awslogs-stream-prefix 31 | value: {{LOG_STREAM_PREFIX}} 32 | 33 | -------------------------------------------------------------------------------- /cdk/docker/leader/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | echo "ENTRYPOINT" >> /entrypoint.complete 4 | 5 | /sbin/tini -- /usr/local/bin/jenkins.sh 6 | -------------------------------------------------------------------------------- /cdk/docker/leader/modify_casc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from jinja2 import Environment, FileSystemLoader 4 | from os import getenv 5 | 6 | 7 | def main(): 8 | # This value comes as a build env var: `SSM_CONFIG_PARAM_NAME` 9 | _env = Environment(loader=FileSystemLoader('/'), autoescape=True) 10 | _template = _env.get_template("/config-as-code.j2") 11 | _config_file = open("/config-as-code.yaml", "w") 12 | 13 | _config_file.write((_template.render( 14 | ECS_CLUSTER_ARN=getenv('cluster_arn'), 15 | AWS_REGION=getenv('aws_region'), 16 | JENKINS_URL=getenv('jenkins_url'), 17 | SUBNET_IDS=getenv('subnet_ids'), 18 | SECURITY_GROUP_IDS=getenv('security_group_ids'), 19 | EXECUTION_ROLE_ARN=getenv('execution_role_arn'), 20 | TASK_ROLE_ARN=getenv('task_role_arn'), 21 | LOG_GROUP=getenv('worker_log_group'), 22 | LOG_STREAM_PREFIX=getenv('worker_log_stream_prefix') 23 | ))) 24 | 25 | _config_file.close() 26 | 27 | 28 | if __name__ == '__main__': 29 | main() -------------------------------------------------------------------------------- /cdk/docker/leader/plugins.txt: -------------------------------------------------------------------------------- 1 | amazon-ecs:1.37 2 | blueocean:1.24.7 3 | pipeline-cloudwatch-logs:0.2 4 | configuration-as-code:1.51 -------------------------------------------------------------------------------- /cdk/docker/leader/test-env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cluster_arn=TEST 4 | aws_region=TEST 5 | jenkins_url=TEST 6 | subnet_ids=TEST 7 | sec_grp_ids=TEST 8 | execution_role_arn=TEST 9 | task_role_arn=TEST 10 | JAVA_OPTS=-Djenkins.install.runSetupWizard=false 11 | -------------------------------------------------------------------------------- /cdk/docker/worker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jenkins/jnlp-slave 2 | -------------------------------------------------------------------------------- /cdk/envs.sh: -------------------------------------------------------------------------------- 1 | CDK_DEFAULT_ACCOUNT= 2 | -------------------------------------------------------------------------------- /cdk/jenkins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jenkins-on-aws/4eb20872954c83b9b5cfc9d1853f9815ecc3000c/cdk/jenkins/__init__.py -------------------------------------------------------------------------------- /cdk/jenkins/ecs.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | aws_ecs, 3 | aws_ec2, 4 | aws_efs, 5 | core 6 | ) 7 | 8 | from configparser import ConfigParser 9 | 10 | config = ConfigParser() 11 | config.read('config.ini') 12 | 13 | 14 | class ECSCluster(core.Stack): 15 | 16 | def __init__(self, scope: core.Stack, id: str, vpc, service_discovery_namespace, **kwargs): 17 | super().__init__(scope, id, **kwargs) 18 | self.vpc = vpc 19 | self.service_discovery_namespace = service_discovery_namespace 20 | 21 | # Create VPC for cluster - best practice is to isolate jenkins to its own vpc 22 | self.cluster = aws_ecs.Cluster( 23 | self, "ECSCluster", 24 | vpc=self.vpc, 25 | default_cloud_map_namespace=aws_ecs.CloudMapNamespaceOptions(name=service_discovery_namespace) 26 | ) 27 | 28 | if config['DEFAULT']['ec2_enabled'] == "yes": 29 | self.asg = self.cluster.add_capacity( 30 | "Ec2", 31 | instance_type=aws_ec2.InstanceType(config['DEFAULT']['instance_type']), 32 | key_name="jenkinsonaws", 33 | ) 34 | 35 | self.efs_sec_grp = aws_ec2.SecurityGroup( 36 | self, "EFSSecGrp", 37 | vpc=self.vpc, 38 | allow_all_outbound=True, 39 | ) 40 | 41 | self.efs_sec_grp.add_ingress_rule( 42 | peer=self.cluster.connections.security_groups[0], 43 | connection=aws_ec2.Port(protocol=aws_ec2.Protocol.ALL,string_representation="ALL",from_port=2049,to_port=2049), 44 | description="EFS" 45 | ) 46 | 47 | self.efs_filesystem = aws_efs.CfnFileSystem( 48 | self, "EFSBackend", 49 | ) 50 | 51 | counter = 0 52 | for subnet in self.vpc.private_subnets: 53 | aws_efs.CfnMountTarget( 54 | self, "EFS{}".format(counter), 55 | file_system_id=self.efs_filesystem.ref, 56 | subnet_id=subnet.subnet_id, 57 | security_groups=[ 58 | self.efs_sec_grp.security_group_id 59 | ] 60 | ) 61 | counter += 1 62 | 63 | self.user_data = """ 64 | sudo yum install -y amazon-efs-utils 65 | sudo mkdir /mnt/efs 66 | sudo chown -R ec2-user: /mnt/efs 67 | sudo chmod -R 0777 /mnt/efs 68 | sudo mount -t efs -o tls /mnt/efs {}:/ efs 69 | """.format(self.efs_filesystem.ref) 70 | 71 | self.asg.add_user_data(self.user_data) 72 | 73 | -------------------------------------------------------------------------------- /cdk/jenkins/jenkins_leader.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | aws_ecs_patterns as ecs_patterns, 3 | aws_ecs as ecs, 4 | aws_ecr_assets as ecr, 5 | aws_ec2 as ec2, 6 | aws_servicediscovery as sd, 7 | aws_iam as iam, 8 | aws_logs as logs, 9 | aws_elasticloadbalancingv2 as elb, 10 | core 11 | ) 12 | 13 | from configparser import ConfigParser 14 | 15 | config = ConfigParser() 16 | config.read('config.ini') 17 | 18 | 19 | class JenkinsLeader(core.Stack): 20 | 21 | def __init__(self, scope: core.Stack, id: str, cluster, vpc, worker, **kwargs) -> None: 22 | super().__init__(scope, id, **kwargs) 23 | self.cluster = cluster 24 | self.vpc = vpc 25 | self.worker = worker 26 | 27 | # Building a custom image for jenkins leader. 28 | self.container_image = ecr.DockerImageAsset( 29 | self, "JenkinsleaderDockerImage", 30 | directory='./docker/leader/' 31 | ) 32 | 33 | if config['DEFAULT']['fargate_enabled'] == "yes" or not config['DEFAULT']['ec2_enabled'] == "yes": 34 | # Task definition details to define the Jenkins leader container 35 | self.jenkins_task = ecs_patterns.ApplicationLoadBalancedTaskImageOptions( 36 | image=ecs.ContainerImage.from_docker_image_asset(self.container_image), 37 | container_port=8080, 38 | enable_logging=True, 39 | environment={ 40 | # https://github.com/jenkinsci/docker/blob/leader/README.md#passing-jvm-parameters 41 | 'JAVA_OPTS': '-Djenkins.install.runSetupWizard=false', 42 | # https://github.com/jenkinsci/configuration-as-code-plugin/blob/leader/README.md#getting-started 43 | 'CASC_JENKINS_CONFIG': '/config-as-code.yaml', 44 | 'network_stack': self.vpc.stack_name, 45 | 'cluster_stack': self.cluster.stack_name, 46 | 'worker_stack': self.worker.stack_name, 47 | 'cluster_arn': self.cluster.cluster.cluster_arn, 48 | 'aws_region': config['DEFAULT']['region'], 49 | 'jenkins_url': config['DEFAULT']['jenkins_url'], 50 | 'subnet_ids': ",".join([x.subnet_id for x in self.vpc.vpc.private_subnets]), 51 | 'security_group_ids': self.worker.worker_security_group.security_group_id, 52 | 'execution_role_arn': self.worker.worker_execution_role.role_arn, 53 | 'task_role_arn': self.worker.worker_task_role.role_arn, 54 | 'worker_log_group': self.worker.worker_logs_group.log_group_name, 55 | 'worker_log_stream_prefix': self.worker.worker_log_stream.log_stream_name 56 | }, 57 | ) 58 | 59 | # Create the Jenkins leader service 60 | self.jenkins_leader_service_main = ecs_patterns.ApplicationLoadBalancedFargateService( 61 | self, "JenkinsleaderService", 62 | cpu=int(config['DEFAULT']['fargate_cpu']), 63 | memory_limit_mib=int(config['DEFAULT']['fargate_memory_limit_mib']), 64 | cluster=self.cluster.cluster, 65 | desired_count=1, 66 | enable_ecs_managed_tags=True, 67 | task_image_options=self.jenkins_task, 68 | cloud_map_options=ecs.CloudMapOptions(name="leader", dns_record_type=sd.DnsRecordType('A')) 69 | ) 70 | 71 | self.jenkins_leader_service = self.jenkins_leader_service_main.service 72 | self.jenkins_leader_task = self.jenkins_leader_service.task_definition 73 | 74 | if config['DEFAULT']['ec2_enabled'] == "yes": 75 | self.jenkins_load_balancer = elb.ApplicationLoadBalancer( 76 | self, "JenkinsleaderELB", 77 | vpc=self.vpc.vpc, 78 | internet_facing=True, 79 | ) 80 | 81 | self.listener = self.jenkins_load_balancer.add_listener("Listener", port=80) 82 | 83 | self.jenkins_leader_task = ecs.Ec2TaskDefinition( 84 | self, "JenkinsleaderTaskDef", 85 | network_mode=ecs.NetworkMode.AWS_VPC, 86 | volumes=[ecs.Volume(name="efs_mount", host=ecs.Host(source_path='/mnt/efs'))], 87 | ) 88 | 89 | self.jenkins_leader_task.add_container( 90 | "JenkinsleaderContainer", 91 | image=ecs.ContainerImage.from_ecr_repository(self.container_image.repository), 92 | cpu=int(config['DEFAULT']['ec2_cpu']), 93 | memory_limit_mib=int(config['DEFAULT']['ec2_memory_limit_mib']), 94 | environment={ 95 | # https://github.com/jenkinsci/docker/blob/leader/README.md#passing-jvm-parameters 96 | 'JAVA_OPTS': '-Djenkins.install.runSetupWizard=false', 97 | # https://github.com/jenkinsci/configuration-as-code-plugin/blob/leader/README.md#getting-started 98 | 'CASC_JENKINS_CONFIG': '/config-as-code.yaml', 99 | 'network_stack': self.vpc.stack_name, 100 | 'cluster_stack': self.cluster.stack_name, 101 | 'worker_stack': self.worker.stack_name, 102 | 'cluster_arn': self.cluster.cluster.cluster_arn, 103 | 'aws_region': config['DEFAULT']['region'], 104 | 'jenkins_url': config['DEFAULT']['jenkins_url'], 105 | 'subnet_ids': ",".join([x.subnet_id for x in self.vpc.vpc.private_subnets]), 106 | 'security_group_ids': self.worker.worker_security_group.security_group_id, 107 | 'execution_role_arn': self.worker.worker_execution_role.role_arn, 108 | 'task_role_arn': self.worker.worker_task_role.role_arn, 109 | 'worker_log_group': self.worker.worker_logs_group.log_group_name, 110 | 'worker_log_stream_prefix': self.worker.worker_log_stream.log_stream_name 111 | }, 112 | logging=ecs.LogDriver.aws_logs( 113 | stream_prefix="Jenkinsleader", 114 | log_retention=logs.RetentionDays.ONE_WEEK 115 | ), 116 | ) 117 | 118 | self.jenkins_leader_task.default_container.add_mount_points( 119 | ecs.MountPoint( 120 | container_path='/var/jenkins_home', 121 | source_volume="efs_mount", 122 | read_only=False 123 | ) 124 | ) 125 | 126 | self.jenkins_leader_task.default_container.add_port_mappings( 127 | ecs.PortMapping( 128 | container_port=8080, 129 | host_port=8080 130 | ) 131 | ) 132 | 133 | self.jenkins_leader_service = ecs.Ec2Service( 134 | self, "EC2leaderService", 135 | task_definition=self.jenkins_leader_task, 136 | cloud_map_options=ecs.CloudMapOptions(name="leader", dns_record_type=sd.DnsRecordType('A')), 137 | desired_count=1, 138 | min_healthy_percent=0, 139 | max_healthy_percent=100, 140 | enable_ecs_managed_tags=True, 141 | cluster=self.cluster.cluster, 142 | ) 143 | 144 | self.target_group = self.listener.add_targets( 145 | "JenkinsleaderTarget", 146 | port=80, 147 | targets=[ 148 | self.jenkins_leader_service.load_balancer_target( 149 | container_name=self.jenkins_leader_task.default_container.container_name, 150 | container_port=8080, 151 | ) 152 | ], 153 | deregistration_delay=core.Duration.seconds(10) 154 | ) 155 | 156 | # Opening port 5000 for leader <--> worker communications 157 | self.jenkins_leader_service.task_definition.default_container.add_port_mappings( 158 | ecs.PortMapping(container_port=50000, host_port=50000) 159 | ) 160 | 161 | # Enable connection between leader and Worker 162 | self.jenkins_leader_service.connections.allow_from( 163 | other=self.worker.worker_security_group, 164 | port_range=ec2.Port( 165 | protocol=ec2.Protocol.TCP, 166 | string_representation='leader to Worker 50000', 167 | from_port=50000, 168 | to_port=50000 169 | ) 170 | ) 171 | 172 | # Enable connection between leader and Worker on 8080 173 | self.jenkins_leader_service.connections.allow_from( 174 | other=self.worker.worker_security_group, 175 | port_range=ec2.Port( 176 | protocol=ec2.Protocol.TCP, 177 | string_representation='leader to Worker 8080', 178 | from_port=8080, 179 | to_port=8080 180 | ) 181 | ) 182 | 183 | # IAM Statements to allow jenkins ecs plugin to talk to ECS as well as the Jenkins cluster # 184 | self.jenkins_leader_task.add_to_task_role_policy( 185 | iam.PolicyStatement( 186 | actions=[ 187 | "ecs:RegisterTaskDefinition", 188 | "ecs:DeregisterTaskDefinition", 189 | "ecs:ListClusters", 190 | "ecs:DescribeContainerInstances", 191 | "ecs:ListTaskDefinitions", 192 | "ecs:DescribeTaskDefinition", 193 | "ecs:DescribeTasks" 194 | ], 195 | resources=[ 196 | "*" 197 | ], 198 | ) 199 | ) 200 | 201 | self.jenkins_leader_task.add_to_task_role_policy( 202 | iam.PolicyStatement( 203 | actions=[ 204 | "ecs:ListContainerInstances" 205 | ], 206 | resources=[ 207 | self.cluster.cluster.cluster_arn 208 | ] 209 | ) 210 | ) 211 | 212 | self.jenkins_leader_task.add_to_task_role_policy( 213 | iam.PolicyStatement( 214 | actions=[ 215 | "ecs:RunTask" 216 | ], 217 | resources=[ 218 | "arn:aws:ecs:{0}:{1}:task-definition/fargate-workers*".format( 219 | self.region, 220 | self.account, 221 | ) 222 | ] 223 | ) 224 | ) 225 | 226 | self.jenkins_leader_task.add_to_task_role_policy( 227 | iam.PolicyStatement( 228 | actions=[ 229 | "ecs:StopTask" 230 | ], 231 | resources=[ 232 | "arn:aws:ecs:{0}:{1}:task/*".format( 233 | self.region, 234 | self.account 235 | ) 236 | ], 237 | conditions={ 238 | "ForAnyValue:ArnEquals": { 239 | "ecs:cluster": self.cluster.cluster.cluster_arn 240 | } 241 | } 242 | ) 243 | ) 244 | 245 | self.jenkins_leader_task.add_to_task_role_policy( 246 | iam.PolicyStatement( 247 | actions=[ 248 | "iam:PassRole" 249 | ], 250 | resources=[ 251 | self.worker.worker_task_role.role_arn, 252 | self.worker.worker_execution_role.role_arn 253 | ] 254 | ) 255 | ) 256 | # END OF JENKINS ECS PLUGIN IAM POLICIES # 257 | -------------------------------------------------------------------------------- /cdk/jenkins/jenkins_worker.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | aws_ecr_assets as ecr, 3 | aws_ec2 as ec2, 4 | aws_iam as iam, 5 | aws_logs as logs, 6 | core 7 | ) 8 | 9 | from configparser import ConfigParser 10 | 11 | config = ConfigParser() 12 | config.read('config.ini') 13 | 14 | class JenkinsWorker(core.Stack): 15 | 16 | def __init__(self, scope: core.Stack, id: str, vpc, cluster, **kwargs) -> None: 17 | super().__init__(scope, id, **kwargs) 18 | self.vpc = vpc 19 | self.cluster = cluster 20 | 21 | # Building a custom image for jenkins leader. 22 | self.container_image = ecr.DockerImageAsset( 23 | self, "JenkinsWorkerDockerImage", 24 | directory='./docker/worker/' 25 | ) 26 | 27 | # Security group to connect workers to leader 28 | self.worker_security_group = ec2.SecurityGroup( 29 | self, "WorkerSecurityGroup", 30 | vpc=self.vpc, 31 | description="Jenkins Worker access to Jenkins leader", 32 | ) 33 | 34 | # IAM execution role for the workers to pull from ECR and push to CloudWatch logs 35 | self.worker_execution_role = iam.Role( 36 | self, "WorkerExecutionRole", 37 | assumed_by=iam.ServicePrincipal('ecs-tasks.amazonaws.com'), 38 | ) 39 | 40 | self.worker_execution_role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name( 41 | 'service-role/AmazonECSTaskExecutionRolePolicy' 42 | ) 43 | ) 44 | 45 | # Task role for worker containers - add to this role for any aws resources that jenkins requires access to 46 | self.worker_task_role = iam.Role( 47 | self, "WorkerTaskRole", 48 | assumed_by=iam.ServicePrincipal('ecs-tasks.amazonaws.com'), 49 | ) 50 | 51 | # Create log group for workers to log 52 | self.worker_logs_group = logs.LogGroup( 53 | self, "WorkerLogGroup", 54 | retention=logs.RetentionDays.ONE_DAY 55 | ) 56 | 57 | # Create log stream for worker log group 58 | self.worker_log_stream = logs.LogStream( 59 | self, "WorkerLogStream", 60 | log_group=self.worker_logs_group 61 | ) 62 | 63 | -------------------------------------------------------------------------------- /cdk/jenkins/network.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | aws_ec2, 3 | core 4 | ) 5 | from configparser import ConfigParser 6 | 7 | config = ConfigParser() 8 | config.read('config.ini') 9 | 10 | 11 | class Network(core.Stack): 12 | 13 | def __init__(self, scope: core.Stack, id: str, **kwargs): 14 | super().__init__(scope, id, **kwargs) 15 | 16 | self.vpc = aws_ec2.Vpc( 17 | self, "Vpc", 18 | cidr=config['DEFAULT']['cidr'], 19 | ) 20 | 21 | -------------------------------------------------------------------------------- /cdk/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk.core==1.53.0 2 | aws-cdk.aws_ecs==1.53.0 3 | aws-cdk.aws_ecs_patterns==1.53.0 4 | aws-cdk.aws_elasticloadbalancingv2==1.53.0 5 | aws-cdk.aws_efs==1.53.0 6 | aws-cdk.aws_iam==1.53.0 7 | aws-cdk.aws_servicediscovery==1.53.0 8 | aws-cdk.aws_ecr==1.53.0 9 | aws-cdk.aws_ecr_assets==1.53.0 10 | -------------------------------------------------------------------------------- /cdk/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | with open("README.md") as fp: 5 | long_description = fp.read() 6 | 7 | 8 | setuptools.setup( 9 | name="cdk", 10 | version="0.0.1", 11 | 12 | description="An empty CDK Python app", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | 16 | author="author", 17 | 18 | package_dir={"": "cdk"}, 19 | packages=setuptools.find_packages(where="cdk"), 20 | 21 | install_requires=[ 22 | "aws-cdk.core", 23 | ], 24 | 25 | python_requires=">=3.6", 26 | 27 | classifiers=[ 28 | "Development Status :: 4 - Beta", 29 | 30 | "Intended Audience :: Developers", 31 | 32 | "License :: OSI Approved :: Apache Software License", 33 | 34 | "Programming Language :: JavaScript", 35 | "Programming Language :: Python :: 3 :: Only", 36 | "Programming Language :: Python :: 3.6", 37 | "Programming Language :: Python :: 3.7", 38 | "Programming Language :: Python :: 3.8", 39 | 40 | "Topic :: Software Development :: Code Generators", 41 | "Topic :: Utilities", 42 | 43 | "Typing :: Typed", 44 | ], 45 | ) 46 | -------------------------------------------------------------------------------- /cdk/setup_env: -------------------------------------------------------------------------------- 1 | python3 -m venv .env 2 | . .env/bin/activate 3 | pip3 install -r requirements.txt -------------------------------------------------------------------------------- /cdk/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jenkins-on-aws/4eb20872954c83b9b5cfc9d1853f9815ecc3000c/cdk/tests/__init__.py -------------------------------------------------------------------------------- /cdk/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jenkins-on-aws/4eb20872954c83b9b5cfc9d1853f9815ecc3000c/cdk/tests/unit/__init__.py -------------------------------------------------------------------------------- /cdk/tests/unit/test_hello_construct.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from aws_cdk import core 4 | 5 | from hello.hello_construct import HelloConstruct 6 | 7 | class TestHelloConstruct(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.app = core.App() 11 | self.stack = core.Stack(self.app, "TestStack") 12 | 13 | def test_num_buckets(self): 14 | num_buckets = 10 15 | hello = HelloConstruct(self.stack, "Test1", num_buckets) 16 | assert len(hello.buckets) == num_buckets --------------------------------------------------------------------------------