├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CREDITS.md ├── LICENSE ├── README.md ├── SETUP-BASTION-HOST.md ├── SETUP-CLOUD9.md ├── SETUP-PRIVATECA.md ├── THIRD-PARTY-NOTICES.md ├── assets ├── images │ ├── 1-c9-create.png │ ├── 10-jenkins-deploy-role.png │ ├── 11-c9-cdk-deploy.png │ ├── 12-jenkins-dashboard.png │ ├── 13-jenkins-sample-jobs.png │ ├── 14-jenkins-sample-job-validation-build.png │ ├── 2-c9-launch.png │ ├── 20-cdk-destroy.png │ ├── 3-launch-iam-role.png │ ├── 4-iam-permission.png │ ├── 5-create-iam-role.png │ ├── 6-c9-modify-role.png │ ├── 7-c9-attach-role.png │ ├── 8-c9-disable-temp-credentials.png │ ├── 9-c9-set-account.png │ ├── architecture.png │ ├── rdp-1-instance-connect.png │ ├── rdp-2-instance-connect-kp.png │ ├── rdp-3-instance-connect-decrypt-pw.png │ ├── rdp-4-instance-connect-get-pw.png │ ├── rdp-5-connect-login.png │ ├── rdp-6-connect-prompt.png │ ├── rdp-7-connect-jenkins-ssl.png │ ├── rdp-8-import-private-root-ca.png │ └── rdp-9-import-private-root-ca.png └── templates │ ├── cfn-cloud9-instance.yaml │ ├── cfn-jenkins-deployment-role.yaml │ └── cfn-windows-bastion-host.yaml ├── cdk ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── cdk.ts ├── cdk.json ├── jest.config.js ├── lib │ ├── cdk-app.ts │ ├── cdk-ecr-stack.ts │ ├── cdk-infra-stack.ts │ ├── cdk-pipeline-ecr-stage.ts │ ├── cdk-pipeline-infra-stage.ts │ ├── cdk-pipeline-stack.ts │ └── cdk-util.ts ├── package-lock.json ├── package.json ├── test │ ├── cdk-app.test.ts │ ├── cdk-ecr-stack.test.ts │ ├── cdk-infra-stack.test.ts │ ├── cdk-pipeline-stack.test.ts │ └── cdk-util.test.ts └── tsconfig.json ├── jenkins ├── README.md ├── agent │ └── Dockerfile └── controller │ ├── Dockerfile │ └── config │ ├── custom.groovy │ ├── jenkins.yaml │ └── plugins.txt └── sample ├── pipeline-ec2 ├── Jenkinsfile └── cfn-webserver.yaml └── pipeline-s3 ├── Jenkinsfile └── cfn-s3.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | cdk.context.json 10 | 11 | # System files 12 | **/.DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2023-04-07 9 | 10 | ### Added 11 | 12 | - Initial Release 13 | 14 | ## Features 15 | - Jenkins Configuration as Code with required plugins. 16 | - Jenkins Configuration with Amazon ECS plugin. 17 | - Jenkins custom controller and agent container builds with private root CA import. 18 | - Jenkins global environment variables and sample job configurations as code. 19 | - Jenkins sample cross account (workload) deployment from DevOps account. 20 | - Jenkins persistent file system leveraging EFS. 21 | - Jenkins sample job configurations as code and sample CloudFormation templates demonstrating integration with Github. 22 | - Jenkins Private Certificate Authority setup with self-signed (default mode) certificate and optional AWS Private CA mode. 23 | - Jenkins deployment to private hosted zone with restricted access from Bastion host. 24 | - Leveraging values from Secrets Manager and Parameter Store in CDK. 25 | - CDK Pipeline using parallel stages and post hooks. 26 | - CDK Code Build authentication with Docker Registry using Secrets Manager. 27 | - Docker cached build and image tag versioning using parameter store. 28 | - ECR Repo Creation, Lifecycle rules and Container Scanning. 29 | - Dynamic variables using CDK context.json. 30 | - CDK Unit tests for the CDK project. 31 | 32 | 33 | ## [1.0.1] - 2023-07-25 34 | 35 | ### Added 36 | 37 | - CDK upgrade and minor updates 38 | 39 | ## Features 40 | - Upgrade CDK to 2.88.0 41 | - Updated the Jenkins Configuration Plugin. 42 | - Updated README and steps. -------------------------------------------------------------------------------- /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, or recently closed, 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 *main* 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' 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](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | ## Credits 2 | 3 | Thank you to the reviewers of initial release v1.0.0 and any future contributors to this project. 4 | 5 | ### Reviewers (AWS) 6 | - Janardhan Molumuri 7 | - Jeremy Schiefer 8 | 9 | ### Contributors 10 | - [Contributors](https://github.com/aws-samples/aws-jenkins-ecs-cdk/graphs/contributors) to this repository. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Build and Deploy Jenkins as code to ECS Fargate using AWS CDK. 2 | 3 | ## Overview 4 | Leverage AWS services Amazon Elastic Container Service (ECS), Amazon Elastic Container Registry (ECR), AWS Certificate Manager, AWS Private Certificate Authority, Amazon Route 53, AWS CodePipeline, AWS CodeCommit, AWS CodeBuild and AWS CDK to deploy highly available and fault tolerant Jenkins Controller and Agent Infrastructure as Code (IaC) and build out a CI/CD pipeline for deploying changes to Jenkins Infrastructure. We will build custom Jenkins Controller and Agent images with the necessary plugins and configurations and leverage Jenkins Configuration as Code to build and push custom images to ECR for both Controller and Agent. We will also deploy a deployment role into a workload account that will allow Jenkins to deploy cloudformation stack to demonstrate cross account deployment using Jenkins. 5 | 6 | ## Architecture 7 | 8 | ![Architecture](assets/images/architecture.png) 9 | 10 | ## Repository Structure 11 | - jenkins - Jenkins build and configurations. For more details review the [README](jenkins/README.md) 12 | - cdk - AWS CDK Project for Building and Deploying Jenkins to ECS Fargate. For more details review the [README](cdk/README.md) 13 | 14 | ## Configure Development Environment 15 | - Follow the steps in the Cloud9 Environment Setup guide - [Setup-Cloud9-Environment](SETUP-CLOUD9.md) 16 | 17 | ## Environment Setup and Local Testing 18 | 19 | 1. Setup and Install Typescript: 20 | ```bash 21 | cd ${HOME}/environment 22 | npm install -g typescript 23 | export CDK_DEFAULT_ACCOUNT=$(aws sts get-caller-identity --output text --query Account) 24 | export CDK_DEFAULT_REGION=$(curl -s 169.254.169.254/latest/dynamic/instance-identity/document | jq -r '.region') 25 | ``` 26 | 27 | 2. Bootstrap AWS CDK: 28 | ```bash 29 | cdk bootstrap aws://$CDK_DEFAULT_ACCOUNT/$CDK_DEFAULT_REGION 30 | ``` 31 | 32 | 3. Clone the source code repository from aws-samples: 33 | ```bash 34 | cd ~/environment 35 | git clone https://github.com/aws-samples/aws-jenkins-ecs-cdk.git 36 | ``` 37 | 38 | 4. Setup Git User: 39 | ``` 40 | git config --global user.name "workshop-admin" 41 | git config --global user.email "workshop-admin@example.com" 42 | ``` 43 | 44 | 5. NPM Install: 45 | ``` 46 | cd ~/environment/aws-jenkins-ecs-cdk/cdk 47 | npm install @types/node 48 | ``` 49 | 50 | ## Create Private CA using AWS Private Certificate Authority. 51 | - CDK Stack will deploy a private hosted zone and an internal ALB. Jenkins will be backed by an ALB that will be accessible from only within the VPC through a Bastion Host. ALB Certificate will be issued through ACM through a Private Certificate Authority. 52 | - Follow the steps in the Private CA Setup guide to complete the Private CA setup - [Setup Private CA](SETUP-PRIVATECA.md) 53 | 54 | ## Build the infrastructure and pipeline using AWS CDK and validate the Jenkins application. 55 | 56 | **Note:** This workshop will create chargeable resources in your account. When finished, please make sure you clean up resources as instructed at the end. 57 | 58 | 1. Please review [README](cdk/README.md) for the default values that are used from cdk.json. 59 | 60 | 2. Current default for **ctxHostedZoneName** is set to **internal.anycompany.com**. Update **ctxHostedZoneName** with appropriate value in the **cdk.json** file and this needs to align with the Private CA configuration. Please review the default values for remaining parameters and update as needed. 61 | 62 | 3. Set up DockerHub Credentials in Secrets Manager for Docker Login credentials. Please provide your personal docker username (not email) and password. 63 | ```bash 64 | aws secretsmanager create-secret \ 65 | --name dockerhub_credentials \ 66 | --description "DockerHub Credentials" \ 67 | --secret-string "{\"username\":\"dockerhub_username\",\"password\":\"dockerhub_password\"}" \ 68 | --region=$CDK_DEFAULT_REGION 69 | ``` 70 | 71 | 4. Set up Jenkins Admin Credentials in Secrets Manager for Jenkins Login credentials. Please provide jenkins username (e.g. admin) and password. 72 | ```bash 73 | aws secretsmanager create-secret \ 74 | --name "/dev/jenkins/admin/credentials" \ 75 | --description "Jenkins Dev Admin User Credentials" \ 76 | --secret-string "{\"username\":\"jenkins_username\",\"password\":\"jenkins_password\"}" \ 77 | --region=$CDK_DEFAULT_REGION 78 | ``` 79 | **Note** Update values for **ctxJenkinsAdminCredentialSecretName** and **ctxDevTeam1Workload1AWSAccountIdParameterName** in **cdk.json** if you do override the secret names in step 4 and 5 above. 80 | 81 | 5. Please identify the **workload** account to allow Jenkins from **DevOps** account to deploy to and run the following command. 82 | ```bash 83 | aws ssm put-parameter \ 84 | --name "/dev/team1/workload1/AWSAccountID" \ 85 | --value "" \ 86 | --type String \ 87 | --region=$CDK_DEFAULT_REGION 88 | ``` 89 | 90 | 6. Run the CloudFormation template available [here](assets/images/images/cf-jenkins-deployment-role.yaml) in the workload account and provide the **DevOps AWSAccountId**. Please review/update the Jenkins deployment role based on your requirements to limit the role to specific permissions needed for Jenkins Agent. 91 | 92 | ![Jenkins Deploy Role](assets/images/10-jenkins-deploy-role.png) 93 | 94 | 7. Deploy **Step Phase 1**: Create Code Commit Repo 95 | - Setup the Code Commit Repo. 96 | ```bash 97 | cd ~/environment/aws-jenkins-ecs-cdk/cdk 98 | cdk deploy 99 | ``` 100 | - When asked whether you want to proceed with the actions, enter `y`. 101 | 102 | ![CDK Deploy](assets/images/11-c9-cdk-deploy.png) 103 | 104 | - Wait for AWS CDK to complete the deployment before proceeding. It will take few minutes to complete `cdk deploy`. Pipeline will fail since the code is not available in code commit repo. 105 | 106 | 8. Deploy **Step Phase 2**: Enable CI/CD Pipeline and deploy infrastructure and application. 107 | - Commit the code and set the origin to the code commit repo that was created. 108 | ```bash 109 | cd ~/environment/aws-jenkins-ecs-cdk 110 | git remote rename origin upstream 111 | git remote add origin "provide codecommit repo HTTPS URL created in above step" 112 | git commit -am "Initial Commit" 113 | git push origin main 114 | ``` 115 | - Code Commit changes will invoke the Code Pipeline. It will take approximately 10 to 15 minutes to complete the deployment. 116 | - Initial deployment will take longer since this includes building the container image for controller and agent and setting up the infrastructure that includes Networking, EFS, ECS Cluster and other dependent components. 117 | - Explore the deployment progress on the CloudFormation console. 118 | 119 | 9. Follow the steps in the Bastion Host Setup guide to complete the Bastion Host setup - [Bastion Host Setup](SETUP-BASTION-HOST.md) and RDP into the Bastion Host. 120 | 121 | 10. Validate the Deployment. 122 | - Access the Jenkins endpoint available at `https://jenkins-dev.internal.anycompany.com` 123 | - Login using the Jenkins Admin User Credentails that you provided in step 4. 124 | 125 | 11. Review the deployed stack. 126 | - View the ECS cluster using the [Amazon ECS console](https://console.aws.amazon.com/ecs). 127 | - View the ECR repo using the [Amazon ECR console](https://console.aws.amazon.com/ecr). 128 | - View the EFS using the [Amazon EFS console](https://console.aws.amazon.com/efs). 129 | - View the CodeCommit repo using the [AWS CodeCommit console](https://console.aws.amazon.com/codecommit). 130 | - View the CodePipeline using the [AWS CodePipeline console](https://console.aws.amazon.com/codepipeline). 131 | - View the CloudFormation using the [AWS CloudFormation console](https://console.aws.amazon.com/cloudformation). 132 | - Review and validate Jenkins application by logging into the Jenkins using admin credentials. 133 | ![CDK Deploy](assets/images/12-jenkins-dashboard.png) 134 | 135 | 12. Review the sample jenkins jobs. The deployment includes three jobs that are configured as code as part of the controller image that is built using the pipeline. 136 | 137 | ![CDK Deploy](assets/images/13-jenkins-sample-jobs.png) 138 | 139 | - **ecs-fargate-validate-configuration** - Job to validate the Jenkins deployment and configuration for controller and agent. 140 | - **ecs-fargate-validate-devops-account-create-s3-bucket** - Job configured to checkout project from GitHub SCM and deploy CloudFormation template to create an S3 bucket in **DevOps** account using Jenkinsfile and pre-configured environment variables. 141 | - **ecs-fargate-validate-workload-account-deploy-ec2-webserver** - Job configured to checkout project from GitHub and deploy CloudFormation template to create an EC2 instance in **Workload** account to default VPC using Jenkinsfile and pre-configured environment variables. 142 | 143 | 13. Validate the CI/CD Pipeline. 144 | - Add a new plugin to the plugins.txt for example **sonar:2.15** 145 | - Commit the change and push the change to the code commit repo. 146 | ```bash 147 | cd ~/environment/aws-jenkins-ecs-cdk 148 | git commit -am "Added Sonar Plugin" 149 | git push origin main 150 | ``` 151 | - Review that the commit started the code pipeline build. It will take approximately 10 minutes to deploy the change which includes building a new Controller image and push the new tag version to Parameter Store. Pipeline should not publish a new agent image, since no changes were made to agent. In order to have more control on ECR repostitory and lifecycle we have avoided the usage of [CDK ECR Asssets](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecr_assets-readme.html). 152 | - Image tag versions are stored in SSM parameters only when there is change to the image due to changes in layers to the docker image. SSM parameter also helps with propagating the change to deploy new image only when changed**. Pipeline should also create a new task definition for the Controller and ECS cluster should **deploy the latest version of the Controller since only the Controller image changed during this deployment. 153 | - Login using the Jenkins Admin User Credentails and validate the list of installed plugins and you should see SonarQube version 2.15 installed successfully. 154 | 155 | 14. Cleaning Up. 156 | - Delete the container images from both controller and agent ECR repositories. 157 | ```bash 158 | aws ecr batch-delete-image --region $CDK_DEFAULT_REGION --repository-name jenkins-controller --image-ids "$(aws ecr list-images --repository-name jenkins-controller --region $CDK_DEFAULT_REGION | jq -r '.imageIds')" 159 | 160 | aws ecr batch-delete-image --region $CDK_DEFAULT_REGION --repository-name jenkins-agent --image-ids "$(aws ecr list-images --repository-name jenkins-agent --region $CDK_DEFAULT_REGION | jq -r '.imageIds')" 161 | ``` 162 | - Delete the Windows Bastion Host stack and wait for completion. 163 | ```bash 164 | aws cloudformation delete-stack --stack-name jenkins-bastion-host-stack --region $CDK_DEFAULT_REGION 165 | aws cloudformation wait stack-delete-complete --stack-name jenkins-bastion-host-stack --region $CDK_DEFAULT_REGION 166 | ``` 167 | 168 | - List all the stacks. 169 | ``` 170 | cd ~/environment/aws-jenkins-ecs-cdk/cdk 171 | cdk list 172 | ``` 173 | - Review the list of stacks and run the delete stack for each one. When asked whether you want to proceed with the actions, enter `y`. Wait for AWS CDK to complete the destroy. 174 | ``` 175 | cdk destroy jenkins-on-ecs-stack/jenkins-iac-dev-deploy/app 176 | cdk destroy jenkins-on-ecs-stack/jenkins-iac-dev-build-image/ecr 177 | cdk destroy jenkins-on-ecs-stack 178 | ``` 179 | 180 | - Delete the secrets from Secrets Manager. 181 | ```bash 182 | aws secretsmanager delete-secret --secret-id dockerhub_credentials --recovery-window-in-days 7 --region $CDK_DEFAULT_REGION 183 | aws secretsmanager delete-secret --secret-id "/dev/jenkins/admin/credentials" --recovery-window-in-days 7 --region $CDK_DEFAULT_REGION 184 | ``` 185 | - Delete the parameters from Parameter Store. 186 | ```bash 187 | aws ssm delete-parameter --name "/dev/team1/workload1/AWSAccountID" --region $CDK_DEFAULT_REGION 188 | aws ssm delete-parameter --name "/dev/jenkins/controller/docker/image/tag" --region $CDK_DEFAULT_REGION 189 | aws ssm delete-parameter --name "/dev/jenkins/agent/docker/image/tag" --region $CDK_DEFAULT_REGION 190 | ``` 191 | 192 | - Self Signed CA Setup (Default) - Delete the ACM Certificate ARN from parameter store and Root CA from Secrets Manager. 193 | ```bash 194 | ACM_CERT_AUTHORITY_ARN=$(aws ssm get-parameter --name "/dev/jenkins/acm/selfSignedCertificateArn" --query "Parameter.Value" --output text --region $CDK_DEFAULT_REGION) 195 | aws acm delete-certificate --certificate-arn $ACM_CERT_AUTHORITY_ARN --region $CDK_DEFAULT_REGION 196 | aws ssm delete-parameter --name "/dev/jenkins/acm/selfSignedCertificateArn" --region $CDK_DEFAULT_REGION 197 | aws secretsmanager delete-secret --secret-id "/dev/jenkins/rootCA" --recovery-window-in-days 7 --region $CDK_DEFAULT_REGION 198 | ``` 199 | 200 | - AWS Private CA Setup - Delete the AWS Private CA and associated parameter from Parameter Store and Root CA from Secrets Manager. 201 | ```bash 202 | PCA_CERT_AUTHORITY_ARN=$(aws ssm get-parameter --name "/dev/jenkins/acmpca/pcaAuthorityArn" --query "Parameter.Value" --output text --region $CDK_DEFAULT_REGION) 203 | aws acm-pca delete-certificate-authority \ 204 | --certificate-authority-arn $PCA_CERT_AUTHORITY_ARN \ 205 | --permanent-deletion-time-in-days 7 206 | --region $CDK_DEFAULT_REGION 207 | aws ssm delete-parameter --name "/dev/jenkins/acmpca/pcaAuthorityArn" --region $CDK_DEFAULT_REGION 208 | aws secretsmanager delete-secret --secret-id "/dev/jenkins/rootCA" --recovery-window-in-days 7 --region $CDK_DEFAULT_REGION 209 | ``` 210 | 211 | - Delete the Cloud9 Instance from AWS Console. 212 | 213 | ## Security 214 | 215 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 216 | 217 | ## License 218 | 219 | This library is licensed under the MIT-0 License. See the LICENSE file. -------------------------------------------------------------------------------- /SETUP-BASTION-HOST.md: -------------------------------------------------------------------------------- 1 | ## Bastion Host Setup for Jenkins Access 2 | 3 | Ensure that you have access completed the Cloud9 setup and have successfully completed step 8 (from README) prior to working through the below steps. Bastion host will be used to access Jenkins application from trusted network and/or IP space. **Please ensure that the AllowedIP restricts to the IP and/or CIDR for Jenkins Admins.** 4 | 5 | ## Bastion Host Setup steps 6 | 7 | **Note:** Below instructions provide steps to complete the setup using AWS CLI. You can also use AWS Console to deploy the CloudFormation Stack. 8 | 9 | 1. Create a keypair. Keypair will be used to decrypt the password for the Bastion Host once it is setup. 10 | - Create a keypair. 11 | ```bash 12 | cd ~/environment && mkdir bastion-host-setup && cd bastion-host-setup 13 | aws ec2 create-key-pair --key-name jenkins-bastion-host-kp --query 'KeyMaterial' --output text > jenkins-bastion-host-kp.pem --region $CDK_DEFAULT_REGION 14 | ``` 15 | - Download the keypair to your client machine from Cloud9 and delete the jenkins-bastion-host-kp.pem. 16 | ```bash 17 | rm jenkins-bastion-host-kp.pem 18 | ``` 19 | 20 | 2. Get the Jenkins VPC ID and Public Subnet ID and VPC CIDR. 21 | ```bash 22 | JENKINS_VPC_ID=$(aws ec2 describe-vpcs --filters Name=tag:aws:cloudformation:stack-name,Values=jenkins-iac-dev-deploy-app --query "Vpcs[].VpcId" --region $CDK_DEFAULT_REGION | jq --raw-output '.[]') 23 | JENKINS_VPC_PUBLIC_SUBNET_ID=$(aws ec2 describe-subnets --filters Name=tag:aws-cdk:subnet-name,Values=public --region $CDK_DEFAULT_REGION --query "Subnets[].SubnetId" | jq --raw-output '.[1]') 24 | JENKINS_VPC_CIDR=$(aws ec2 describe-vpcs --filters Name=tag:aws:cloudformation:stack-name,Values=jenkins-iac-dev-deploy-app --region $CDK_DEFAULT_REGION --query "Vpcs[].CidrBlock" | jq --raw-output '.[]') 25 | ``` 26 | 3. Set the variables for keypair name and identify the IP that you want to access Jenkins from. If you are connected through VPN and/or Direct Connect you can provide the CIDR range else you can provide the host /32 CIDR that you want to connect from. 27 | ```bash 28 | BASTION_HOST_KP='jenkins-bastion-host-kp' 29 | BASTION_HOST_ALLOWED_IP='PROVIDE THE CIDR FOR ALLOWED IP' 30 | ``` 31 | 32 | 3. Deploy the Bastion Host to the public subnet. 33 | 34 | - Deploy the CloudFormation stack for the Windows Bastion Host. 35 | ```bash 36 | aws cloudformation create-stack --stack-name jenkins-bastion-host-stack \ 37 | --template-body file://$HOME/environment/aws-jenkins-ecs-cdk/assets/templates/cfn-windows-bastion-host.yaml \ 38 | --parameters ParameterKey=JenkinsVPCId,ParameterValue=${JENKINS_VPC_ID} ParameterKey=JenkinsPublicSubnetId,ParameterValue=${JENKINS_VPC_PUBLIC_SUBNET_ID} ParameterKey=JenkinsVPCCidr,ParameterValue=${JENKINS_VPC_CIDR} ParameterKey=KeyName,ParameterValue=${BASTION_HOST_KP} ParameterKey=AllowedIP,ParameterValue=${BASTION_HOST_ALLOWED_IP} --capabilities CAPABILITY_NAMED_IAM --region=$CDK_DEFAULT_REGION 39 | ``` 40 | 41 | 42 | - The above command will output a StackId. To see if the stack is completed successfully, run the below command. This will exit once stack is completed. Please note this will take aproximately 5 to 10 mins. 43 | ```bash 44 | aws cloudformation wait stack-create-complete --stack-name jenkins-bastion-host-stack --region=$CDK_DEFAULT_REGION 45 | ``` 46 | 47 | ## Connecting to Bastion Host. 48 | 49 | 1. Getting the password. 50 | - Navigate to AWS Console and click **connect**. 51 | 52 | ![RDP Instance Connect](assets/images/rdp-1-instance-connect.png) 53 | 54 | - Click on **RDP Client** tab. 55 | 56 | ![RDP Instance Connect KP](assets/images/rdp-2-instance-connect-kp.png) 57 | 58 | - Click on Get Password. 59 | 60 | ![RDP Instance Connect KP](assets/images/rdp-2-instance-connect-kp.png) 61 | 62 | - Upload **jenkins-bastion-host-kp.pem** file to decrypt the password. 63 | 64 | ![RDP Instance Connect Decrypt Password](assets/images/rdp-3-instance-connect-decrypt-pw.png) 65 | 66 | - Click on **Decrypt Password** 67 | 68 | ![RDP Instance Connect Get Password](assets/images/rdp-4-instance-connect-get-pw.png) 69 | 70 | - Note the decrypted password. 71 | 72 | 2. Connecting to the bastion host using RDP client. 73 | - Click on new connection. 74 | 75 | - Add a new PC and provide the Public IP of the Bastion Host. 76 | 77 | ![RDP Connect Login](assets/images/rdp-5-connect-login.png) 78 | 79 | - Enter Username (Administrator) and Password (from step 1 above) and hit connect. 80 | 81 | 82 | 3. Import Root CA into Windows Trusted Root Certification Authorities. 83 | 84 | - Once logged in to Windows, copy the rootCA.crt to your Bastion Host desktop. 85 | 86 | - Open up cert manager and click on Trusted Root Certification on the explorer. 87 | 88 | ![RDP Trusted Root Certification Manager](assets/images/rdp-8-import-private-root-ca.png) 89 | 90 | - Right click on All Tasks and Import Task and import the private-root-ca.cer from your desktop. 91 | 92 | ![RDP Trusted Root Certification Manager Import](assets/images/rdp-9-import-private-root-ca.png) 93 | 94 | 4. Validate the Private Root CA Import. 95 | - Open up EDGE browser. 96 | 97 | - Navigate to `https://jenkins-dev.internal.anycompany.com` and you should see no certificate warnings. 98 | 99 | ![Jenkins Login Screen](assets/images/rdp-7-connect-jenkins-ssl.png) -------------------------------------------------------------------------------- /SETUP-CLOUD9.md: -------------------------------------------------------------------------------- 1 | ### AWS account 2 | 3 | Ensure you have access to an AWS account, and a set of credentials with *Administrator* permissions. **Note:** In a production environment we would recommend locking permissions down to the bare minimum needed to operate the pipeline. 4 | 5 | ### Create an AWS Cloud9 environment 6 | 7 | Log into the AWS Management Console and search for Cloud9 services in the search bar. Click Cloud9 and create an AWS Cloud9 environment in the a region e.g. `us-east-2` based on Amazon Linux 2. You can provide a environment name of `workshop-environment` for name and select the instance type as **t2.micro** or **t3.micro**. 8 | ![Cloud9 Create](assets/images/1-c9-create.png) 9 | 10 | ### Configure the AWS Cloud9 environment 11 | 12 | Launch the AWS Cloud9 IDE. Close the `Welcome` tab and open a new `Terminal` tab. 13 | 14 | ![Cloud9 Launch](assets/images/2-c9-launch.png) 15 | 16 | #### Create and attach an IAM role for your Cloud9 instance 17 | 18 | Disable Cloud9 temporary credentials, and create and attach an IAM role for your Cloud9 instance so that you can deploy using AWS CDK for initial setup. 19 | 20 | 1. Follow [this deep link to find your Cloud9 EC2 instance](https://console.aws.amazon.com/ec2/v2/home?#Instances:tag:Name=aws-cloud9-;sort=desc:launchTime) 21 | 2. Download the CloudFormation template. We will use the template to create the IAM role for the Cloud9 instance. 22 | ```bash 23 | cd ~/environment && mkdir cloud9-setup && cd cloud9-setup 24 | curl -OL https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/main/assets/templates/cfn-cloud9-instance.yaml 25 | ``` 26 | 3. Deploy the CloudFormation template using the below steps and wait for completion: 27 | ```bash 28 | aws cloudformation deploy --template-file cfn-cloud9-instance.yaml --stack-name cloud9-instance-role-stack --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM 29 | ``` 30 | 4. Select the instance, then choose **Actions / Security / Modify IAM Role**. Note: If you cannot find this menu option, then look under **Actions / Instance Settings / Modify IAM Role** instead. 31 | ![cloud9-modify-role](assets/images/6-c9-modify-role.png) 32 | 8. Choose **cloud9-instance-profile** from the **IAM Role** drop down, and select **Save** 33 | ![cloud9-attach-role](assets/images/7-c9-attach-role.png) 34 | 9. Return to your Cloud9 workspace and click the gear icon (in top right corner), or click to open a new tab and choose "Open Preferences" 35 | 10. Select **AWS SETTINGS** 36 | 11. Turn off **AWS managed temporary credentials** 37 | 12. Close the Preferences tab 38 | ![cloud9-disable-temp-credentials](assets/images/8-c9-disable-temp-credentials.png) 39 | 13. In the Cloud9 terminal pane, execute the command: 40 | ```bash 41 | rm -vf ${HOME}/.aws/credentials 42 | ``` 43 | 14. Install JSON parser JQ. 44 | ```bash 45 | sudo yum install jq -y 46 | ``` 47 | 15. As a final check, use the [GetCallerIdentity](https://docs.aws.amazon.com/cli/latest/reference/sts/get-caller-identity.html) CLI command to validate that the Cloud9 IDE is using the correct IAM role. 48 | ```bash 49 | aws sts get-caller-identity --query Arn | grep cloud9-instance-role -q && echo "IAM role valid" || echo "IAM role NOT valid" 50 | ``` -------------------------------------------------------------------------------- /SETUP-PRIVATECA.md: -------------------------------------------------------------------------------- 1 | ## Security Disclaimer 2 | Instructions provided below are in the context of sample code for testing. You are responsible for testing, securing, and optimizing the sample code, as appropriate for production grade use based on your specific quality control practices and standards and security practices. 3 | 4 | ## Private CA Setup 5 | 6 | Ensure you have access completed the Cloud9 setup prior to working through the below steps. In order to apply SSL certificate to the Internal ALB that is accessible only in the VPC through a private Amazon Route 53 hosted zone we have two options, the first one is importing certificate from your PKI infrastructure to Amazon Certificate Manager or leverage AWS Private CA is used to issue SSL certificate. It is recommended to use AWS Private CA but please review the pricing details before you leverage this option. For the deployment we will be using a self signed certificate. Please review the necessary setup and configuration to use eiher of the options. Default mode for certificate is **self-signed** and this is configured in cdk.json. The two options are **self-signed** and **acm-pca**. 7 | 8 | ## Option 1: Self Signed Certificate using OpenSSL - Default mode for sample deployment. 9 | 10 | ### Self Signed Certificate setup steps 11 | 1. Create a new folder for self signed certificate files. 12 | ```bash 13 | cd ~/environment && mkdir openssl-setup && cd openssl-setup 14 | ``` 15 | 16 | 2. Create Certificate Authority (CA) 17 | ```bash 18 | openssl req -x509 \ 19 | -sha256 -days 365 \ 20 | -nodes \ 21 | -newkey rsa:2048 \ 22 | -subj "/CN=internal.anycompany.com/C=US/ST=WA/L=Seattle/O=AnyCompany/OU=IT-Applications" \ 23 | -keyout rootCA.key -out rootCA.crt 24 | ``` 25 | 26 | 3. Generate Private Key using openssl. 27 | ```bash 28 | openssl genrsa 2048 > jenkins-dev.private.key 29 | ``` 30 | 31 | 4. Create a Certificate Signing Request 32 | - Create a file called csr.conf with the configuration values. 33 | ```bash 34 | cat > csr.conf <> encoded_pem.txt 90 | ``` 91 | - Create a secret to store the root CA. 92 | ```bash 93 | aws secretsmanager create-secret \ 94 | --name /dev/jenkins/rootCA \ 95 | --description "Jenkins Private Root CA" \ 96 | --secret-string file://encoded_pem.txt \ 97 | --region=$CDK_DEFAULT_REGION 98 | ``` 99 | - Validate that the contents were stored correctly by running the below command to retrive the value and decode it. 100 | ```bash 101 | aws secretsmanager get-secret-value --secret-id /dev/jenkins/rootCA --region $CDK_DEFAULT_REGION --query 'SecretString' --output text | base64 -d 102 | ``` 103 | 104 | 8. Store the ACM Certificate ARN in parameter store that will be used for the Jenkins ALB. 105 | ```bash 106 | aws ssm put-parameter \ 107 | --name "/dev/jenkins/acm/selfSignedCertificateArn" \ 108 | --value "" \ 109 | --type String \ 110 | --region=$CDK_DEFAULT_REGION 111 | ``` 112 | 113 | 9. cdk.json default value for **ctxACMCertMode** is **self-signed**. 114 | 115 | ### AWS Certificate Manager 116 | - [Importing certificates into AWS Certificate Manager](https://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html) 117 | 118 | ## Option 2: AWS Private CA - Recommended for development and production. 119 | 120 | **Note** Please review the pricing for AWS Private CA. AWS Private CA offers 30-day free trial. If you do want to avoid the charges please make sure that you review the details on AWS Private CA Pricing [here](https://aws.amazon.com/private-ca/pricing/). 121 | 122 | ### AWS Private CA setup steps 123 | 124 | **Note:** Below instructions provide steps to complete the setup using AWS CLI. You can also use AWS Console if that is more comfortable for you. For more details on AWS Private CA setup and documentation please refer to the [reference section](#aws-private-ca-reference). 125 | 126 | 1. Create a new folder for private CA and create CA config. 127 | - Create a new directory. 128 | 129 | ```bash 130 | cd ~/environment && mkdir privateca-setup && cd privateca-setup 131 | ``` 132 | - Create ca_config.txt. Please update the values for subject as necessary. 133 | ```bash 134 | cat > ca_config.txt < ca.csr 170 | ``` 171 | - Using the CSR from the previous step as the argument for the --csr parameter, issue the root certificate. Provide the Certificate ARN from above step 2. 172 | ```bash 173 | aws acm-pca issue-certificate \ 174 | --certificate-authority-arn arn:aws:acm-pca:region:account:certificate-authority/CA_ID \ 175 | --csr file://ca.csr \ 176 | --signing-algorithm SHA256WITHRSA \ 177 | --template-arn arn:aws:acm-pca:::template/RootCACertificate/V1 \ 178 | --validity Value=730,Type=DAYS \ 179 | --region $CDK_DEFAULT_REGION 180 | ``` 181 | - Retrieve the root certificate. Provide the Certificate ARN from above step 2 and certificate arn from above issue certificate step. 182 | ```bash 183 | aws acm-pca get-certificate \ 184 | --certificate-authority-arn arn:aws:acm-pca:region:account:certificate-authority/CA_ID \ 185 | --certificate-arn arn:aws:acm-pca:region:account:certificate-authority/CA_ID/certificate/certificate_ID \ 186 | --region $CDK_DEFAULT_REGION \ 187 | --output text > cert.pem 188 | ``` 189 | - Import the root CA certificate to install it on the CA. Provide the Certificate ARN from above step 2. 190 | ```bash 191 | aws acm-pca import-certificate-authority-certificate \ 192 | --certificate-authority-arn arn:aws:acm-pca:region:account:certificate-authority/CA_ID \ 193 | --region $CDK_DEFAULT_REGION \ 194 | --certificate file://cert.pem 195 | ``` 196 | 197 | 4. Store the root CA certificate in secrets manager that can be used by CDK pipeline when building container image. 198 | - Let us encode the pem with base64 to preserve the format of the pem file. 199 | ```bash 200 | cd ~/environment/privateca-setup 201 | echo $(base64 -w 0 cert.pem) >> encoded_pem.txt 202 | ``` 203 | - Create a secret to store the root CA. 204 | ```bash 205 | aws secretsmanager create-secret \ 206 | --name /dev/jenkins/rootCA \ 207 | --description "Jenkins Private Root CA" \ 208 | --secret-string file://encoded_pem.txt \ 209 | --region=$CDK_DEFAULT_REGION 210 | ``` 211 | - Validate that the contents were stored correctly by running the below command to retrive the value and decode it. 212 | ```bash 213 | aws secretsmanager get-secret-value --secret-id /dev/jenkins/rootCA --region $CDK_DEFAULT_REGION --query 'SecretString' --output text | base64 -d 214 | ``` 215 | 216 | 5. Store the Private Certificate Authority ARN in parameter store that will be used to issue certificate for the Jenkins ALB through Amazon Certificate Manager. 217 | ```bash 218 | aws ssm put-parameter \ 219 | --name "/dev/jenkins/acmpca/certificateAuthorityArn" \ 220 | --value "" \ 221 | --type String \ 222 | --region=$CDK_DEFAULT_REGION 223 | ``` 224 | 6. Update cdk.json value for **ctxACMCertMode** as **acm-pca**. 225 | 226 | ### AWS Private CA Reference 227 | - [What is AWS Private CA?](https://docs.aws.amazon.com/privateca/latest/userguide/PcaWelcome.html) 228 | - [Creating a private CA](https://docs.aws.amazon.com/privateca/latest/userguide/create-CA.html) 229 | 230 | -------------------------------------------------------------------------------- /THIRD-PARTY-NOTICES.md: -------------------------------------------------------------------------------- 1 | Build and Deploy Jenkins as code to ECS Fargate using AWS CDK includes the following third-party software/licensing: 2 | 3 | ## Official Jenkins Docker image License 4 | Please review license terms here [LICENSE](https://github.com/jenkinsci/docker/blob/master/LICENSE.txt). 5 | -------------------------------------------------------------------------------- /assets/images/1-c9-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/1-c9-create.png -------------------------------------------------------------------------------- /assets/images/10-jenkins-deploy-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/10-jenkins-deploy-role.png -------------------------------------------------------------------------------- /assets/images/11-c9-cdk-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/11-c9-cdk-deploy.png -------------------------------------------------------------------------------- /assets/images/12-jenkins-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/12-jenkins-dashboard.png -------------------------------------------------------------------------------- /assets/images/13-jenkins-sample-jobs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/13-jenkins-sample-jobs.png -------------------------------------------------------------------------------- /assets/images/14-jenkins-sample-job-validation-build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/14-jenkins-sample-job-validation-build.png -------------------------------------------------------------------------------- /assets/images/2-c9-launch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/2-c9-launch.png -------------------------------------------------------------------------------- /assets/images/20-cdk-destroy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/20-cdk-destroy.png -------------------------------------------------------------------------------- /assets/images/3-launch-iam-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/3-launch-iam-role.png -------------------------------------------------------------------------------- /assets/images/4-iam-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/4-iam-permission.png -------------------------------------------------------------------------------- /assets/images/5-create-iam-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/5-create-iam-role.png -------------------------------------------------------------------------------- /assets/images/6-c9-modify-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/6-c9-modify-role.png -------------------------------------------------------------------------------- /assets/images/7-c9-attach-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/7-c9-attach-role.png -------------------------------------------------------------------------------- /assets/images/8-c9-disable-temp-credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/8-c9-disable-temp-credentials.png -------------------------------------------------------------------------------- /assets/images/9-c9-set-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/9-c9-set-account.png -------------------------------------------------------------------------------- /assets/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/architecture.png -------------------------------------------------------------------------------- /assets/images/rdp-1-instance-connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/rdp-1-instance-connect.png -------------------------------------------------------------------------------- /assets/images/rdp-2-instance-connect-kp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/rdp-2-instance-connect-kp.png -------------------------------------------------------------------------------- /assets/images/rdp-3-instance-connect-decrypt-pw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/rdp-3-instance-connect-decrypt-pw.png -------------------------------------------------------------------------------- /assets/images/rdp-4-instance-connect-get-pw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/rdp-4-instance-connect-get-pw.png -------------------------------------------------------------------------------- /assets/images/rdp-5-connect-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/rdp-5-connect-login.png -------------------------------------------------------------------------------- /assets/images/rdp-6-connect-prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/rdp-6-connect-prompt.png -------------------------------------------------------------------------------- /assets/images/rdp-7-connect-jenkins-ssl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/rdp-7-connect-jenkins-ssl.png -------------------------------------------------------------------------------- /assets/images/rdp-8-import-private-root-ca.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/rdp-8-import-private-root-ca.png -------------------------------------------------------------------------------- /assets/images/rdp-9-import-private-root-ca.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-jenkins-ecs-cdk/52ce0b27891457bf35928a99be4635e59d8d6096/assets/images/rdp-9-import-private-root-ca.png -------------------------------------------------------------------------------- /assets/templates/cfn-cloud9-instance.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 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 this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # 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 IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: 2010-09-09 17 | Description: The AWS CloudFormation template for creating instance role to be assumed by Cloud9 instance to deploy the stack. 18 | 19 | Resources: 20 | Cloud9InstanceRole: 21 | Type: AWS::IAM::Role 22 | Properties: 23 | RoleName: cloud9-instance-role 24 | AssumeRolePolicyDocument: 25 | Version: 2012-10-17 26 | Statement: 27 | - Effect: Allow 28 | Principal: 29 | Service: 30 | - "cloud9.amazonaws.com" 31 | - "ec2.amazonaws.com" 32 | Action: 33 | - sts:AssumeRole 34 | ManagedPolicyArns: 35 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 36 | - arn:aws:iam::aws:policy/AWSCloud9SSMInstanceProfile 37 | Metadata: 38 | cfn_nag: 39 | rules_to_suppress: 40 | - id: W28 41 | reason: "Resource role name explicitly named." 42 | 43 | Cloud9InstancePolicy: 44 | Type: AWS::IAM::Policy 45 | Properties: 46 | PolicyName: cloud9-instance-policy 47 | PolicyDocument: 48 | Version: "2012-10-17" 49 | Statement: 50 | - Effect: Allow 51 | Action: 52 | - "cloudformation:*" 53 | - "acm-pca:CreateCertificateAuthority" 54 | - "acm-pca:ListCertificateAuthorities" 55 | - "acm-pca:ListTags" 56 | - "acm-pca:TagCertificateAuthority" 57 | - "acm-pca:UntagCertificateAuthority" 58 | - "iam:ListRoles" 59 | - "ec2:DescribeVpcs" 60 | - 'ec2:DescribeSubnets' 61 | - 'ec2:DescribeImages' 62 | - 'ec2:DescribeImportImageTasks' 63 | - 'ec2:DescribeInstances' 64 | - 'ec2:DescribeSecurityGroupRules' 65 | - 'ec2:DescribeInstanceTypes' 66 | - 'ec2:DescribeSecurityGroups' 67 | - 'ec2:DescribeSecurityGroupReferences' 68 | - 'ec2:ReportInstanceStatus' 69 | - 'ec2:RunInstances' 70 | - 'ec2:CreateTags' 71 | - 'ec2:DescribeTags' 72 | - 'ec2:DeleteTags' 73 | - 'ec2:DescribeKeyPairs' 74 | - "codecommit:ListRepositories" 75 | - 'ec2:AuthorizeSecurityGroupIngress' 76 | - 'ec2:RevokeSecurityGroupEgress' 77 | - 'ec2:RevokeSecurityGroupIngress' 78 | - 'ec2:CreateSecurityGroup' 79 | - 'ec2:AuthorizeSecurityGroupEgress' 80 | - 'ec2:DeleteSecurityGroup' 81 | - 'ec2:StartInstances' 82 | - 'ec2:StopInstances' 83 | - 'ec2:TerminateInstances' 84 | Resource: "*" 85 | - Effect: Allow 86 | Action: 87 | - "ecr:DescribeRegistry" 88 | - "ecr:DeleteRegistryPolicy" 89 | - "ecr:PutRegistryPolicy" 90 | - "ecr:PutRegistryScanningConfiguration" 91 | - "ecr:GetRegistryPolicy" 92 | - "ecr:CreateRepository" 93 | - "ecr:GetAuthorizationToken" 94 | - "ecr:BatchImportUpstreamImage" 95 | - "ecr:GetRegistryScanningConfiguration" 96 | Resource: "*" 97 | - Effect: Allow 98 | Action: 99 | - "acm-pca:ImportCertificateAuthorityCertificate" 100 | - "acm-pca:GetCertificate" 101 | - "acm-pca:IssueCertificate" 102 | - "acm-pca:GetCertificateAuthorityCsr" 103 | - "acm-pca:DescribeCertificateAuthority" 104 | - "acm-pca:DeleteCertificateAuthority" 105 | Resource: !Sub "arn:aws:acm-pca:*:${AWS::AccountId}:certificate-authority/*" 106 | - Effect: Allow 107 | Action: 108 | - "ssm:PutParameter" 109 | - "ssm:GetParameter" 110 | - "ssm:DeleteParameter" 111 | Resource: !Sub "arn:aws:ssm:*:${AWS::AccountId}:parameter/*" 112 | - Effect: Allow 113 | Action: 114 | - "secretsmanager:GetSecretValue" 115 | - "secretsmanager:PutSecretValue" 116 | - "secretsmanager:CreateSecret" 117 | - "secretsmanager:DeleteSecret" 118 | Resource: !Sub "arn:aws:secretsmanager:*:${AWS::AccountId}:secret*" 119 | - Effect: Allow 120 | Action: 121 | - "sts:AssumeRole" 122 | Resource: 123 | - "arn:aws:iam::*:role/cdk-*" 124 | - Effect: Allow 125 | Action: 126 | - "codecommit:*" 127 | Resource: !Sub "arn:aws:codecommit:*:${AWS::AccountId}:*" 128 | - Effect: Allow 129 | Action: 130 | - "s3:*" 131 | Resource: 132 | - "arn:aws:s3:::cdktoolkit-stagingbucket-*" 133 | - "arn:aws:s3:::cdk-*" 134 | - Effect: Allow 135 | Action: 136 | - "ec2:CreateKeyPair" 137 | Resource: !Sub "arn:aws:ec2:*:${AWS::AccountId}:key-pair/*" 138 | - Effect: Allow 139 | Action: 140 | - "acm:ImportCertificate" 141 | - "acm:DeleteCertificate" 142 | Resource: !Sub "arn:aws:acm:*:${AWS::AccountId}:certificate/*" 143 | - Effect: Allow 144 | Action: 145 | - "iam:UntagRole" 146 | - "iam:TagRole" 147 | - "iam:PutRolePermissionsBoundary" 148 | - "iam:CreateRole" 149 | - "iam:AttachRolePolicy" 150 | - "iam:PutRolePolicy" 151 | - "iam:DetachRolePolicy" 152 | - "iam:PassRole" 153 | - "iam:DeleteRolePolicy" 154 | - "iam:CreatePolicyVersion" 155 | - "iam:GetRole" 156 | - "iam:PutUserPermissionsBoundary" 157 | - "iam:DeleteRole" 158 | - "iam:TagPolicy" 159 | - "iam:TagUser" 160 | - "iam:CreatePolicy" 161 | - "iam:UntagUser" 162 | - "iam:PutUserPolicy" 163 | - "iam:UntagPolicy" 164 | - "iam:UpdateRole" 165 | - "iam:UntagInstanceProfile" 166 | - "iam:GetRolePolicy" 167 | - "iam:TagInstanceProfile" 168 | - 'iam:CreateInstanceProfile' 169 | - 'iam:DeleteInstanceProfile' 170 | - 'iam:RemoveRoleFromInstanceProfile' 171 | - 'iam:AddRoleToInstanceProfile' 172 | Resource: 173 | - !Sub "arn:aws:iam::${AWS::AccountId}:role/*" 174 | - !Sub "arn:aws:iam::${AWS::AccountId}:user/*" 175 | - !Sub "arn:aws:iam::${AWS::AccountId}:policy/*" 176 | - !Sub "arn:aws:iam::${AWS::AccountId}:instance-profile/*" 177 | - Effect: Allow 178 | Action: 179 | - 'ecr:ListImages' 180 | - 'ecr:BatchDeleteImage' 181 | - "ecr:TagResource" 182 | - "ecr:GetLifecyclePolicy" 183 | - "ecr:BatchGetRepositoryScanningConfiguration" 184 | - "ecr:GetRepositoryPolicy" 185 | - "ecr:StartImageScan" 186 | - "ecr:UploadLayerPart" 187 | - "ecr:DescribeImageScanFindings" 188 | - "ecr:DeleteRepositoryPolicy" 189 | - "ecr:DeleteRepository" 190 | - "ecr:BatchGetImage" 191 | - "ecr:DescribeImageReplicationStatus" 192 | - "ecr:DescribeImages" 193 | - "ecr:ListTagsForResource" 194 | - "ecr:PutImageTagMutability" 195 | - "ecr:InitiateLayerUpload" 196 | - "ecr:StartLifecyclePolicyPreview" 197 | - "ecr:SetRepositoryPolicy" 198 | - "ecr:DescribeRepositories" 199 | - "ecr:UntagResource" 200 | - "ecr:GetLifecyclePolicyPreview" 201 | - "ecr:BatchCheckLayerAvailability" 202 | - "ecr:DeleteLifecyclePolicy" 203 | - "ecr:ReplicateImage" 204 | - "ecr:PutImageScanningConfiguration" 205 | - "ecr:PutLifecyclePolicy" 206 | - "ecr:PutImage" 207 | - "ecr:CompleteLayerUpload" 208 | - "ecr:GetDownloadUrlForLayer" 209 | Resource: !Sub "arn:aws:ecr:*:${AWS::AccountId}:repository/*" 210 | Roles: 211 | - !Ref Cloud9InstanceRole 212 | Metadata: 213 | cfn_nag: 214 | rules_to_suppress: 215 | - id: F4 216 | reason: "Actions cannot be restricted to resources." 217 | - id: W12 218 | reason: "Actions cannot be restricted to resources." 219 | 220 | Cloud9InstanceProfile: 221 | Type: AWS::IAM::InstanceProfile 222 | Properties: 223 | InstanceProfileName: cloud9-instance-profile 224 | Roles: 225 | - !Ref Cloud9InstanceRole -------------------------------------------------------------------------------- /assets/templates/cfn-jenkins-deployment-role.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 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 this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # 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 IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: 2010-09-09 17 | Description: The AWS CloudFormation template for creating jenkins role to be assumed by DevSecOps account to carry out deployment into target workload account(s). 18 | 19 | Parameters: 20 | DevOpsAccountID: 21 | Description : Account ID of the DevOps AWS Account where Jenkins is deployed. 22 | Type: String 23 | ConstraintDescription: Must be a valid AWS Account ID without hyphens. 24 | AllowedPattern: '\d{12}' 25 | MinLength: 12 26 | MaxLength: 12 27 | 28 | Resources: 29 | JenkinsDeploymentRole: 30 | Type: AWS::IAM::Role 31 | Properties: 32 | RoleName: jenkins-deployment-role 33 | AssumeRolePolicyDocument: 34 | Version: 2012-10-17 35 | Statement: 36 | - 37 | Effect: Allow 38 | Principal: 39 | AWS: 40 | - !Sub arn:aws:iam::${DevOpsAccountID}:root 41 | Action: 42 | - sts:AssumeRole 43 | ManagedPolicyArns: 44 | - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess 45 | Metadata: 46 | cfn_nag: 47 | rules_to_suppress: 48 | - id: W28 49 | reason: "Resource role name explicitly named to be referenced in the Jenkins Global Configuration." 50 | 51 | JenkinsDeploymentPolicy: 52 | Type: AWS::IAM::Policy 53 | Properties: 54 | PolicyName: jenkins-deployment-policy 55 | PolicyDocument: 56 | Version: "2012-10-17" 57 | Statement: 58 | - Effect: Allow 59 | Action: 60 | - 'cloudformation:ListStacks' 61 | - 'cloudformation:DescribeAccountLimits' 62 | - 'cloudformation:EstimateTemplateCost' 63 | - 'cloudformation:ValidateTemplate' 64 | - 'cloudformation:SignalResource' 65 | - 'cloudformation:DescribeChangeSetHooks' 66 | - 'cloudformation:CreateChangeSet' 67 | - 'cloudformation:DescribeChangeSet' 68 | - 'cloudformation:ExecuteChangeSet' 69 | - 'cloudformation:DeleteChangeSet' 70 | - 'cloudformation:ListChangeSets' 71 | - 'cloudformation:DeleteStackInstances' 72 | - 'cloudformation:DescribeStackResource' 73 | - 'cloudformation:DescribeStackEvents' 74 | - 'cloudformation:UpdateStack' 75 | - 'cloudformation:ListStackResources' 76 | - 'cloudformation:DescribeStackInstance' 77 | - 'cloudformation:DescribeStackResources' 78 | - 'cloudformation:DescribeStacks' 79 | - 'cloudformation:RollbackStack' 80 | - 'cloudformation:DescribeStackResourceDrifts' 81 | - 'cloudformation:CancelUpdateStack' 82 | - 'cloudformation:CreateStackInstances' 83 | - 'cloudformation:CreateStack' 84 | - 'cloudformation:DetectStackDrift' 85 | - 'cloudformation:ContinueUpdateRollback' 86 | - 'cloudformation:GetTemplate' 87 | - 'cloudformation:UntagResource' 88 | - 'cloudformation:GetTemplateSummary' 89 | - 'cloudformation:TagResource' 90 | - 'ec2:CreateTags' 91 | - 'ec2:DescribeTags' 92 | - 'ec2:DeleteTags' 93 | - 'ec2:DescribeInstances' 94 | - 'ec2:DescribeSecurityGroupRules' 95 | - 'ec2:DescribeInstanceTypes' 96 | - 'ec2:DescribeSubnets' 97 | - 'ec2:DescribeImportImageTasks' 98 | - 'ec2:DescribeSecurityGroups' 99 | - 'ec2:DescribeImages' 100 | - 'ec2:DescribeSecurityGroupReferences' 101 | - 'ec2:DescribeVpcs' 102 | - 'ec2:ReportInstanceStatus' 103 | - 'ec2:AuthorizeSecurityGroupIngress' 104 | - 'ec2:RevokeSecurityGroupEgress' 105 | - 'ec2:RevokeSecurityGroupIngress' 106 | - 'ec2:CreateSecurityGroup' 107 | - 'ec2:AuthorizeSecurityGroupEgress' 108 | - 'ec2:DeleteSecurityGroup' 109 | - 'ec2:StartInstances' 110 | - 'ec2:RunInstances' 111 | - 'ec2:StopInstances' 112 | - 'ec2:TerminateInstances' 113 | - 's3:PutAccountPublicAccessBlock' 114 | - 's3:PutBucketPolicy' 115 | - 's3:PutBucketAcl' 116 | - 's3:CreateBucket' 117 | - 's3:DeleteBucket' 118 | - 's3:PutEncryptionConfiguration' 119 | - 's3:GetEncryptionConfiguration' 120 | - 's3:PutBucketTagging' 121 | - 's3:GetBucketPolicy' 122 | - 's3:DeleteJobTagging' 123 | - 's3:PutBucketPublicAccessBlock' 124 | - 's3:PutAccessPointPublicAccessBlock' 125 | - 's3:PutObjectAcl' 126 | Resource: '*' 127 | - Effect: Allow 128 | Action: 129 | - 'iam:CreateInstanceProfile' 130 | - 'iam:DeleteInstanceProfile' 131 | - 'iam:RemoveRoleFromInstanceProfile' 132 | - 'iam:AddRoleToInstanceProfile' 133 | Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:instance-profile/*' 134 | - Effect: Allow 135 | Action: 136 | - 'iam:PassRole' 137 | - 'iam:CreateRole' 138 | - 'iam:DeleteRole' 139 | - 'iam:AttachRolePolicy' 140 | - 'iam:DetachRolePolicy' 141 | - 'iam:UpdateRole' 142 | Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/*' 143 | Roles: 144 | - !Ref JenkinsDeploymentRole 145 | Metadata: 146 | cfn_nag: 147 | rules_to_suppress: 148 | - id: W12 149 | reason: "Actions cannot be restricted to resources and some are allowed to limit the verbose level to each action." 150 | 151 | Outputs: 152 | OutJenkinsDeploymentRoleArn: 153 | Value: !GetAtt JenkinsDeploymentRole.Arn 154 | Export: 155 | Name: JenkinsDeploymentRole-Arn 156 | -------------------------------------------------------------------------------- /assets/templates/cfn-windows-bastion-host.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 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 this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # 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 IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: 2010-09-09 17 | Description: CloudFormation template to create BastionHost instance in the Public Subnet to access the Jenkins Instance. 18 | 19 | Parameters: 20 | LatestAmiId: 21 | Type: "AWS::SSM::Parameter::Value" 22 | Default: /aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base 23 | AllowedValues: 24 | - /aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base 25 | InstanceType: 26 | Description: Please select an EC2 Instance Type 27 | Type: String 28 | Default: t3.small 29 | AllowedValues: 30 | - t3.micro 31 | - t3.small 32 | - t3.medium 33 | JenkinsVPCId: 34 | Description: Please select the VPC for the Jenkins Deployment 35 | Type: AWS::EC2::VPC::Id 36 | JenkinsPublicSubnetId: 37 | Description: Please select a Public Subnet Id from the Jenkins VPC 38 | Type: AWS::EC2::Subnet::Id 39 | JenkinsVPCCidr: 40 | Description: Please specify the CIDR for the Jenkins VPC 41 | Type: String 42 | AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})" 43 | ConstraintDescription: Must be a valid IP CIDR range of the form x.x.x.x/x 44 | KeyName: 45 | Type: AWS::EC2::KeyPair::KeyName 46 | Description: Name of an existing EC2 KeyPair to RDP access for Jenkins Administration. 47 | AllowedIP: 48 | Description: Please provide the IP for access to the Bastion Host 49 | Type: String 50 | 51 | Resources: 52 | BastionHostSecurityGroup: 53 | Type: 'AWS::EC2::SecurityGroup' 54 | Properties: 55 | GroupDescription: Enable HTTP access via port 3389 56 | VpcId: !Ref JenkinsVPCId 57 | SecurityGroupEgress: 58 | - CidrIp: !Ref AllowedIP 59 | Description: Allow all outbound traffic on port 3389 from Instance 60 | FromPort: 3389 61 | IpProtocol: tcp 62 | ToPort: 3389 63 | - CidrIp: !Ref JenkinsVPCCidr 64 | Description: Allow all outbound traffic on port 443 from Instance to VPC 65 | FromPort: 443 66 | IpProtocol: tcp 67 | ToPort: 443 68 | 69 | SecurityGroupIngressPortRDP: 70 | Type: AWS::EC2::SecurityGroupIngress 71 | Properties: 72 | Description: Allow all inbound traffic on port 3389 73 | IpProtocol: tcp 74 | FromPort: 3389 75 | ToPort: 3389 76 | GroupId: !Ref BastionHostSecurityGroup 77 | CidrIp: !Ref AllowedIP 78 | 79 | BastionHostInstanceRole: 80 | Type: 'AWS::IAM::Role' 81 | Properties: 82 | AssumeRolePolicyDocument: 83 | Version: '2012-10-17' 84 | Statement: 85 | - Effect: Allow 86 | Principal: 87 | Service: 88 | - ec2.amazonaws.com 89 | Action: 90 | - 'sts:AssumeRole' 91 | Path: / 92 | ManagedPolicyArns: 93 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 94 | 95 | BastionHostInstanceProfile: 96 | Type: "AWS::IAM::InstanceProfile" 97 | Properties: 98 | Path: "/" 99 | Roles: 100 | - !Ref BastionHostInstanceRole 101 | 102 | BastionHost: 103 | Type: AWS::EC2::Instance 104 | Properties: 105 | ImageId: !Ref LatestAmiId 106 | InstanceType: !Ref InstanceType 107 | IamInstanceProfile: !Ref BastionHostInstanceProfile 108 | KeyName: !Ref "KeyName" 109 | NetworkInterfaces: 110 | - AssociatePublicIpAddress: true 111 | DeviceIndex: "0" 112 | GroupSet: 113 | - !Ref BastionHostSecurityGroup 114 | SubnetId: !Ref JenkinsPublicSubnetId 115 | Tags: 116 | - Key: Name 117 | Value: !Ref AWS::StackName 118 | 119 | Outputs: 120 | BastionHostIP: 121 | Description: The IP of the Web Server Instance 122 | Value: !GetAtt BastionHost.PublicIp -------------------------------------------------------------------------------- /cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | cdk.context.json 10 | 11 | # System files 12 | **/.DS_Store -------------------------------------------------------------------------------- /cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /cdk/README.md: -------------------------------------------------------------------------------- 1 | # CDK Project for Building and Deploying Jenkins on ECS Fargate 2 | 3 | The `cdk.json` file tells the CDK Toolkit how to execute your app. Default context variables are configured in the file but there are two context variables that you would need to provide that include ctxHostedZoneId and ctxHostedZoneName. Prerequisite for running this project includes an active domain in Route 53. 4 | 5 | ## Prerequisites 6 | Setup typescript and Install CDK: 7 | ```bash 8 | npm install -g typescript 9 | npm install -g aws-cdk 10 | ``` 11 | 12 | ## Project Structure 13 | 14 | |Class Type | Files | Tests 15 | |--------------------------|---|---| 16 | |Main Class | bin/cdk.ts | | 17 | |Pipeline Main Class | lib/cdk-pipeline-stack.ts | test/cdk-pipeline-stack.test.ts | 18 | |Pipeline Stages | lib/cdk-pipeline-ecr-stage.ts, lib/cdk-pipeline-infra-stage.ts | | 19 | |Stack Classes | lib/cdk-ecr-stack.ts, lib/cdk-infra-stack.ts | test/cdk-ecr-stack.test.ts, test/cdk-infra-stack.test.ts | 20 | |Util Class | lib/cdk-util.ts | test/cdk-util.test.ts | 21 | |App Class | lib/cdk-app.ts | test/cdk-app.test.ts | 22 | 23 | ## CDK Context Default Variables (cdk.json) 24 | |Usage | Context Variable Name | Default Value | Override Required | 25 | |--------------------------|---|---|---| 26 | |Jenkins Hosted Zone Id | ctxHostedZoneId | **UPDATEME** | **Y** | 27 | |Jenkins Hosted Zone Name | ctxHostedZoneName | **UPDATEME** | **Y** | 28 | |Jenkins Default JNLP Port | ctxJnlpPort | 50000 | N | 29 | |Jenkins Domain Name | ctxJenkinsDomainNamePrefix | jenkins-dev | N | 30 | |Jenkins Controller Name| ctxJenkinsControllerName | jenkins-controller | N | 31 | |Jenkins Controller Image Tag Parameter Name | ctxJenkinsControllerImageTagParameterName | /dev/jenkins/controller/docker/image/tag | N | 32 | |Jenkins Agent Name | ctxJenkinsAgentName | jenkins-agent | N | 33 | |Jenkins Controller Image Tag Parameter Name | ctxJenkinsAgentImageTagParameterName | /dev/jenkins/agent/docker/image/tag | N | 34 | |Jenkins Admin User Credentials | ctxJenkinsAdminCredentialSecretName | /dev/jenkins/admin/credentials | N | 35 | |Jenkins Workload Account Parameter Name | ctxDevTeam1Workload1AWSAccountIdParameterName | /dev/team1/workload1/AWSAccountID | N | 36 | 37 | ## Useful commands 38 | 39 | * `npm run build` compile typescript to js 40 | * `npm run watch` watch for changes and compile 41 | * `npm run test` perform the jest unit tests 42 | * `cdk deploy` deploy this stack to your default AWS account/region 43 | * `cdk diff` compare deployed stack with current state 44 | * `cdk synth` emits the synthesized CloudFormation template -------------------------------------------------------------------------------- /cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "source-map-support/register"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import { CdkPipelineStack } from "../lib/cdk-pipeline-stack"; 5 | import { AwsSolutionsChecks } from 'cdk-nag'; 6 | import { NagSuppressions } from 'cdk-nag'; 7 | 8 | const app = new cdk.App(); 9 | const jenkinsStack = new CdkPipelineStack(app, "jenkins-on-ecs-stack", { 10 | stackName: "jenkins-iac-dev", 11 | description: "Jenkins Application Controller and Agent on ECS Fargate, deployed as Configuration as Code", 12 | env: { 13 | account: process.env.CDK_DEFAULT_ACCOUNT, 14 | region: process.env.CDK_DEFAULT_REGION, 15 | } 16 | }); 17 | 18 | cdk.Aspects.of(app).add(new AwsSolutionsChecks()); 19 | NagSuppressions.addStackSuppressions(jenkinsStack, [ 20 | { id: 'AwsSolutions-S1', reason: 'CDK construct does not provide a way to enable logging for S3 Bucket managed by Code Pipeline: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.pipelines.CodePipeline.html' }, 21 | { id: 'AwsSolutions-IAM5', reason: '1/Default policies for code pipeline and these are resourced to s3 bucket, account and CDK limits to customize the default policies: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.pipelines.CodePipeline.html, 2/ecr:GetAuthorizationToken does not allow to scope to resource' }, 22 | { id: 'AwsSolutions-CB3', reason: 'Building Docker Images for Jenkins Controller and Agent requires docker daemon on the code build instance' }, 23 | { id: 'AwsSolutions-CB4', reason: 'CodeBuildStep does not provide a way to specify KMS Key for encryption: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.pipelines.CodeBuildStepProps.html' }, 24 | ]); -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 25 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/core:checkSecretUsage": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 31 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 32 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 33 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 34 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 35 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 36 | "@aws-cdk/core:enablePartitionLiterals": true, 37 | "@aws-cdk/core:target-partitions": [ 38 | "aws", 39 | "aws-cn" 40 | ], 41 | "ctxJnlpPort": 50000, 42 | "ctxHostedZoneName": "internal.anycompany.com", 43 | "ctxJenkinsDomainNamePrefix": "jenkins-dev", 44 | "ctxJenkinsControllerName": "jenkins-controller", 45 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 46 | "ctxJenkinsAgentName": "jenkins-agent", 47 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 48 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 49 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 50 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 51 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 52 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 53 | "ctxACMCertMode": "self-signed" 54 | } 55 | } -------------------------------------------------------------------------------- /cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /cdk/lib/cdk-app.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from "aws-cdk-lib"; 2 | import { BuildEnvironmentVariableType } from "aws-cdk-lib/aws-codebuild"; 3 | 4 | export class CdkApp { 5 | private stack: Stack; 6 | 7 | constructor(stack: Stack) { 8 | this.stack = stack; 9 | } 10 | /************************************************************************/ 11 | /* generate buildspec.yaml 12 | /************************************************************************/ 13 | public getBuildSpec(appName: string, ssmImageTagParameter: string, secretPrivateRootCAId: string) { 14 | return { 15 | version: "0.2", 16 | env: { 17 | variables: { 18 | IMAGE_TAG: "latest", 19 | } 20 | }, 21 | phases: { 22 | install: { 23 | commands: [ 24 | "echo running install commands...", 25 | "COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8)", 26 | "IMAGE_TAG=${COMMIT_HASH:=latest}", 27 | `CURRENT_IMAGE_DIGEST=$(aws ssm get-parameter --name ${ssmImageTagParameter} --query "Parameter.Value" --output text --region $AWS_DEFAULT_REGION)`, 28 | `PRIVATE_ROOT_CA=$(aws secretsmanager get-secret-value --secret-id ${secretPrivateRootCAId} --query "SecretString" --output text --region $AWS_DEFAULT_REGION)` 29 | ], 30 | }, 31 | pre_build: { 32 | commands: [ 33 | "echo Logging in to Amazon ECR...", 34 | "echo $DOCKER_USER_PASSWORD | docker login -u $DOCKER_USER_NAME --password-stdin", 35 | "aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com", 36 | ], 37 | }, 38 | build: { 39 | commands: [ 40 | "echo Build started on `date`", 41 | `cd ${appName}`, 42 | "docker pull $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$CURRENT_IMAGE_DIGEST || true", 43 | "docker build --build-arg PRIVATE_ROOT_CA=$PRIVATE_ROOT_CA --cache-from $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$CURRENT_IMAGE_DIGEST -t $IMAGE_REPO_NAME:$IMAGE_TAG .", 44 | "docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG" 45 | ], 46 | }, 47 | post_build: { 48 | commands: [ 49 | "echo Running post build steps...", 50 | "docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG", 51 | "NEW_IMAGE_DIGEST=$(echo $(docker inspect $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG) | jq '.[].RepoDigests' | jq '.[]' | cut -d ':' -f 2 | cut -c 1-12)", 52 | "if [ $NEW_IMAGE_DIGEST = $CURRENT_IMAGE_DIGEST ] ; then echo No docker image changes; fi;", 53 | `if [ $NEW_IMAGE_DIGEST != $CURRENT_IMAGE_DIGEST ] ; then docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$NEW_IMAGE_DIGEST; docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$NEW_IMAGE_DIGEST; aws ssm put-parameter --name ${ssmImageTagParameter} --type String --value $NEW_IMAGE_DIGEST --overwrite; fi;` 54 | ], 55 | }, 56 | } 57 | }; 58 | } 59 | 60 | /************************************************************************/ 61 | /* prepare build environment variables 62 | /************************************************************************/ 63 | public getBuildEnvironment(repoName: string) { 64 | return { 65 | AWS_ACCOUNT_ID: { 66 | value: this.stack.account, 67 | }, 68 | AWS_DEFAULT_REGION: { 69 | value: this.stack.region, 70 | }, 71 | IMAGE_REPO_NAME: { 72 | value: repoName, 73 | }, 74 | IMAGE_TAG: { 75 | value: "latest", 76 | }, 77 | DOCKER_USER_NAME: { 78 | type: BuildEnvironmentVariableType.SECRETS_MANAGER, 79 | value: "dockerhub_credentials:username", 80 | }, 81 | DOCKER_USER_PASSWORD: { 82 | type: BuildEnvironmentVariableType.SECRETS_MANAGER, 83 | value: "dockerhub_credentials:password", 84 | }, 85 | }; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /cdk/lib/cdk-ecr-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { TagStatus } from "aws-cdk-lib/aws-ecr"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import * as ecr from "aws-cdk-lib/aws-ecr"; 5 | 6 | export class CdkECRStack extends cdk.Stack { 7 | public readonly controllerRepository: ecr.Repository; 8 | public readonly agentRepository: ecr.Repository; 9 | 10 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 11 | super(scope, id, props); 12 | 13 | /************************************************************************/ 14 | /* Create ECR Repo for Jenkins Controller 15 | /************************************************************************/ 16 | this.controllerRepository = new ecr.Repository( 17 | this, 18 | `${this.stackName}-controller`, 19 | { 20 | repositoryName: "jenkins-controller", 21 | imageScanOnPush: true, 22 | removalPolicy: cdk.RemovalPolicy.DESTROY, 23 | } 24 | ); 25 | 26 | /************************************************************************/ 27 | /* Create ECR Repo for Jenkins Agent 28 | /************************************************************************/ 29 | this.agentRepository = new ecr.Repository( 30 | this, 31 | `${this.stackName}-agent`, 32 | { 33 | repositoryName: "jenkins-agent", 34 | imageScanOnPush: true, 35 | removalPolicy: cdk.RemovalPolicy.DESTROY, 36 | } 37 | ); 38 | 39 | /************************************************************************/ 40 | /* Add Lifecycle Rules for Controller and Agent repo 41 | /************************************************************************/ 42 | this.controllerRepository.addLifecycleRule({ 43 | tagStatus: TagStatus.ANY, 44 | maxImageCount: 10, 45 | }); 46 | this.controllerRepository.addLifecycleRule({ 47 | tagStatus: TagStatus.UNTAGGED, 48 | maxImageAge: cdk.Duration.days(1), 49 | }); 50 | 51 | this.agentRepository.addLifecycleRule({ 52 | tagStatus: TagStatus.ANY, 53 | maxImageCount: 10, 54 | }); 55 | this.agentRepository.addLifecycleRule({ 56 | tagStatus: TagStatus.UNTAGGED, 57 | maxImageAge: cdk.Duration.days(1), 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cdk/lib/cdk-infra-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { CdkUtil} from "./cdk-util"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import * as iam from "aws-cdk-lib/aws-iam"; 5 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 6 | import * as ecr from "aws-cdk-lib/aws-ecr"; 7 | import * as ecs from "aws-cdk-lib/aws-ecs"; 8 | import * as efs from "aws-cdk-lib/aws-efs"; 9 | import * as logs from "aws-cdk-lib/aws-logs"; 10 | import * as acm from "aws-cdk-lib/aws-certificatemanager"; 11 | import * as acmpca from 'aws-cdk-lib/aws-acmpca'; 12 | import * as route53 from "aws-cdk-lib/aws-route53"; 13 | import * as targets from "aws-cdk-lib/aws-route53-targets"; 14 | import * as ecsPatterns from "aws-cdk-lib/aws-ecs-patterns"; 15 | import * as elb from "aws-cdk-lib/aws-elasticloadbalancingv2"; 16 | import * as ssm from 'aws-cdk-lib/aws-ssm'; 17 | import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; 18 | import * as servicediscovery from "aws-cdk-lib/aws-servicediscovery"; 19 | import { SslPolicy } from "aws-cdk-lib/aws-elasticloadbalancingv2"; 20 | 21 | export enum CERT_MODE { 22 | SELFSIGNED = "self-signed", 23 | ACMPCA = "acm-pca" 24 | } 25 | 26 | export class CdkInfraStack extends cdk.Stack { 27 | private jnlpPort: number; 28 | private hostedZoneName: string; 29 | private jenkinsDomainNamePrefix: string; 30 | private jenkinsBaseUrl: string; 31 | private jenkinsControllerName: string; 32 | private jenkinsControllerImageTagParameterName: string; 33 | private jenkinsAgentName: string; 34 | private jenkinsAgentImageTagParameterName: string; 35 | private jenkinsAdminCredentialSecretName: string; 36 | private jenkinsNakedDomainName: string; 37 | private devTeam1Workload1AWSAccountIdParameterName: string; 38 | private acmPCACertificateArnParameterName: string; 39 | private acmSelfSignedCertificateArnParameterName: string; 40 | private acmCertMode: string; 41 | private certificate: cdk.aws_certificatemanager.ICertificate; 42 | 43 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 44 | super(scope, id, props); 45 | 46 | /************************************************************************/ 47 | /* Get CDK Context Values 48 | /************************************************************************/ 49 | const util = new CdkUtil(this); 50 | this.jnlpPort = util.getCdkContextValue("ctxJnlpPort"); 51 | this.hostedZoneName = util.getCdkContextValue("ctxHostedZoneName"); 52 | this.jenkinsDomainNamePrefix = util.getCdkContextValue("ctxJenkinsDomainNamePrefix"); 53 | this.jenkinsControllerName = util.getCdkContextValue("ctxJenkinsControllerName"); 54 | this.jenkinsControllerImageTagParameterName = util.getCdkContextValue("ctxJenkinsControllerImageTagParameterName"); 55 | this.jenkinsAgentName = util.getCdkContextValue("ctxJenkinsAgentName"); 56 | this.jenkinsAgentImageTagParameterName = util.getCdkContextValue("ctxJenkinsAgentImageTagParameterName"); 57 | this.jenkinsAdminCredentialSecretName = util.getCdkContextValue("ctxJenkinsAdminCredentialSecretName"); 58 | this.devTeam1Workload1AWSAccountIdParameterName = util.getCdkContextValue("ctxDevTeam1Workload1AWSAccountIdParameterName"); 59 | this.acmPCACertificateArnParameterName = util.getCdkContextValue("ctxACMPCACertificateArnParameterName") 60 | this.acmSelfSignedCertificateArnParameterName = util.getCdkContextValue("ctxACMSelfSignedCertificateArnParameterName") 61 | this.jenkinsBaseUrl = "https://" + this.jenkinsDomainNamePrefix + "." + this.hostedZoneName; 62 | this.jenkinsNakedDomainName = this.jenkinsDomainNamePrefix + "." + this.hostedZoneName; 63 | this.acmCertMode = util.getCdkContextValue("ctxACMCertMode"); 64 | 65 | /************************************************************************/ 66 | /* Lookup ECR Repo, Secrets and Parameter defaults 67 | /************************************************************************/ 68 | const jenkinsControllerRepo = ecr.Repository.fromRepositoryName(this, 'jenkins-controller-repo', this.jenkinsControllerName) 69 | const jenkinsAgentLeaderRepo = ecr.Repository.fromRepositoryName(this, 'jenkins-agent-repo', this.jenkinsAgentName) 70 | 71 | const jenkinsCredentials = secretsmanager.Secret.fromSecretNameV2( 72 | this, 73 | `${this.stackName}-admin-user-credentials`, 74 | this.jenkinsAdminCredentialSecretName, 75 | ); 76 | 77 | const jenkinsControllerTag = ssm.StringParameter.valueForStringParameter( 78 | this, 79 | this.jenkinsControllerImageTagParameterName, 80 | ); 81 | 82 | const jenkinsAgentTag = ssm.StringParameter.valueForStringParameter( 83 | this, 84 | this.jenkinsAgentImageTagParameterName, 85 | ); 86 | 87 | const devTeam1Workload1TargetAccount = ssm.StringParameter.valueForStringParameter( 88 | this, 89 | this.devTeam1Workload1AWSAccountIdParameterName, 90 | ); 91 | 92 | 93 | 94 | /************************************************************************/ 95 | /* VPC, Subnets, Networking 96 | /************************************************************************/ 97 | const vpc = new ec2.Vpc(this, `${this.stackName}-vpc`, { 98 | vpcName: this.stackName, 99 | ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/20'), 100 | maxAzs: 2, 101 | subnetConfiguration: [ 102 | { 103 | cidrMask: 24, 104 | name: "public", 105 | subnetType: ec2.SubnetType.PUBLIC, 106 | mapPublicIpOnLaunch: false 107 | }, 108 | { 109 | cidrMask: 24, 110 | name: "private-alb", 111 | subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, 112 | }, 113 | { 114 | cidrMask: 24, 115 | name: "private-app", 116 | subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, 117 | }, 118 | ], 119 | }); 120 | 121 | /************************************************************************/ 122 | /* Create private hosted zone and issue a private certificate for jenkins 123 | /************************************************************************/ 124 | const hostedZone = new route53.PrivateHostedZone(this, `${this.stackName}-hosted-zone`, { 125 | zoneName: this.hostedZoneName, 126 | vpc: vpc 127 | }); 128 | 129 | /************************************************************************/ 130 | /* Lookup self signed cert or issue a certificate using ACM PCA 131 | /************************************************************************/ 132 | if (this.acmCertMode == CERT_MODE.SELFSIGNED){ 133 | const acmSelfSignedCertificateArn = ssm.StringParameter.valueForStringParameter( 134 | this, 135 | this.acmSelfSignedCertificateArnParameterName, 136 | ); 137 | this.certificate = acm.Certificate.fromCertificateArn(this, `${this.stackName}-acm-certificate`, acmSelfSignedCertificateArn); 138 | } else if (this.acmCertMode == CERT_MODE.ACMPCA) { 139 | const acmPCACertificateArn = ssm.StringParameter.valueForStringParameter( 140 | this, 141 | this.acmPCACertificateArnParameterName, 142 | ); 143 | this.certificate = new acm.PrivateCertificate(this, `${this.stackName}-acm-pca-certificate`, { 144 | domainName: this.jenkinsNakedDomainName, 145 | certificateAuthority: acmpca.CertificateAuthority.fromCertificateAuthorityArn(this, 'acm-pca-certificate', acmPCACertificateArn) 146 | }); 147 | } 148 | 149 | /***************************************************************************/ 150 | /* Jenkins ECS cluster 151 | /***************************************************************************/ 152 | const ecsCluster = new ecs.Cluster( 153 | this, 154 | `${this.stackName}-ecs-cluster`, 155 | { 156 | vpc: vpc, 157 | clusterName: `${this.stackName}-ecs-cluster`, 158 | defaultCloudMapNamespace: { 159 | name: `${this.stackName}-private`, 160 | type: servicediscovery.NamespaceType.DNS_PRIVATE, 161 | vpc: vpc 162 | }, 163 | containerInsightsV2: ecs.ContainerInsights.ENABLED 164 | } 165 | ); 166 | 167 | /***************************************************************************/ 168 | /* Jenkins ECS Cluster Security Group 169 | /***************************************************************************/ 170 | const ecsClusterSG = new ec2.SecurityGroup( 171 | this, 172 | `${this.stackName}-controller-sg`, 173 | { 174 | securityGroupName: `${this.stackName}-controller-sg`, 175 | vpc: vpc, 176 | allowAllOutbound: true, 177 | description: "Jenkins Controller Service Security Group", 178 | } 179 | ); 180 | 181 | const efsSG = new ec2.SecurityGroup( 182 | this, 183 | `${this.stackName}-efs-sg`, 184 | { 185 | securityGroupName: `${this.stackName}-efs-sg`, 186 | vpc: vpc, 187 | allowAllOutbound: false, 188 | description: "Jenkins EFS Security Group", 189 | } 190 | ); 191 | 192 | efsSG.connections.allowFrom( 193 | new ec2.Connections({ 194 | securityGroups: [ecsClusterSG], 195 | }), 196 | ec2.Port.tcp(2049), 197 | 'allow traffic on port 2049 from Jenkins Controller to EFS', 198 | ); 199 | 200 | efsSG.connections.allowTo( 201 | new ec2.Connections({ 202 | securityGroups: [ecsClusterSG], 203 | }), 204 | ec2.Port.tcp(2049), 205 | 'allow traffic to port 2049 from EFS to Jenkins Controller', 206 | ); 207 | 208 | /***************************************************************************/ 209 | /* Jenkins EFS File System and Access Point 210 | /***************************************************************************/ 211 | const fileSystem = new efs.FileSystem(this, `${this.stackName}-efs`, { 212 | fileSystemName: `${this.stackName}-efs`, 213 | vpc: vpc, 214 | vpcSubnets: { 215 | subnetGroupName: "private-app" 216 | }, 217 | encrypted: true, 218 | securityGroup: efsSG, 219 | performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, 220 | removalPolicy: cdk.RemovalPolicy.DESTROY, 221 | enableAutomaticBackups: true 222 | }); 223 | 224 | const accessPoint = new efs.AccessPoint(this, `${this.stackName}-efs-ap`, { 225 | fileSystem: fileSystem, 226 | posixUser: { 227 | uid: "1000", 228 | gid: "1000" 229 | }, 230 | createAcl: { 231 | ownerUid: "1000", 232 | ownerGid: "1000", 233 | permissions: "755" 234 | }, 235 | path: '/jenkins' 236 | }); 237 | 238 | /***************************************************************************/ 239 | /* Jenkins Controller and Agent execution and task role 240 | /***************************************************************************/ 241 | const jenkinsAgentExecutionRole = new iam.Role( 242 | this, 243 | `${this.stackName}-agent-execution-role`, 244 | { 245 | roleName: `${this.stackName}-agent-execution-role`, 246 | assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"), 247 | managedPolicies: [ 248 | iam.ManagedPolicy.fromAwsManagedPolicyName( 249 | "service-role/AmazonECSTaskExecutionRolePolicy" 250 | ) 251 | ] 252 | } 253 | ); 254 | 255 | const jenkinsAgentTaskRole = new iam.Role( 256 | this, 257 | `${this.stackName}-agent-task-role`, 258 | { 259 | roleName: `${this.stackName}-agent-task-role`, 260 | assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"), 261 | inlinePolicies: { 262 | "create-loggroup": new iam.PolicyDocument({ 263 | statements: [ 264 | new iam.PolicyStatement({ 265 | effect: iam.Effect.ALLOW, 266 | actions: [ 267 | "logs:CreateLogGroup", 268 | "logs:PutLogEvents", 269 | "logs:TagResource" 270 | ], 271 | resources: [ 272 | `arn:aws:logs:${this.region}:${this.account}:log-group/*` 273 | ], 274 | }), 275 | ] 276 | }) 277 | } 278 | } 279 | ); 280 | 281 | const jenkinsControllerExecutionRole = new iam.Role( 282 | this, 283 | `${this.stackName}-controller-execution-role`, 284 | { 285 | roleName: `${this.stackName}-controller-execution-role`, 286 | assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"), 287 | managedPolicies: [ 288 | iam.ManagedPolicy.fromAwsManagedPolicyName( 289 | "service-role/AmazonECSTaskExecutionRolePolicy" 290 | ) 291 | ] 292 | } 293 | ); 294 | 295 | const jenkinsControllerTaskRole = new iam.Role( 296 | this, 297 | `${this.stackName}-controller-task-role`, 298 | { 299 | roleName: `${this.stackName}-controller-task-role`, 300 | assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"), 301 | managedPolicies: [ 302 | iam.ManagedPolicy.fromAwsManagedPolicyName( 303 | "service-role/AmazonECSTaskExecutionRolePolicy" 304 | ) 305 | ], 306 | inlinePolicies: { 307 | "secrets-role": new iam.PolicyDocument({ 308 | statements: [ 309 | new iam.PolicyStatement({ 310 | effect: iam.Effect.ALLOW, 311 | actions: [ 312 | "secretsmanager:GetSecretValue" 313 | ], 314 | resources: [ 315 | `arn:${this.partition}:secretsmanager:${this.region}:${this.account}:secret:jenkins_secret` 316 | ], 317 | }), 318 | ] 319 | }), 320 | "ecr-role": new iam.PolicyDocument({ 321 | statements: [ 322 | new iam.PolicyStatement({ 323 | effect: iam.Effect.ALLOW, 324 | actions: ["ecr:GetAuthorizationToken"], 325 | resources: ["*"], 326 | }), 327 | new iam.PolicyStatement({ 328 | effect: iam.Effect.ALLOW, 329 | actions: [ 330 | "ecr:BatchCheckLayerAvailability", 331 | "ecr:GetDownloadUrlForLayer", 332 | "ecr:GetRepositoryPolicy", 333 | "ecr:DescribeRepositories", 334 | "ecr:ListImages", 335 | "ecr:DescribeImages", 336 | "ecr:BatchGetImage", 337 | "ecr:GetLifecyclePolicy", 338 | "ecr:GetLifecyclePolicyPreview", 339 | "ecr:ListTagsForResource", 340 | "ecr:DescribeImageScanFindings", 341 | ], 342 | resources: [ 343 | `arn:aws:ecr:${this.region}:${this.account}:repository/jenkins*` 344 | ], 345 | }), 346 | ], 347 | }), 348 | "create-loggroup": new iam.PolicyDocument({ 349 | statements: [ 350 | new iam.PolicyStatement({ 351 | effect: iam.Effect.ALLOW, 352 | actions: [ 353 | "logs:CreateLogGroup", 354 | "logs:PutLogEvents", 355 | "logs:TagResource" 356 | ], 357 | resources: [ 358 | `arn:aws:logs:${this.region}:${this.account}:log-group/*` 359 | ], 360 | }), 361 | ] 362 | }), 363 | "efs-access": new iam.PolicyDocument({ 364 | statements: [ 365 | new iam.PolicyStatement({ 366 | effect: iam.Effect.ALLOW, 367 | actions: [ 368 | "elasticfilesystem:ClientRootAccess", 369 | "elasticfilesystem:ClientMount", 370 | "elasticfilesystem:ClientWrite", 371 | "elasticfilesystem:DescribeMountTargets" 372 | ], 373 | resources: [ 374 | `arn:aws:elasticfilesystem:${this.region}:${this.account}:file-system/${fileSystem.fileSystemId}` 375 | ], 376 | }), 377 | ] 378 | }), 379 | "ssm-access": new iam.PolicyDocument({ 380 | statements: [ 381 | new iam.PolicyStatement({ 382 | effect: iam.Effect.ALLOW, 383 | actions: [ 384 | "ssmmessages:CreateControlChannel", 385 | "ssmmessages:CreateDataChannel", 386 | "ssmmessages:OpenControlChannel", 387 | "ssmmessages:OpenDataChannel" 388 | ], 389 | resources: ["*"], 390 | }), 391 | ] 392 | }), 393 | "launch-agent": new iam.PolicyDocument({ 394 | statements: [ 395 | new iam.PolicyStatement({ 396 | effect: iam.Effect.ALLOW, 397 | actions: [ 398 | "ecs:RegisterTaskDefinition", 399 | "ecs:ListClusters", 400 | "ecs:DescribeContainerInstances", 401 | "ecs:ListTaskDefinitions", 402 | "ecs:DescribeTaskDefinition", 403 | "ecs:DeregisterTaskDefinition", 404 | ], 405 | resources: ["*"], 406 | }), 407 | new iam.PolicyStatement({ 408 | effect: iam.Effect.ALLOW, 409 | actions: [ 410 | "ecs:ListContainerInstances", 411 | "ecs:DescribeClusters" 412 | ], 413 | resources: [ 414 | `arn:aws:ecs:${this.region}:${this.account}:cluster/${ecsCluster.clusterName}` 415 | ], 416 | }), 417 | new iam.PolicyStatement({ 418 | effect: iam.Effect.ALLOW, 419 | actions: [ 420 | "ecs:RunTask" 421 | ], 422 | conditions: { 423 | ArnEquals: { 424 | "ecs:cluster":`arn:aws:ecs:${this.region}:${this.account}:cluster/${ecsCluster.clusterName}` 425 | }, 426 | }, 427 | resources: [ 428 | `arn:aws:ecs:${this.region}:${this.account}:task-definition/*` 429 | ], 430 | }), 431 | new iam.PolicyStatement({ 432 | effect: iam.Effect.ALLOW, 433 | actions: [ 434 | "ecs:DescribeTasks", 435 | "ecs:StopTask", 436 | ], 437 | conditions: { 438 | ArnEquals: { 439 | "ecs:cluster":`arn:aws:ecs:${this.region}:${this.account}:cluster/${ecsCluster.clusterName}` 440 | }, 441 | }, 442 | resources: [ 443 | `arn:aws:ecs:${this.region}:${this.account}:task/*` 444 | ], 445 | }), 446 | new iam.PolicyStatement({ 447 | effect: iam.Effect.ALLOW, 448 | actions: [ 449 | "iam:PassRole" 450 | ], 451 | resources: [ 452 | jenkinsAgentTaskRole.roleArn, 453 | jenkinsAgentExecutionRole.roleArn 454 | ], 455 | }), 456 | ], 457 | }), 458 | "assume-role": new iam.PolicyDocument({ 459 | statements: [ 460 | new iam.PolicyStatement({ 461 | effect: iam.Effect.ALLOW, 462 | actions: [ 463 | "sts:AssumeRole" 464 | ], 465 | resources: [ 466 | `arn:${this.partition}:iam::${this.account}:role/jenkins-deployment-role`, 467 | `arn:${this.partition}:iam::${devTeam1Workload1TargetAccount}:role/jenkins-deployment-role` 468 | ], 469 | }), 470 | ] 471 | }) 472 | }, 473 | } 474 | ); 475 | 476 | /***************************************************************************/ 477 | /* Create Log Groups for Controller and Agent 478 | /***************************************************************************/ 479 | const controllerLogGroup = new logs.LogGroup(this, `${this.stackName}-controller-loggroup`, { 480 | logGroupName: `/aws/ecs/${ecsCluster.clusterName}/service/jenkins-controller`, 481 | retention: logs.RetentionDays.ONE_MONTH, 482 | removalPolicy: cdk.RemovalPolicy.DESTROY 483 | }); 484 | 485 | const controllerLogging = ecs.LogDrivers.awsLogs({ 486 | streamPrefix: this.jenkinsControllerName, 487 | logGroup: controllerLogGroup 488 | }); 489 | 490 | const agentLogGroup = new logs.LogGroup(this, `${this.stackName}-agent-loggroup`, { 491 | logGroupName: `/aws/ecs/${ecsCluster.clusterName}/service/jenkins-agent`, 492 | retention: logs.RetentionDays.ONE_MONTH, 493 | removalPolicy: cdk.RemovalPolicy.DESTROY 494 | }); 495 | 496 | const agentLogging = ecs.LogDrivers.awsLogs({ 497 | streamPrefix: this.jenkinsAgentName, 498 | logGroup: agentLogGroup 499 | }); 500 | 501 | /***************************************************************************/ 502 | /* Jenkins Agent Security Group Intercommunication 503 | /***************************************************************************/ 504 | const ecsJenkinsAgentSG = new ec2.SecurityGroup( 505 | this, 506 | `${this.stackName}-agent-sg`, 507 | { 508 | securityGroupName: `${this.stackName}-agent-sg`, 509 | vpc: vpc, 510 | allowAllOutbound: false, 511 | description: "Jenkins Agent Security Group", 512 | } 513 | ); 514 | 515 | ecsClusterSG.connections.allowFrom( 516 | new ec2.Connections({ 517 | securityGroups: [ecsJenkinsAgentSG], 518 | }), 519 | ec2.Port.tcp(this.jnlpPort), 520 | 'allow traffic on port 50000 from the Jenkins Agent', 521 | ); 522 | 523 | ecsJenkinsAgentSG.addEgressRule( 524 | ec2.Peer.ipv4('0.0.0.0/0'), 525 | ec2.Port.tcp(443), 526 | 'allow outbound traffic on port 443 from Jenkins Agent', 527 | ); 528 | 529 | /***************************************************************************/ 530 | /* Jenkins ECS Loadbalancer 531 | /***************************************************************************/ 532 | const jenkinsALB = new elb.ApplicationLoadBalancer(this, `${this.stackName}-controller-lb`, { 533 | vpc, 534 | internetFacing: false, 535 | loadBalancerName: this.stackName, 536 | vpcSubnets: { 537 | onePerAz: true, 538 | subnetGroupName: "private-alb" 539 | } 540 | }) 541 | 542 | /***************************************************************************/ 543 | /* Jenkins ECS Loadbalancer, Service and Task Definition 544 | /***************************************************************************/ 545 | const loadBalancedFargateService = 546 | new ecsPatterns.ApplicationLoadBalancedFargateService( 547 | this, 548 | `${this.stackName}-lb-fargate-service`, 549 | { 550 | loadBalancer: jenkinsALB, 551 | cluster: ecsCluster, 552 | securityGroups: [ecsClusterSG], 553 | taskSubnets: { 554 | subnetGroupName: "private-app" 555 | }, 556 | serviceName: "jenkins-controller-service", 557 | cpu: 2048, 558 | memoryLimitMiB: 4096, 559 | desiredCount: 1, 560 | assignPublicIp: true, 561 | healthCheckGracePeriod: cdk.Duration.seconds(300), 562 | taskImageOptions: { 563 | family: this.jenkinsControllerName, 564 | image: ecs.ContainerImage.fromEcrRepository(jenkinsControllerRepo, jenkinsControllerTag), 565 | containerName: this.jenkinsControllerName, 566 | containerPort: 8080, 567 | taskRole: jenkinsControllerTaskRole, 568 | executionRole: jenkinsControllerExecutionRole, 569 | enableLogging: true, 570 | logDriver: controllerLogging, 571 | environment: { 572 | ECS_CLUSTER: ecsCluster.clusterArn, 573 | AWS_REGION: this.region, 574 | JENKINS_URL: this.jenkinsBaseUrl, 575 | JENKINS_CONTROLLER_PRIVATE_TUNNEL_URL: `controller.${ecsCluster.defaultCloudMapNamespace?.namespaceName}:${this.jnlpPort}`, 576 | PRIVATE_SUBNET_IDS: vpc.privateSubnets.map((item) => {return item.subnetId}).join(","), 577 | AGENT_ECR_IMAGE_URL: `${jenkinsAgentLeaderRepo.repositoryUri}:${jenkinsAgentTag}`, 578 | AGENT_SECURITY_GROUP_ID: ecsJenkinsAgentSG.securityGroupId, 579 | AGENT_TASK_ROLE_ARN: jenkinsAgentTaskRole.roleArn, 580 | AGENT_EXECUTION_ROLE_ARN: jenkinsAgentExecutionRole.roleArn, 581 | LOG_GROUP: agentLogGroup.logGroupName, 582 | LOG_STREAM_PREFIX: this.jenkinsAgentName, 583 | DEFAULT_ACCOUNT: this.account, 584 | DEFAULT_ACCOUNT_JENKINS_ROLE: `arn:${this.partition}:iam::${this.account}:role/jenkins-deployment-role`, 585 | TEAM1_APP1_DEV_WORKLOAD_ACCOUNT: devTeam1Workload1TargetAccount, 586 | TEAM1_APP1_DEV_WORKLOAD_JENKINS_ROLE: `arn:${this.partition}:iam::${devTeam1Workload1TargetAccount}:role/jenkins-deployment-role` 587 | }, 588 | secrets: { 589 | JENKINS_USERNAME: ecs.Secret.fromSecretsManager(jenkinsCredentials, "username"), 590 | JENKINS_PASSWORD: ecs.Secret.fromSecretsManager(jenkinsCredentials, "password") 591 | } 592 | }, 593 | enableExecuteCommand: true, 594 | protocol: elb.ApplicationProtocol.HTTPS, 595 | certificate: this.certificate, 596 | sslPolicy: SslPolicy.TLS12_EXT, 597 | redirectHTTP: true 598 | } 599 | ); 600 | 601 | /***************************************************************************/ 602 | /* Jenkins create A record for the domain 603 | /***************************************************************************/ 604 | new route53.ARecord(this, `${this.stackName}-A-record`, { 605 | zone: hostedZone, 606 | recordName: this.jenkinsNakedDomainName, 607 | target: route53.RecordTarget.fromAlias(new targets.LoadBalancerTarget(loadBalancedFargateService.loadBalancer)), 608 | }); 609 | 610 | /***************************************************************************/ 611 | /* Jenkins Controller Task Definition EFS Mount and Volume 612 | /***************************************************************************/ 613 | loadBalancedFargateService.taskDefinition.addVolume({ 614 | name: "jenkins-volume", 615 | efsVolumeConfiguration: { 616 | fileSystemId: fileSystem.fileSystemId, 617 | transitEncryption: 'ENABLED', 618 | authorizationConfig:{ 619 | accessPointId: accessPoint.accessPointId, 620 | iam: 'ENABLED' 621 | } 622 | } 623 | }) 624 | 625 | loadBalancedFargateService.taskDefinition.defaultContainer?.addMountPoints({ 626 | containerPath: '/var/jenkins_home', 627 | sourceVolume: "jenkins-volume", 628 | readOnly: false 629 | }) 630 | 631 | /***************************************************************************/ 632 | /* Jenkins task port mappings and Cloudmap configuration 633 | /***************************************************************************/ 634 | loadBalancedFargateService.taskDefinition.defaultContainer?.addPortMappings({ 635 | containerPort: this.jnlpPort, 636 | hostPort: this.jnlpPort 637 | }); 638 | 639 | loadBalancedFargateService.service.enableCloudMap({ 640 | name: 'controller', 641 | dnsRecordType: servicediscovery.DnsRecordType.A, 642 | container: loadBalancedFargateService.taskDefinition.defaultContainer, 643 | containerPort: this.jnlpPort 644 | }) 645 | 646 | /***************************************************************************/ 647 | /* Jenkins LB healthcheck 648 | /***************************************************************************/ 649 | loadBalancedFargateService.targetGroup.configureHealthCheck({ 650 | path: "/login", 651 | port: "8080", 652 | }); 653 | 654 | } 655 | } 656 | -------------------------------------------------------------------------------- /cdk/lib/cdk-pipeline-ecr-stage.ts: -------------------------------------------------------------------------------- 1 | import { Stage, StageProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { CdkECRStack } from "./cdk-ecr-stack"; 4 | 5 | export class CdkPipelineECRStage extends Stage { 6 | constructor(scope: Construct, id: string, props?: StageProps) { 7 | super(scope, id, props); 8 | new CdkECRStack(this, "ecr", { 9 | description: "Jenkins ECR Repo Configuration" 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cdk/lib/cdk-pipeline-infra-stage.ts: -------------------------------------------------------------------------------- 1 | import { Stage, StageProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { CdkInfraStack } from "./cdk-infra-stack"; 4 | 5 | export class CdkPipelineInfraStage extends Stage { 6 | constructor(scope: Construct, id: string, props?: StageProps) { 7 | super(scope, id, props); 8 | new CdkInfraStack(this, "app", { 9 | description: "Jenkins Application and Infrastructure" 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cdk/lib/cdk-pipeline-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { CdkApp } from "./cdk-app"; 4 | import { CdkUtil} from "./cdk-util"; 5 | import { pipelines, StackProps } from "aws-cdk-lib"; 6 | import { CdkPipelineECRStage } from "./cdk-pipeline-ecr-stage"; 7 | import { CdkPipelineInfraStage } from "./cdk-pipeline-infra-stage"; 8 | import * as iam from "aws-cdk-lib/aws-iam"; 9 | import * as ssm from "aws-cdk-lib/aws-ssm"; 10 | import * as codecommit from "aws-cdk-lib/aws-codecommit"; 11 | import * as codebuild from "aws-cdk-lib/aws-codebuild"; 12 | import { 13 | CodeBuildStep, 14 | CodePipeline, 15 | CodePipelineSource, 16 | } from "aws-cdk-lib/pipelines"; 17 | import { Cache, LocalCacheMode } from "aws-cdk-lib/aws-codebuild"; 18 | 19 | export class CdkPipelineStack extends cdk.Stack { 20 | private jenkinsControllerName: string; 21 | private jenkinsControllerImageTagParameterName: string; 22 | private jenkinsAgentName: string; 23 | private jenkinsAgentImageTagParameterName: string; 24 | private jenkinsPrivateRootCA: string; 25 | 26 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 27 | super(scope, id, props); 28 | 29 | /************************************************************************/ 30 | /* Get CDK Context Values 31 | /************************************************************************/ 32 | const util = new CdkUtil(this); 33 | this.jenkinsControllerName = util.getCdkContextValue("ctxJenkinsControllerName"); 34 | this.jenkinsControllerImageTagParameterName = util.getCdkContextValue("ctxJenkinsControllerImageTagParameterName"); 35 | this.jenkinsAgentName = util.getCdkContextValue("ctxJenkinsAgentName"); 36 | this.jenkinsAgentImageTagParameterName = util.getCdkContextValue("ctxJenkinsAgentImageTagParameterName"); 37 | this.jenkinsPrivateRootCA = util.getCdkContextValue("ctxJenkinsPrivateRootCAParameterName"); 38 | 39 | /************************************************************************/ 40 | /* Code Commit Repo 41 | /************************************************************************/ 42 | const repo = new codecommit.Repository(this, `${this.stackName}-repo`, { 43 | repositoryName: this.stackName, 44 | description: "Repository for Jenkins Application Code and Infrastructure", 45 | }); 46 | 47 | /************************************************************************/ 48 | /* Create SSM Parameters for storing Docker Image Versions 49 | /************************************************************************/ 50 | new ssm.StringParameter(this, `${this.stackName}-controller-docker-image-version`, { 51 | parameterName: this.jenkinsControllerImageTagParameterName, 52 | stringValue: 'latest', 53 | }); 54 | 55 | new ssm.StringParameter(this, `${this.stackName}-agent-docker-image-version`, { 56 | parameterName: this.jenkinsAgentImageTagParameterName, 57 | stringValue: 'latest', 58 | }); 59 | 60 | /************************************************************************/ 61 | /* Code Pipeline 62 | /************************************************************************/ 63 | const pipeline = new CodePipeline(this, `${this.stackName}-pipeline`, { 64 | pipelineName: `${this.stackName}-pipeline`, 65 | selfMutation: false, 66 | crossAccountKeys: false, 67 | synth: new CodeBuildStep(`${this.stackName}-synth`, { 68 | projectName: `${this.stackName}-synth`, 69 | input: CodePipelineSource.codeCommit(repo, "main"), 70 | installCommands: ["npm install -g aws-cdk"], 71 | commands: ["cd cdk", "npm ci", "npm run test", "npm run build", "npx cdk synth"], 72 | primaryOutputDirectory: "cdk/cdk.out", 73 | buildEnvironment: { 74 | computeType: codebuild.ComputeType.MEDIUM, 75 | }, 76 | }), 77 | }); 78 | 79 | /************************************************************************/ 80 | /* Code Build Role 81 | /************************************************************************/ 82 | const buildRole = new iam.Role(this, `${this.stackName}-build-role`, { 83 | assumedBy: new iam.ServicePrincipal("codebuild.amazonaws.com"), 84 | description: `${this.stackName}-build-role`, 85 | inlinePolicies: { 86 | "jenkins-code-build-policy": new iam.PolicyDocument({ 87 | statements: [ 88 | new iam.PolicyStatement({ 89 | effect: iam.Effect.ALLOW, 90 | actions: [ 91 | "ssm:GetParameter", 92 | "ssm:PutParameter" 93 | ], 94 | resources: [ 95 | `arn:${this.partition}:ssm:${this.region}:${this.account}:parameter${this.jenkinsControllerImageTagParameterName}`, 96 | `arn:${this.partition}:ssm:${this.region}:${this.account}:parameter${this.jenkinsAgentImageTagParameterName}`, 97 | ], 98 | }), 99 | new iam.PolicyStatement({ 100 | effect: iam.Effect.ALLOW, 101 | actions: ["secretsmanager:GetSecretValue"], 102 | resources: [ 103 | `arn:${this.partition}:secretsmanager:${this.region}:${this.account}:secret:dockerhub_credentials`, 104 | `arn:${this.partition}:secretsmanager:${this.region}:${this.account}:secret:${this.jenkinsPrivateRootCA}*` 105 | ], 106 | }), 107 | new iam.PolicyStatement({ 108 | effect: iam.Effect.ALLOW, 109 | actions: ["ecr:GetAuthorizationToken"], 110 | resources: ["*"], 111 | }), 112 | new iam.PolicyStatement({ 113 | effect: iam.Effect.ALLOW, 114 | actions: [ 115 | "ecr:BatchCheckLayerAvailability", 116 | "ecr:GetDownloadUrlForLayer", 117 | "ecr:GetRepositoryPolicy", 118 | "ecr:DescribeRepositories", 119 | "ecr:ListImages", 120 | "ecr:DescribeImages", 121 | "ecr:BatchGetImage", 122 | "ecr:GetLifecyclePolicy", 123 | "ecr:GetLifecyclePolicyPreview", 124 | "ecr:ListTagsForResource", 125 | "ecr:DescribeImageScanFindings", 126 | "ecr:InitiateLayerUpload", 127 | "ecr:UploadLayerPart", 128 | "ecr:CompleteLayerUpload", 129 | "ecr:PutImage", 130 | ], 131 | resources: [ 132 | `arn:${this.partition}:ecr:${this.region}:${this.account}:repository/jenkins*`, 133 | ], 134 | }), 135 | ], 136 | }), 137 | }, 138 | }); 139 | 140 | /************************************************************************/ 141 | /* Code Build and Push to ECR Stage 142 | /************************************************************************/ 143 | const app = new CdkApp(this); 144 | const buildECRImage = new CdkPipelineECRStage( 145 | this, 146 | `${this.stackName}-build-image` 147 | ); 148 | pipeline.addStage(buildECRImage, { 149 | post: [ 150 | new pipelines.CodeBuildStep(`${this.stackName}-build-controller`, { 151 | projectName: `${this.stackName}-build-controller`, 152 | partialBuildSpec: codebuild.BuildSpec.fromObject( 153 | app.getBuildSpec("jenkins/controller", this.jenkinsControllerImageTagParameterName, this.jenkinsPrivateRootCA) 154 | ), 155 | commands: ["echo building jenkins controller image"], 156 | buildEnvironment: { 157 | buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3, 158 | computeType: codebuild.ComputeType.MEDIUM, 159 | privileged: true, 160 | environmentVariables: app.getBuildEnvironment(this.jenkinsControllerName), 161 | }, 162 | role: buildRole, 163 | cache: Cache.local(LocalCacheMode.DOCKER_LAYER) 164 | }), 165 | new pipelines.CodeBuildStep(`${this.stackName}-build-agent`, { 166 | projectName: `${this.stackName}-build-agent`, 167 | partialBuildSpec: codebuild.BuildSpec.fromObject( 168 | app.getBuildSpec("jenkins/agent", this.jenkinsAgentImageTagParameterName, this.jenkinsPrivateRootCA) 169 | ), 170 | commands: ["echo building jenkins agent image"], 171 | buildEnvironment: { 172 | buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3, 173 | computeType: codebuild.ComputeType.MEDIUM, 174 | privileged: true, 175 | environmentVariables: app.getBuildEnvironment(this.jenkinsAgentName), 176 | }, 177 | role: buildRole, 178 | cache: Cache.local(LocalCacheMode.DOCKER_LAYER) 179 | }), 180 | ], 181 | }); 182 | 183 | /************************************************************************/ 184 | /* Code Deploy Stage 185 | /************************************************************************/ 186 | const deploy = new CdkPipelineInfraStage(this, `${this.stackName}-deploy`); 187 | pipeline.addStage(deploy); 188 | 189 | /************************************************************************/ 190 | /* Code Deploy Stage 191 | /************************************************************************/ 192 | new cdk.CfnOutput(this, "CodeCommitRepositoryUrl", { 193 | value: repo.repositoryCloneUrlHttp, 194 | }); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /cdk/lib/cdk-util.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from "aws-cdk-lib"; 2 | 3 | export class CdkUtil { 4 | private stack: Stack; 5 | 6 | constructor(stack: Stack) { 7 | this.stack = stack; 8 | } 9 | 10 | public getCdkContextValue(key: string) { 11 | let val = this.stack.node.tryGetContext(key); 12 | 13 | if (val === "UPDATEME") { 14 | throw Error(key + " cannot be the default UPDATEME value"); 15 | } 16 | 17 | if (val === null || val === undefined || val === "") { 18 | throw Error(key + " cannot be null or empty or DEFAULT value"); 19 | } 20 | return val; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.14", 15 | "@types/node": "22.7.9", 16 | "jest": "^29.7.0", 17 | "ts-jest": "^29.2.5", 18 | "aws-cdk": "2.1006.0", 19 | "ts-node": "^10.9.2", 20 | "typescript": "~5.6.3" 21 | }, 22 | "dependencies": { 23 | "aws-cdk-lib": "2.189.1", 24 | "cdk-nag": "^2.21.10", 25 | "constructs": "^10.0.0", 26 | "source-map-support": "^0.5.21" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cdk/test/cdk-app.test.ts: -------------------------------------------------------------------------------- 1 | import assert = require('assert'); 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { CdkApp } from '../lib/cdk-app'; 4 | import * as CdkPipelineStack from '../lib/cdk-pipeline-stack'; 5 | import { CdkUtil } from '../lib/cdk-util'; 6 | const fs = require('fs'); 7 | 8 | test("CDK Prepare Build Spec", () => { 9 | //GIVEN 10 | const app = new cdk.App({context: { 11 | "ctxJnlpPort": 50000, 12 | "ctxHostedZoneName": "example.com", 13 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 14 | "ctxJenkinsControllerName": "jenkins-controller", 15 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 16 | "ctxJenkinsAgentName": "jenkins-agent", 17 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 18 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 19 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 20 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 21 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 22 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 23 | "ctxACMCertMode": "self-signed" 24 | }}); 25 | 26 | // WHEN 27 | const stack = new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack'); 28 | const cdkApp = new CdkApp(stack); 29 | const controllerBuildSpec = cdkApp.getBuildSpec("jenkins-controller", "/dev/jenkins/controller/docker/image/tag", "/dev/jenkins/rootCA"); 30 | const agentBuildSpec = cdkApp.getBuildSpec("jenkins-agent", "/dev/jenkins/agent/docker/image/tag", "/dev/jenkins/rootCA"); 31 | 32 | // THEN 33 | //console.log(controllerBuildSpec); 34 | //console.log(agentBuildSpec.phases.build); 35 | 36 | }); 37 | 38 | -------------------------------------------------------------------------------- /cdk/test/cdk-ecr-stack.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Template } from "aws-cdk-lib/assertions"; 3 | import * as CdkECRStack from "../lib/cdk-ecr-stack"; 4 | 5 | test("CDK ECR Stack", () => { 6 | const app = new cdk.App(); 7 | 8 | // WHEN 9 | const stack = new CdkECRStack.CdkECRStack(app, "CDKECRStack"); 10 | 11 | // THEN 12 | const template = Template.fromStack(stack); 13 | 14 | // ASSERT 15 | template.resourceCountIs("AWS::ECR::Repository", 2); 16 | template.hasResourceProperties("AWS::ECR::Repository", { 17 | ImageScanningConfiguration: { 18 | "ScanOnPush": true 19 | }, 20 | LifecyclePolicy: { 21 | "LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"untagged\",\"countType\":\"sinceImagePushed\",\"countNumber\":1,\"countUnit\":\"days\"},\"action\":{\"type\":\"expire\"}},{\"rulePriority\":2,\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":10},\"action\":{\"type\":\"expire\"}}]}" 22 | } 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /cdk/test/cdk-infra-stack.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Template } from "aws-cdk-lib/assertions"; 3 | import * as CdkInfraStack from "../lib/cdk-infra-stack"; 4 | 5 | test("CDK Infra Stack Self Signed Cert", () => { 6 | //GIVEN 7 | const app = new cdk.App({context: { 8 | "ctxJnlpPort": 50000, 9 | "ctxHostedZoneName": "example.com", 10 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 11 | "ctxJenkinsControllerName": "jenkins-controller", 12 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 13 | "ctxJenkinsAgentName": "jenkins-agent", 14 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 15 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 16 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 17 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 18 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 19 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 20 | "ctxACMCertMode": "self-signed" 21 | }}); 22 | 23 | // WHEN 24 | const stack = new CdkInfraStack.CdkInfraStack(app, 'CDKInfraStack'); 25 | 26 | // THEN 27 | const template = Template.fromStack(stack); 28 | 29 | // Assert VPC and Subnets 30 | template.resourceCountIs("AWS::EC2::VPC", 1); 31 | template.hasResourceProperties("AWS::EC2::VPC", { 32 | CidrBlock: "10.0.0.0/20", 33 | EnableDnsHostnames: true, 34 | EnableDnsSupport: true, 35 | }); 36 | 37 | template.resourceCountIs("AWS::EC2::Subnet", 6); 38 | template.hasResourceProperties("AWS::EC2::Subnet", { 39 | CidrBlock: "10.0.0.0/24", 40 | MapPublicIpOnLaunch: false, 41 | }); 42 | 43 | template.resourceCountIs("AWS::ECS::Service", 1); 44 | template.hasResourceProperties("AWS::ECS::Service", { 45 | "DesiredCount": 1, 46 | "EnableECSManagedTags": false, 47 | "EnableExecuteCommand": true, 48 | "HealthCheckGracePeriodSeconds": 300, 49 | "LaunchType": "FARGATE", 50 | "ServiceName": "jenkins-controller-service", 51 | "DeploymentConfiguration": { 52 | "MaximumPercent": 200, 53 | "MinimumHealthyPercent": 50 54 | }, 55 | }); 56 | 57 | template.resourceCountIs("AWS::ECS::TaskDefinition", 1); 58 | template.hasResourceProperties("AWS::ECS::TaskDefinition", { 59 | Cpu: "2048", 60 | Family: "jenkins-controller", 61 | Memory: "4096", 62 | NetworkMode: "awsvpc", 63 | RequiresCompatibilities: [ 64 | "FARGATE" 65 | ], 66 | }); 67 | 68 | template.resourceCountIs("AWS::ECS::Cluster", 1); 69 | template.hasResourceProperties("AWS::ECS::Cluster", { 70 | "ClusterName": "CDKInfraStack-ecs-cluster", 71 | "ClusterSettings": [ 72 | { 73 | "Name": "containerInsights", 74 | "Value": "enabled" 75 | } 76 | ] 77 | }) 78 | 79 | template.resourceCountIs("AWS::ServiceDiscovery::PrivateDnsNamespace", 1); 80 | template.hasResourceProperties("AWS::ServiceDiscovery::PrivateDnsNamespace", { 81 | "Name": "CDKInfraStack-private", 82 | }) 83 | 84 | template.resourceCountIs("AWS::Route53::RecordSet", 1); 85 | template.hasResourceProperties("AWS::Route53::RecordSet", { 86 | "Name": "jenkins-test.example.com.", 87 | "Type": "A", 88 | }) 89 | 90 | 91 | template.resourceCountIs("AWS::ServiceDiscovery::Service", 1); 92 | template.hasResourceProperties("AWS::ServiceDiscovery::Service", { 93 | "Name": "controller", 94 | "HealthCheckCustomConfig": { 95 | "FailureThreshold": 1 96 | }, 97 | }) 98 | 99 | template.resourceCountIs("AWS::EFS::FileSystem", 1); 100 | template.hasResourceProperties("AWS::EFS::FileSystem", { 101 | "BackupPolicy": { 102 | "Status": "ENABLED" 103 | }, 104 | "Encrypted": true, 105 | "FileSystemTags": [ 106 | { 107 | "Key": "Name", 108 | "Value": "CDKInfraStack-efs" 109 | }, 110 | ], 111 | "PerformanceMode": "generalPurpose" 112 | }); 113 | template.resourceCountIs("AWS::EFS::AccessPoint", 1); 114 | template.hasResourceProperties("AWS::EFS::AccessPoint", { 115 | "FileSystemId": { 116 | "Ref": "CDKInfraStackefsD9202CAF" 117 | }, 118 | "PosixUser": { 119 | "Gid": "1000", 120 | "Uid": "1000" 121 | }, 122 | "RootDirectory": { 123 | "CreationInfo": { 124 | "OwnerGid": "1000", 125 | "OwnerUid": "1000", 126 | "Permissions": "755" 127 | }, 128 | "Path": "/jenkins" 129 | } 130 | }) 131 | 132 | template.resourceCountIs("AWS::EC2::SecurityGroup", 4); 133 | template.resourceCountIs("AWS::Logs::LogGroup", 2); 134 | 135 | template.resourceCountIs("AWS::ElasticLoadBalancingV2::LoadBalancer", 1); 136 | template.hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", { 137 | "LoadBalancerAttributes": [ 138 | { 139 | "Key": "deletion_protection.enabled", 140 | "Value": "false" 141 | } 142 | ], 143 | "Name": "CDKInfraStack", 144 | "Scheme": "internal", 145 | }) 146 | 147 | 148 | template.resourceCountIs("AWS::ElasticLoadBalancingV2::Listener", 2); 149 | template.hasResourceProperties("AWS::ElasticLoadBalancingV2::Listener", { 150 | "Port": 443, 151 | "Protocol": "HTTPS", 152 | "SslPolicy": "ELBSecurityPolicy-TLS-1-2-Ext-2018-06" 153 | }) 154 | 155 | template.hasResourceProperties("AWS::ElasticLoadBalancingV2::Listener", { 156 | "Port": 80, 157 | "Protocol": "HTTP", 158 | }) 159 | 160 | template.resourceCountIs("AWS::ElasticLoadBalancingV2::TargetGroup", 1); 161 | template.hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", { 162 | "HealthCheckPath": "/login", 163 | "HealthCheckPort": "8080", 164 | "Port": 80, 165 | "Protocol": "HTTP", 166 | "TargetGroupAttributes": [ 167 | { 168 | "Key": "stickiness.enabled", 169 | "Value": "false" 170 | } 171 | ], 172 | "TargetType": "ip", 173 | }) 174 | }); 175 | 176 | test("CDK Infra Stack ACM PCA", () => { 177 | //GIVEN 178 | const app = new cdk.App({context: { 179 | "ctxJnlpPort": 50000, 180 | "ctxHostedZoneName": "example.com", 181 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 182 | "ctxJenkinsControllerName": "jenkins-controller", 183 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 184 | "ctxJenkinsAgentName": "jenkins-agent", 185 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 186 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 187 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 188 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 189 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 190 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 191 | "ctxACMCertMode": "acm-pca" 192 | }}); 193 | 194 | // WHEN 195 | const stack = new CdkInfraStack.CdkInfraStack(app, 'CDKInfraStack'); 196 | 197 | // THEN 198 | const template = Template.fromStack(stack); 199 | 200 | // Assert VPC and Subnets 201 | template.resourceCountIs("AWS::EC2::VPC", 1); 202 | template.hasResourceProperties("AWS::EC2::VPC", { 203 | CidrBlock: "10.0.0.0/20", 204 | EnableDnsHostnames: true, 205 | EnableDnsSupport: true, 206 | }); 207 | 208 | template.resourceCountIs("AWS::EC2::Subnet", 6); 209 | template.hasResourceProperties("AWS::EC2::Subnet", { 210 | CidrBlock: "10.0.0.0/24", 211 | MapPublicIpOnLaunch: false, 212 | }); 213 | 214 | template.resourceCountIs("AWS::ECS::Service", 1); 215 | template.hasResourceProperties("AWS::ECS::Service", { 216 | "DesiredCount": 1, 217 | "EnableECSManagedTags": false, 218 | "EnableExecuteCommand": true, 219 | "HealthCheckGracePeriodSeconds": 300, 220 | "LaunchType": "FARGATE", 221 | "ServiceName": "jenkins-controller-service", 222 | "DeploymentConfiguration": { 223 | "MaximumPercent": 200, 224 | "MinimumHealthyPercent": 50 225 | }, 226 | }); 227 | 228 | template.resourceCountIs("AWS::ECS::TaskDefinition", 1); 229 | template.hasResourceProperties("AWS::ECS::TaskDefinition", { 230 | Cpu: "2048", 231 | Family: "jenkins-controller", 232 | Memory: "4096", 233 | NetworkMode: "awsvpc", 234 | RequiresCompatibilities: [ 235 | "FARGATE" 236 | ], 237 | }); 238 | 239 | template.resourceCountIs("AWS::ECS::Cluster", 1); 240 | template.hasResourceProperties("AWS::ECS::Cluster", { 241 | "ClusterName": "CDKInfraStack-ecs-cluster", 242 | "ClusterSettings": [ 243 | { 244 | "Name": "containerInsights", 245 | "Value": "enabled" 246 | } 247 | ] 248 | }) 249 | 250 | template.resourceCountIs("AWS::ServiceDiscovery::PrivateDnsNamespace", 1); 251 | template.hasResourceProperties("AWS::ServiceDiscovery::PrivateDnsNamespace", { 252 | "Name": "CDKInfraStack-private", 253 | }) 254 | 255 | template.resourceCountIs("AWS::Route53::RecordSet", 1); 256 | template.hasResourceProperties("AWS::Route53::RecordSet", { 257 | "Name": "jenkins-test.example.com.", 258 | "Type": "A", 259 | }) 260 | 261 | template.resourceCountIs("AWS::ServiceDiscovery::Service", 1); 262 | template.hasResourceProperties("AWS::ServiceDiscovery::Service", { 263 | "Name": "controller", 264 | "HealthCheckCustomConfig": { 265 | "FailureThreshold": 1 266 | }, 267 | }) 268 | 269 | template.resourceCountIs("AWS::CertificateManager::Certificate", 1); 270 | template.hasResourceProperties("AWS::CertificateManager::Certificate", { 271 | "DomainName": "jenkins-test.example.com", 272 | }) 273 | 274 | template.resourceCountIs("AWS::EFS::FileSystem", 1); 275 | template.hasResourceProperties("AWS::EFS::FileSystem", { 276 | "BackupPolicy": { 277 | "Status": "ENABLED" 278 | }, 279 | "Encrypted": true, 280 | "FileSystemTags": [ 281 | { 282 | "Key": "Name", 283 | "Value": "CDKInfraStack-efs" 284 | }, 285 | ], 286 | "PerformanceMode": "generalPurpose" 287 | }); 288 | template.resourceCountIs("AWS::EFS::AccessPoint", 1); 289 | template.hasResourceProperties("AWS::EFS::AccessPoint", { 290 | "FileSystemId": { 291 | "Ref": "CDKInfraStackefsD9202CAF" 292 | }, 293 | "PosixUser": { 294 | "Gid": "1000", 295 | "Uid": "1000" 296 | }, 297 | "RootDirectory": { 298 | "CreationInfo": { 299 | "OwnerGid": "1000", 300 | "OwnerUid": "1000", 301 | "Permissions": "755" 302 | }, 303 | "Path": "/jenkins" 304 | } 305 | }) 306 | 307 | template.resourceCountIs("AWS::EC2::SecurityGroup", 4); 308 | template.resourceCountIs("AWS::Logs::LogGroup", 2); 309 | 310 | template.resourceCountIs("AWS::ElasticLoadBalancingV2::LoadBalancer", 1); 311 | template.hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", { 312 | "LoadBalancerAttributes": [ 313 | { 314 | "Key": "deletion_protection.enabled", 315 | "Value": "false" 316 | } 317 | ], 318 | "Name": "CDKInfraStack", 319 | "Scheme": "internal", 320 | }) 321 | 322 | 323 | template.resourceCountIs("AWS::ElasticLoadBalancingV2::Listener", 2); 324 | template.hasResourceProperties("AWS::ElasticLoadBalancingV2::Listener", { 325 | "Port": 443, 326 | "Protocol": "HTTPS", 327 | "SslPolicy": "ELBSecurityPolicy-TLS-1-2-Ext-2018-06" 328 | }) 329 | 330 | template.hasResourceProperties("AWS::ElasticLoadBalancingV2::Listener", { 331 | "Port": 80, 332 | "Protocol": "HTTP", 333 | }) 334 | 335 | template.resourceCountIs("AWS::ElasticLoadBalancingV2::TargetGroup", 1); 336 | template.hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", { 337 | "HealthCheckPath": "/login", 338 | "HealthCheckPort": "8080", 339 | "Port": 80, 340 | "Protocol": "HTTP", 341 | "TargetGroupAttributes": [ 342 | { 343 | "Key": "stickiness.enabled", 344 | "Value": "false" 345 | } 346 | ], 347 | "TargetType": "ip", 348 | }) 349 | }); 350 | -------------------------------------------------------------------------------- /cdk/test/cdk-pipeline-stack.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as Cdk from '../lib/cdk-pipeline-stack'; 4 | 5 | test('CDK Pipeline Stack', () => { 6 | const stackName = "CDKPipelineStack"; 7 | 8 | //GIVEN 9 | const app = new cdk.App({context: { 10 | "ctxJnlpPort": 50000, 11 | "ctxHostedZoneName": "example.com", 12 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 13 | "ctxJenkinsControllerName": "jenkins-controller", 14 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 15 | "ctxJenkinsAgentName": "jenkins-agent", 16 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 17 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 18 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 19 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 20 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 21 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 22 | "ctxACMCertMode": "self-signed" 23 | }}); 24 | 25 | // WHEN 26 | const stack = new Cdk.CdkPipelineStack(app, 'CDKPipelineStack'); 27 | 28 | // THEN 29 | const template = Template.fromStack(stack); 30 | 31 | // Assert Code Commit Repo 32 | template.resourceCountIs("AWS::CodeCommit::Repository", 1); 33 | template.hasResourceProperties("AWS::CodeCommit::Repository", { 34 | RepositoryName: stackName, 35 | }); 36 | 37 | // Assert Code Pipeline 38 | template.resourceCountIs("AWS::CodePipeline::Pipeline", 1); 39 | template.hasResourceProperties("AWS::CodePipeline::Pipeline", { 40 | Name: `${stackName}-pipeline`, 41 | }); 42 | 43 | // Assert Code Build Project 44 | template.resourceCountIs("AWS::CodeBuild::Project", 3); 45 | template.hasResourceProperties("AWS::CodeBuild::Project", { 46 | "Environment": { 47 | "ComputeType": "BUILD_GENERAL1_MEDIUM", 48 | "PrivilegedMode": false, 49 | "Type": "LINUX_CONTAINER" 50 | }, 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /cdk/test/cdk-util.test.ts: -------------------------------------------------------------------------------- 1 | import assert = require('assert'); 2 | import * as cdk from 'aws-cdk-lib'; 3 | import * as CdkPipelineStack from '../lib/cdk-pipeline-stack'; 4 | import { CdkUtil } from '../lib/cdk-util'; 5 | 6 | test("CDK Util Context Values Exists", () => { 7 | //GIVEN 8 | const app = new cdk.App({context: { 9 | "ctxJnlpPort": 50000, 10 | "ctxHostedZoneName": "example.com", 11 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 12 | "ctxJenkinsControllerName": "jenkins-controller", 13 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 14 | "ctxJenkinsAgentName": "jenkins-agent", 15 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 16 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 17 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 18 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 19 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 20 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 21 | "ctxACMCertMode": "self-signed" 22 | }}); 23 | 24 | // WHEN 25 | let stack = new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack'); 26 | 27 | // THEN 28 | let cdkUtil = new CdkUtil(stack); 29 | 30 | // ASSERT 31 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxJnlpPort"), 50000) 32 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxHostedZoneName"), "example.com") 33 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxJenkinsDomainNamePrefix"), "jenkins-test") 34 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxJenkinsControllerName"), "jenkins-controller") 35 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxJenkinsControllerImageTagParameterName"), "/dev/jenkins/controller/docker/image/tag") 36 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxJenkinsAgentName"), "jenkins-agent") 37 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxJenkinsAgentImageTagParameterName"), "/dev/jenkins/agent/docker/image/tag") 38 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxJenkinsAdminCredentialSecretName"), "/dev/jenkins/admin/credentials") 39 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxDevTeam1Workload1AWSAccountIdParameterName"), "/dev/team1/workload1/AWSAccountID") 40 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxACMPCACertificateArnParameterName"), "/dev/jenkins/acmpca/certificateAuthorityArn") 41 | assert.strictEqual(cdkUtil.getCdkContextValue("ctxJenkinsPrivateRootCAParameterName"), "/dev/jenkins/rootCA") 42 | }); 43 | 44 | test("CDK Util Context Value ctxJnlpPort missing Throws Error", () => { 45 | //GIVEN 46 | const app = new cdk.App({context: { 47 | "ctxHostedZoneName": "example.com", 48 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 49 | "ctxJenkinsControllerName": "jenkins-controller", 50 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 51 | "ctxJenkinsAgentName": "jenkins-agent", 52 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 53 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 54 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 55 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 56 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 57 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 58 | "ctxACMCertMode": "self-signed" 59 | }}); 60 | 61 | // WHEN, THEN 62 | expect(() => { 63 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 64 | }).toThrowError("ctxJnlpPort cannot be null"); 65 | 66 | }); 67 | 68 | test("CDK Util Context Value ctxHostedZoneName missing Throws Error", () => { 69 | //GIVEN 70 | const app = new cdk.App({context: { 71 | "ctxJnlpPort": 50000, 72 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 73 | "ctxJenkinsControllerName": "jenkins-controller", 74 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 75 | "ctxJenkinsAgentName": "jenkins-agent", 76 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 77 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 78 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 79 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 80 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 81 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 82 | "ctxACMCertMode": "self-signed" 83 | }}); 84 | 85 | // WHEN, THEN 86 | expect(() => { 87 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 88 | }).toThrowError("ctxHostedZoneName cannot be null"); 89 | 90 | }); 91 | 92 | test("CDK Util Context Value ctxJenkinsDomainNamePrefix missing Throws Error", () => { 93 | //GIVEN 94 | const app = new cdk.App({context: { 95 | "ctxJnlpPort": 50000, 96 | "ctxHostedZoneName": "example.com", 97 | "ctxJenkinsControllerName": "jenkins-controller", 98 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 99 | "ctxJenkinsAgentName": "jenkins-agent", 100 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 101 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 102 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 103 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 104 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 105 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 106 | "ctxACMCertMode": "self-signed" 107 | }}); 108 | 109 | // WHEN, THEN 110 | expect(() => { 111 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 112 | }).toThrowError("ctxJenkinsDomainNamePrefix cannot be null"); 113 | 114 | }); 115 | 116 | test("CDK Util Context Value ctxJenkinsControllerName missing Throws Error", () => { 117 | //GIVEN 118 | const app = new cdk.App({context: { 119 | "ctxJnlpPort": 50000, 120 | "ctxHostedZoneName": "example.com", 121 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 122 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 123 | "ctxJenkinsAgentName": "jenkins-agent", 124 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 125 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 126 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 127 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 128 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 129 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 130 | "ctxACMCertMode": "self-signed" 131 | }}); 132 | 133 | // WHEN, THEN 134 | expect(() => { 135 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 136 | }).toThrowError("ctxJenkinsControllerName cannot be null"); 137 | 138 | }); 139 | 140 | test("CDK Util Context Value ctxJenkinsControllerImageTagParameterName missing Throws Error", () => { 141 | //GIVEN 142 | const app = new cdk.App({context: { 143 | "ctxJnlpPort": 50000, 144 | "ctxHostedZoneName": "example.com", 145 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 146 | "ctxJenkinsControllerName": "jenkins-controller", 147 | "ctxJenkinsAgentName": "jenkins-agent", 148 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 149 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 150 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 151 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 152 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 153 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 154 | "ctxACMCertMode": "self-signed" 155 | }}); 156 | 157 | // WHEN, THEN 158 | expect(() => { 159 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 160 | }).toThrowError("ctxJenkinsControllerImageTagParameterName cannot be null"); 161 | 162 | }); 163 | 164 | test("CDK Util Context Value ctxJenkinsAgentName missing Throws Error", () => { 165 | //GIVEN 166 | const app = new cdk.App({context: { 167 | "ctxJnlpPort": 50000, 168 | "ctxHostedZoneName": "example.com", 169 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 170 | "ctxJenkinsControllerName": "jenkins-controller", 171 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 172 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 173 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 174 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 175 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 176 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 177 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 178 | "ctxACMCertMode": "self-signed" 179 | }}); 180 | 181 | // WHEN, THEN 182 | expect(() => { 183 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 184 | }).toThrowError("ctxJenkinsAgentName cannot be null"); 185 | 186 | }); 187 | 188 | test("CDK Util Context Value ctxJenkinsAgentImageTagParameterName missing Throws Error", () => { 189 | //GIVEN 190 | const app = new cdk.App({context: { 191 | "ctxJnlpPort": 50000, 192 | "ctxHostedZoneName": "example.com", 193 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 194 | "ctxJenkinsControllerName": "jenkins-controller", 195 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 196 | "ctxJenkinsAgentName": "jenkins-agent", 197 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 198 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 199 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 200 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 201 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 202 | "ctxACMCertMode": "self-signed" 203 | }}); 204 | 205 | // WHEN, THEN 206 | expect(() => { 207 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 208 | }).toThrowError("ctxJenkinsAgentImageTagParameterName cannot be null"); 209 | 210 | }); 211 | 212 | test("CDK Util Context Value ctxJenkinsAdminCredentialSecretName missing Throws Error", () => { 213 | //GIVEN 214 | const app = new cdk.App({context: { 215 | "ctxJnlpPort": 50000, 216 | "ctxHostedZoneName": "example.com", 217 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 218 | "ctxJenkinsControllerName": "jenkins-controller", 219 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 220 | "ctxJenkinsAgentName": "jenkins-agent", 221 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 222 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 223 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 224 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 225 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 226 | "ctxACMCertMode": "self-signed" 227 | }}); 228 | 229 | // WHEN, THEN 230 | expect(() => { 231 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 232 | }).toThrowError("ctxJenkinsAdminCredentialSecretName cannot be null"); 233 | 234 | }); 235 | 236 | test("CDK Util Context Value ctxDevTeam1Workload1AWSAccountIdParameterName missing Throws Error", () => { 237 | //GIVEN 238 | const app = new cdk.App({context: { 239 | "ctxJnlpPort": 50000, 240 | "ctxHostedZoneName": "example.com", 241 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 242 | "ctxJenkinsControllerName": "jenkins-controller", 243 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 244 | "ctxJenkinsAgentName": "jenkins-agent", 245 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 246 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 247 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 248 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 249 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 250 | "ctxACMCertMode": "self-signed" 251 | }}); 252 | 253 | // WHEN, THEN 254 | expect(() => { 255 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 256 | }).toThrowError("ctxDevTeam1Workload1AWSAccountIdParameterName cannot be null"); 257 | 258 | }); 259 | 260 | test("CDK Util Context Value ctxACMPCACertificateArnParameterName missing Throws Error", () => { 261 | //GIVEN 262 | const app = new cdk.App({context: { 263 | "ctxJnlpPort": 50000, 264 | "ctxHostedZoneName": "example.com", 265 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 266 | "ctxJenkinsControllerName": "jenkins-controller", 267 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 268 | "ctxJenkinsAgentName": "jenkins-agent", 269 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 270 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 271 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 272 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 273 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 274 | "ctxACMCertMode": "self-signed" 275 | }}); 276 | 277 | // WHEN, THEN 278 | expect(() => { 279 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 280 | }).toThrowError("ctxACMPCACertificateArnParameterName cannot be null"); 281 | 282 | }); 283 | 284 | test("CDK Util Context Value ctxACMSelfSignedCertificateArnParameterName missing Throws Error", () => { 285 | //GIVEN 286 | const app = new cdk.App({context: { 287 | "ctxJnlpPort": 50000, 288 | "ctxHostedZoneName": "example.com", 289 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 290 | "ctxJenkinsControllerName": "jenkins-controller", 291 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 292 | "ctxJenkinsAgentName": "jenkins-agent", 293 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 294 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 295 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 296 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 297 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA", 298 | "ctxACMCertMode": "self-signed" 299 | }}); 300 | 301 | // WHEN, THEN 302 | expect(() => { 303 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 304 | }).toThrowError("ctxACMSelfSignedCertificateArnParameterName cannot be null"); 305 | 306 | }); 307 | 308 | test("CDK Util Context Value ctxJenkinsPrivateRootCAParameterName missing Throws Error", () => { 309 | //GIVEN 310 | const app = new cdk.App({context: { 311 | "ctxJnlpPort": 50000, 312 | "ctxHostedZoneName": "example.com", 313 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 314 | "ctxJenkinsControllerName": "jenkins-controller", 315 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 316 | "ctxJenkinsAgentName": "jenkins-agent", 317 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 318 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 319 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 320 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 321 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 322 | "ctxACMCertMode": "self-signed" 323 | }}); 324 | 325 | // WHEN, THEN 326 | expect(() => { 327 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 328 | }).toThrowError("ctxJenkinsPrivateRootCAParameterName cannot be null"); 329 | 330 | }); 331 | 332 | test("CDK Util Context Value ctxACMCertMode missing Throws Error", () => { 333 | //GIVEN 334 | const app = new cdk.App({context: { 335 | "ctxJnlpPort": 50000, 336 | "ctxHostedZoneName": "example.com", 337 | "ctxJenkinsDomainNamePrefix": "jenkins-test", 338 | "ctxJenkinsControllerName": "jenkins-controller", 339 | "ctxJenkinsControllerImageTagParameterName": "/dev/jenkins/controller/docker/image/tag", 340 | "ctxJenkinsAgentName": "jenkins-agent", 341 | "ctxJenkinsAgentImageTagParameterName": "/dev/jenkins/agent/docker/image/tag", 342 | "ctxJenkinsAdminCredentialSecretName": "/dev/jenkins/admin/credentials", 343 | "ctxDevTeam1Workload1AWSAccountIdParameterName": "/dev/team1/workload1/AWSAccountID", 344 | "ctxACMPCACertificateArnParameterName": "/dev/jenkins/acmpca/certificateAuthorityArn", 345 | "ctxACMSelfSignedCertificateArnParameterName": "/dev/jenkins/acm/selfSignedCertificateArn", 346 | "ctxJenkinsPrivateRootCAParameterName": "/dev/jenkins/rootCA" 347 | }}); 348 | 349 | // WHEN, THEN 350 | expect(() => { 351 | new CdkPipelineStack.CdkPipelineStack(app, 'CDKPipelineStack') 352 | }).toThrowError("ctxACMCertMode cannot be null"); 353 | 354 | }); -------------------------------------------------------------------------------- /cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /jenkins/README.md: -------------------------------------------------------------------------------- 1 | # Jenkins Application Controller and Agent Configurations 2 | 3 | ## Project Details 4 | - **agent** folder includes the Dockerfile for the custom agent image. 5 | - **controller** folder includes the Dockerfile for the custom controller image including the configuration files. 6 | - jenkins.yaml - Configuration file for ECS Cloud and environment variables provided during runtime from the CDK Infrastructure stack. 7 | - plugins.txt - Provides required plugins along with appropriate versions. 8 | - custom.groovy - Init Groovy script for setting the defaults. 9 | 10 | ## Reference 11 | * https://github.com/jenkinsci/docker 12 | * https://hub.docker.com/_/jenkins 13 | 14 | ## Plugin Reference 15 | * https://plugins.jenkins.io/configuration-as-code/ 16 | * https://plugins.jenkins.io/amazon-ecs/ 17 | 18 | ## Sample Job Configurations 19 | - **ecs-fargate-validate-configuration** - Job to validate the deployment configuration for controller and agent. 20 | - **ecs-fargate-validate-devops-account-create-s3-bucket** - Job configured to checkout project from GitHub SCM and deploy CloudFormation template to create an S3 bucket in DevOps account using configuration from Jenkinsfile and environment variables. 21 | - **ecs-fargate-validate-workload-account-deploy-ec2-webserver** - Job configured to checkout project from GitHub and deploy CloudFormation template to create an EC2 instance in Workload account using configuration from Jenkinsfile and environment variables. Please note that the template provided is only to demonstrate EC2 deployment through Jenkins. Please follow infrastructure best practices to deploy applications to AWS. 22 | 23 | ## Useful commands 24 | * Assume Role: `aws sts assume-role --role-arn arn:aws:iam::111111111111:role/jenkins-deployment-role --role-session-name jenkins-deployment-role` 25 | * ECS Execute Command: `aws ecs execute-command --cluster --task --interactive --command "/bin/sh"` 26 | 27 | ## Known Issues 28 | * Amazon ECS v1.46 issues with launching agent container: https://github.com/jenkinsci/amazon-ecs-plugin/issues 29 | * Vulnerabilities identitifed on ECR Scan on Jenkins controller and agent images. -------------------------------------------------------------------------------- /jenkins/agent/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PRIVATE_ROOT_CA 2 | FROM jenkins/inbound-agent:latest-jdk11 3 | 4 | USER root 5 | RUN apt-get update && apt-get install curl unzip jq -y 6 | RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 7 | RUN unzip awscliv2.zip 8 | RUN ./aws/install 9 | ARG PRIVATE_ROOT_CA 10 | RUN echo $PRIVATE_ROOT_CA | base64 --decode >> /tmp/private-root-ca.pem 11 | RUN keytool -importcert -noprompt -trustcacerts -alias jenkins-cert -file /tmp/private-root-ca.pem -cacerts -storepass changeit 12 | RUN rm -rf awscliv2.zip && rm -rf /tmp/* /var/tmp/* 13 | 14 | USER jenkins -------------------------------------------------------------------------------- /jenkins/controller/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PRIVATE_ROOT_CA 2 | FROM jenkins/jenkins:lts-jdk11 3 | 4 | USER root 5 | RUN apt-get update && apt-get install -y 6 | COPY --chown=jenkins:jenkins config/jenkins.yaml /usr/share/jenkins/ref/jenkins.yaml 7 | COPY --chown=jenkins:jenkins config/custom.groovy /usr/share/jenkins/ref/init.groovy.d/custom.groovy 8 | COPY --chown=jenkins:jenkins config/plugins.txt /usr/share/jenkins/ref/plugins.txt 9 | RUN echo 2.0 > /usr/share/jenkins/ref/jenkins.install.UpgradeWizard.state 10 | ARG PRIVATE_ROOT_CA 11 | RUN echo $PRIVATE_ROOT_CA | base64 --decode >> /tmp/private-root-ca.pem 12 | RUN keytool -importcert -noprompt -trustcacerts -alias jenkins-cert -file /tmp/private-root-ca.pem -cacerts -storepass changeit 13 | RUN rm -rf /tmp/* /var/tmp/* 14 | 15 | USER jenkins 16 | RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt -------------------------------------------------------------------------------- /jenkins/controller/config/custom.groovy: -------------------------------------------------------------------------------- 1 | import jenkins.model.* 2 | import jenkins.install.InstallState 3 | Jenkins.instance.setNumExecutors(0) //Recommended to not run builds on the built-in-node 4 | 5 | url = System.env.JENKINS_URL // Setting the Jenkins Base URL 6 | urlConfig = JenkinsLocationConfiguration.get() 7 | urlConfig.setUrl(url) 8 | urlConfig.save() -------------------------------------------------------------------------------- /jenkins/controller/config/jenkins.yaml: -------------------------------------------------------------------------------- 1 | jenkins: 2 | numExecutors: 0 3 | slaveAgentPort: 50000 4 | systemMessage: Jenkins Serverless on AWS ECS Fargate 5 | globalNodeProperties: 6 | - envVars: 7 | env: 8 | - key: DEFAULT_ACCOUNT 9 | value: ${DEFAULT_ACCOUNT} 10 | - key: DEFAULT_ACCOUNT_JENKINS_ROLE 11 | value: ${DEFAULT_ACCOUNT_JENKINS_ROLE} 12 | - key: TEAM1_APP1_DEV_WORKLOAD_ACCOUNT 13 | value: ${TEAM1_APP1_DEV_WORKLOAD_ACCOUNT} 14 | - key: TEAM1_APP1_DEV_WORKLOAD_JENKINS_ROLE 15 | value: ${TEAM1_APP1_DEV_WORKLOAD_JENKINS_ROLE} 16 | agentProtocols: 17 | - JNLP4-connect 18 | authorizationStrategy: 19 | loggedInUsersCanDoAnything: 20 | allowAnonymousRead: false 21 | securityRealm: 22 | local: 23 | allowsSignup: false 24 | users: 25 | - id: ${JENKINS_USERNAME} 26 | password: ${JENKINS_PASSWORD} 27 | clouds: 28 | - ecs: 29 | credentialsId: false 30 | cluster: ${ECS_CLUSTER} 31 | name: ecs-cloud 32 | regionName: ${AWS_REGION} 33 | jenkinsUrl: ${JENKINS_URL} 34 | tunnel: ${JENKINS_CONTROLLER_PRIVATE_TUNNEL_URL} 35 | templates: 36 | - label: fargate 37 | templateName: jenkins-agent 38 | assignPublicIp: true 39 | image: ${AGENT_ECR_IMAGE_URL} 40 | launchType: FARGATE 41 | networkMode: awsvpc 42 | cpu: 1024 43 | memoryReservation: 2048 44 | subnets: ${PRIVATE_SUBNET_IDS} 45 | securityGroups: ${AGENT_SECURITY_GROUP_ID} 46 | taskRole: ${AGENT_TASK_ROLE_ARN} 47 | executionRole: ${AGENT_EXECUTION_ROLE_ARN} 48 | logDriver: awslogs 49 | logDriverOptions: 50 | - name: awslogs-region 51 | value: ${AWS_REGION} 52 | - name: awslogs-group 53 | value: ${LOG_GROUP} 54 | - name: awslogs-stream-prefix 55 | value: ${LOG_STREAM_PREFIX} 56 | jobs: 57 | - script: > 58 | folder('sample-jobs') 59 | - script: > 60 | pipelineJob('sample-jobs/ecs-fargate-validate-configuration') { 61 | definition { 62 | cps { 63 | script("""\ 64 | pipeline { 65 | agent { 66 | label 'fargate' 67 | } 68 | stages { 69 | stage ('validate') { 70 | steps { 71 | sh "aws --version" 72 | } 73 | } 74 | stage('build') { 75 | steps { 76 | echo 'Hello from Jenkins agent running on ECS Fargate!' 77 | } 78 | } 79 | } 80 | }""".stripIndent()) 81 | sandbox() 82 | } 83 | } 84 | } 85 | - script: > 86 | pipelineJob('sample-jobs/ecs-fargate-validate-devops-account-create-s3-bucket') { 87 | definition { 88 | cpsScm { 89 | scm { 90 | git { 91 | remote { 92 | url('https://github.com/aws-samples/aws-jenkins-ecs-cdk.git') 93 | } 94 | branch('*/main') 95 | } 96 | } 97 | scriptPath('sample/pipeline-s3/Jenkinsfile') 98 | lightweight() 99 | } 100 | } 101 | } 102 | - script: > 103 | pipelineJob('sample-jobs/ecs-fargate-validate-workload-account-deploy-ec2-webserver') { 104 | definition { 105 | cpsScm { 106 | scm { 107 | git { 108 | remote { 109 | url('https://github.com/aws-samples/aws-jenkins-ecs-cdk.git') 110 | } 111 | branch('*/main') 112 | } 113 | } 114 | scriptPath('sample/pipeline-ec2/Jenkinsfile') 115 | lightweight() 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /jenkins/controller/config/plugins.txt: -------------------------------------------------------------------------------- 1 | configuration-as-code:1647.ve39ca_b_829b_42 2 | workflow-aggregator:590.v6a_d052e5a_a_b_5 3 | amazon-ecs:1.41 4 | pipeline-aws:1.43 5 | git:4.14.1 6 | github:1.36.0 7 | job-dsl:1.81 8 | 9 | -------------------------------------------------------------------------------- /sample/pipeline-ec2/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | label 'fargate' 4 | } 5 | 6 | stages { 7 | stage('deploy webserver') { 8 | steps { 9 | withAWS(roleAccount:"${TEAM1_APP1_DEV_WORKLOAD_ACCOUNT}", role:"${TEAM1_APP1_DEV_WORKLOAD_JENKINS_ROLE}") { 10 | script{ 11 | vpcId = sh (script: "aws ec2 describe-vpcs --filters Name=isDefault,Values=true --query \"Vpcs[].VpcId\" --region us-east-1 | jq --raw-output '.[]'",returnStdout: true).trim() 12 | echo "Deploying to default VPC Id: ${vpcId}" 13 | sh "aws cloudformation deploy --template-file sample/pipeline-ec2/cfn-webserver.yaml --stack-name sample-webserver-stack --parameter-overrides DefaultVPCId=${vpcId}" 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample/pipeline-ec2/cfn-webserver.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 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 this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # 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 IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: 2010-09-09 17 | Description: Sample CloudFormation Template to create webserver instance from Jenkins Job. 18 | 19 | Parameters: 20 | LatestAmiId: 21 | Type: "AWS::SSM::Parameter::Value" 22 | Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 23 | AllowedValues: 24 | - /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 25 | InstanceType: 26 | Description: Please select an EC2 Instance Type 27 | Type: String 28 | Default: t2.micro 29 | AllowedValues: 30 | - t2.micro 31 | DefaultVPCId: 32 | Description: Default VPC ID 33 | Type: AWS::EC2::VPC::Id 34 | 35 | Resources: 36 | WebServerSecurityGroup: 37 | Type: 'AWS::EC2::SecurityGroup' 38 | Properties: 39 | GroupDescription: Enable HTTP access via port 80 40 | VpcId: !Ref DefaultVPCId 41 | SecurityGroupEgress: 42 | - CidrIp: 0.0.0.0/0 43 | Description: Allow all outbound traffic on port 80 from Instance 44 | FromPort: 80 45 | IpProtocol: tcp 46 | ToPort: 80 47 | - CidrIp: 0.0.0.0/0 48 | Description: Allow all outbound traffic on port 443 from Instance 49 | FromPort: 443 50 | IpProtocol: tcp 51 | ToPort: 443 52 | Metadata: 53 | cfn_nag: 54 | rules_to_suppress: 55 | - id: W2 56 | reason: "Egress on port 80 and 443 to install packages on the instance." 57 | - id: W5 58 | reason: "Egress on port 80 and 443 to install packages on the instance." 59 | - id: W9 60 | reason: "Ingress on port 80 from /0." 61 | 62 | SecurityGroupIngressPort80: 63 | Type: AWS::EC2::SecurityGroupIngress 64 | Properties: 65 | Description: Allow all inbound traffic on port 80 66 | IpProtocol: tcp 67 | FromPort: 80 68 | ToPort: 80 69 | GroupId: !Ref WebServerSecurityGroup 70 | CidrIp: 0.0.0.0/0 71 | 72 | WebServerInstanceRole: 73 | Type: 'AWS::IAM::Role' 74 | Properties: 75 | AssumeRolePolicyDocument: 76 | Version: '2012-10-17' 77 | Statement: 78 | - Effect: Allow 79 | Principal: 80 | Service: 81 | - ec2.amazonaws.com 82 | Action: 83 | - 'sts:AssumeRole' 84 | Path: / 85 | ManagedPolicyArns: 86 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 87 | 88 | WebServerInstanceProfile: 89 | Type: "AWS::IAM::InstanceProfile" 90 | Properties: 91 | Path: "/" 92 | Roles: 93 | - !Ref WebServerInstanceRole 94 | 95 | WebServer: 96 | Type: AWS::EC2::Instance 97 | Properties: 98 | ImageId: !Ref LatestAmiId 99 | InstanceType: !Ref InstanceType 100 | IamInstanceProfile: !Ref WebServerInstanceProfile 101 | SecurityGroupIds: 102 | - !Ref WebServerSecurityGroup 103 | UserData: 104 | Fn::Base64: |- 105 | #!/bin/bash 106 | sudo su 107 | yum update -y 108 | yum install httpd -y 109 | service httpd start 110 | chkconfig httpd on 111 | echo "

Deployed from Jenkins in DevOps Account.


Web server is running at: $(curl http://169.254.169.254/latest/meta-data/public-hostname)

" > /var/www/html/index.html 112 | 113 | Outputs: 114 | WebServerIP: 115 | Description: The IP of the Web Server Instance 116 | Value: !GetAtt WebServer.PublicIp -------------------------------------------------------------------------------- /sample/pipeline-s3/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | label 'fargate' 4 | } 5 | 6 | stages { 7 | stage('create-s3-bucket') { 8 | steps { 9 | withAWS(roleAccount:"${DEFAULT_ACCOUNT}", role:"${DEFAULT_ACCOUNT_JENKINS_ROLE}") { 10 | sh "aws cloudformation deploy --template-file sample/pipeline-s3/cfn-s3.yaml --stack-name sample-s3-stack --parameter-overrides BucketName=jenkins-zzz-demox-${BUILD_NUMBER}" 11 | } 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sample/pipeline-s3/cfn-s3.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 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 this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # 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 IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: 2010-09-09 17 | Description: Sample CloudFormation Template for creating S3 bucket from Jenkins Job. 18 | 19 | Parameters: 20 | BucketName: 21 | Description: Please provide bucket name. 22 | Type: String 23 | 24 | Resources: 25 | S3Bucket: 26 | Type: 'AWS::S3::Bucket' 27 | DeletionPolicy: Retain 28 | Properties: 29 | BucketName: !Ref BucketName 30 | BucketEncryption: 31 | ServerSideEncryptionConfiguration: 32 | - ServerSideEncryptionByDefault: 33 | SSEAlgorithm: AES256 34 | PublicAccessBlockConfiguration: 35 | BlockPublicAcls: true 36 | BlockPublicPolicy: true 37 | IgnorePublicAcls: true 38 | RestrictPublicBuckets: true 39 | Metadata: 40 | cfn_nag: 41 | rules_to_suppress: 42 | - id: W35 43 | reason: "Access logging disabled for the bucket." 44 | 45 | S3SourceBucketPolicy: 46 | Type: 'AWS::S3::BucketPolicy' 47 | Properties: 48 | Bucket: !Ref S3Bucket 49 | PolicyDocument: 50 | Statement: 51 | - Sid: AllowSSLRequestsOnly 52 | Action: 53 | - "s3:*" 54 | Effect: Deny 55 | Resource: 56 | - !Sub "arn:aws:s3:::${S3Bucket}" 57 | - !Sub "arn:aws:s3:::${S3Bucket}/*" 58 | Condition: 59 | Bool: 60 | "aws:SecureTransport": "false" 61 | Principal: 62 | AWS: "*" 63 | 64 | Outputs: 65 | BucketName: 66 | Value: !Ref S3Bucket 67 | Description: Name of the sample Amazon S3 bucket. --------------------------------------------------------------------------------