├── lib ├── construct │ ├── jenkins │ │ ├── resources │ │ │ ├── .gitignore │ │ │ ├── .dockerignore │ │ │ ├── config │ │ │ │ ├── plugins.txt │ │ │ │ ├── agentTestJob.xml │ │ │ │ ├── initialConfig.groovy │ │ │ │ └── jenkins.yaml.ejs │ │ │ ├── controller.Dockerfile │ │ │ ├── agent-userdata.sh │ │ │ └── agent-userdata-windows.yml │ │ ├── agent-mac.ts │ │ ├── controller.ts │ │ └── agent-ec2-fleet.ts │ ├── resources │ │ └── unity-accelerator-init-config.yaml │ └── unity-accelerator.ts └── jenkins-unity-build-stack.ts ├── .npmignore ├── .gitignore ├── .prettierrc ├── docs ├── imgs │ ├── mac-jobs.png │ ├── mac-quota.png │ ├── websocket.png │ ├── accelerator.png │ ├── jenkins_gui.png │ ├── mac-select-ami.png │ ├── Reference-Architecture.png │ ├── ebs-pool.svg │ └── ami-workflow.svg ├── using-ami-for-caching.md ├── deployment_ja.md └── setup-mac-instance.md ├── jest.config.js ├── CODE_OF_CONDUCT.md ├── .github └── workflows │ ├── build.yml │ └── update_snapshot.yml ├── tsconfig.json ├── package.json ├── LICENSE ├── cdk.json ├── bin └── jenkins-unity-build.ts ├── test └── jenkins-unity-build.test.ts ├── CONTRIBUTING.md └── README.md /lib/construct/jenkins/resources/.gitignore: -------------------------------------------------------------------------------- 1 | config/jenkins*.yaml 2 | -------------------------------------------------------------------------------- /lib/construct/jenkins/resources/.dockerignore: -------------------------------------------------------------------------------- 1 | agent-userdata.sh 2 | agent-userdata-windows.yml 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /docs/imgs/mac-jobs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-developing-apple-ios-and-vision-pro-applications-with-unity-on-amazon-ec2/HEAD/docs/imgs/mac-jobs.png -------------------------------------------------------------------------------- /docs/imgs/mac-quota.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-developing-apple-ios-and-vision-pro-applications-with-unity-on-amazon-ec2/HEAD/docs/imgs/mac-quota.png -------------------------------------------------------------------------------- /docs/imgs/websocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-developing-apple-ios-and-vision-pro-applications-with-unity-on-amazon-ec2/HEAD/docs/imgs/websocket.png -------------------------------------------------------------------------------- /docs/imgs/accelerator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-developing-apple-ios-and-vision-pro-applications-with-unity-on-amazon-ec2/HEAD/docs/imgs/accelerator.png -------------------------------------------------------------------------------- /docs/imgs/jenkins_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-developing-apple-ios-and-vision-pro-applications-with-unity-on-amazon-ec2/HEAD/docs/imgs/jenkins_gui.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/imgs/mac-select-ami.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-developing-apple-ios-and-vision-pro-applications-with-unity-on-amazon-ec2/HEAD/docs/imgs/mac-select-ami.png -------------------------------------------------------------------------------- /docs/imgs/Reference-Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions-library-samples/guidance-for-developing-apple-ios-and-vision-pro-applications-with-unity-on-amazon-ec2/HEAD/docs/imgs/Reference-Architecture.png -------------------------------------------------------------------------------- /lib/construct/jenkins/resources/config/plugins.txt: -------------------------------------------------------------------------------- 1 | configuration-as-code:latest 2 | workflow-aggregator:latest 3 | git:latest 4 | docker-plugin:latest 5 | docker-workflow:latest 6 | timestamper:latest 7 | ec2-fleet:latest 8 | artifact-manager-s3:latest 9 | jobcacher:latest 10 | amazon-ecr:latest 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, workflow_dispatch] 3 | jobs: 4 | Build-and-Test-CDK: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - name: Use Node.js 9 | uses: actions/setup-node@v3 10 | with: 11 | node-version: "16.x" 12 | - run: | 13 | npm ci 14 | npm run build 15 | npm run test 16 | -------------------------------------------------------------------------------- /.github/workflows/update_snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Update snapshot 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | update: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Use Node.js 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: "16.x" 15 | - run: npm ci 16 | - run: npm run test -- -u 17 | - name: Add & Commit 18 | uses: EndBug/add-and-commit@v9 19 | with: 20 | add: "test/__snapshots__/." 21 | message: "update snapshot" 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020" 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aws-samples/jenkins-unity-build", 3 | "version": "0.1.0", 4 | "private": true, 5 | "bin": { 6 | "jenkins-unity-build": "bin/jenkins-unity-build.js" 7 | }, 8 | "scripts": { 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "test": "jest", 12 | "cdk": "cdk" 13 | }, 14 | "devDependencies": { 15 | "@types/ejs": "^3.1.1", 16 | "@types/jest": "^29.2.5", 17 | "@types/node": "18.11.18", 18 | "aws-cdk": "^2.98.0", 19 | "jest": "^29.3.1", 20 | "ts-jest": "^29.0.3", 21 | "ts-node": "^10.9.1", 22 | "typescript": "~4.9.4" 23 | }, 24 | "dependencies": { 25 | "aws-cdk-lib": "^2.98.0", 26 | "constructs": "^10.0.0", 27 | "ejs": "^3.1.8", 28 | "source-map-support": "^0.5.21", 29 | "upsert-slr": "^1.0.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/construct/jenkins/resources/controller.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/x86_64 jenkins/jenkins:lts-jdk17 2 | ARG CONFIG_FILE_NAME='jenkins.TestStack.yaml' 3 | 4 | # https://github.com/jenkinsci/docker 5 | 6 | # https://github.com/jenkinsci/plugin-installation-manager-tool 7 | COPY config/plugins.txt /usr/share/jenkins/ref/plugins.txt 8 | RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt 9 | 10 | # post initilization script https://www.jenkins.io/doc/book/managing/groovy-hook-scripts/ 11 | COPY config/initialConfig.groovy /usr/share/jenkins/ref/init.groovy.d/InitialConfig.groovy.override 12 | 13 | # Configuration as code https://plugins.jenkins.io/configuration-as-code/ 14 | COPY config/$CONFIG_FILE_NAME /usr/share/jenkins/ref/jenkins.yaml.override 15 | 16 | # Sample Jobs 17 | COPY config/agentTestJob.xml /usr/share/jenkins/ref/jobs/agent-test/config.xml.override 18 | 19 | ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/construct/resources/unity-accelerator-init-config.yaml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | # We use cloud-init config to allow run the commands every time the instance reboots. 3 | # https://cloudinit.readthedocs.io/en/latest/reference/examples.html 4 | 5 | # Execute on every reboot https://dev.classmethod.jp/articles/execute_userdata_on_restart/ 6 | cloud_config_modules: 7 | - [runcmd, always] 8 | cloud_final_modules: 9 | - [scripts-user, always] 10 | 11 | # runcmd creates a sh file and pass it to script-user module (only on the first boot by default). 12 | # You need to also execute runcmd module on every reboot to reflect changes. 13 | # https://cloudinit.readthedocs.io/en/latest/reference/modules.html#runcmd 14 | runcmd: 15 | - | 16 | #!/bin/bash 17 | echo Initializing... $(date) 18 | 19 | # Install docker and other dependencies 20 | yum update -y 21 | yum install -y git docker 22 | systemctl enable docker 23 | systemctl start docker 24 | usermod -aG docker ec2-user 25 | chmod 777 /var/run/docker.sock 26 | 27 | # Start Unity Accelerator using Docker https://docs.unity3d.com/Manual/UnityAccelerator.html#docker 28 | # Sometimes (e.g. reboot) the docker container is already running. We remove any existing container here. 29 | docker rm -f accelerator 30 | 31 | # You can change login credential for Accelerator Web UI here. (PASSWORD, USER) 32 | docker run --name accelerator -p 80:80 -p 10080:10080 --env PASSWORD=passw0rd --env USER=admin -v "/home/ec2-user/agent:/agent" -d --restart unless-stopped unitytechnologies/accelerator:latest 33 | 34 | # They are not necessary but useful for debugging 35 | yum install -y tmux htop 36 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/jenkins-unity-build.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-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/construct/jenkins/resources/config/agentTestJob.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | 8 | 65 | true 66 | 67 | 68 | -------------------------------------------------------------------------------- /lib/construct/jenkins/resources/config/initialConfig.groovy: -------------------------------------------------------------------------------- 1 | import jenkins.install.InstallState 2 | import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey.DirectEntryPrivateKeySource 3 | import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey 4 | import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl 5 | import com.cloudbees.plugins.credentials.* 6 | import com.cloudbees.plugins.credentials.domains.Domain 7 | import hudson.model.* 8 | import jenkins.model.Jenkins 9 | import jenkins.model.JenkinsLocationConfiguration 10 | 11 | // You can change Jenkins timezone here 12 | // System.setProperty('org.apache.commons.jelly.tags.fmt.timeZone', 'Asia/Tokyo') 13 | 14 | def key = System.env.PRIVATE_KEY 15 | 16 | // https://plugins.jenkins.io/ec2-fleet/#plugin-content-groovy 17 | BasicSSHUserPrivateKey unixSshCredentials = new BasicSSHUserPrivateKey( 18 | CredentialsScope.GLOBAL, 19 | 'instance-ssh-key-unix', 20 | 'ec2-user', 21 | new DirectEntryPrivateKeySource(key), 22 | '', 23 | 'SSH private key for UNIX agents' 24 | ) 25 | 26 | BasicSSHUserPrivateKey windowsSshCredentials = new BasicSSHUserPrivateKey( 27 | CredentialsScope.GLOBAL, 28 | 'instance-ssh-key-windows', 29 | 'Administrator', 30 | new DirectEntryPrivateKeySource(key), 31 | '', 32 | 'SSH private key for Windows agents' 33 | ) 34 | 35 | // https://github.com/jenkinsci/aws-credentials-plugin/blob/master/src/main/java/com/cloudbees/jenkins/plugins/awscredentials/AWSCredentialsImpl.java 36 | AWSCredentialsImpl awsCredential = new AWSCredentialsImpl( 37 | CredentialsScope.GLOBAL, 38 | 'ecr-role', 39 | '', 40 | '', 41 | 'IAM role arn used for Amazon ECR plugin', 42 | System.env.ECR_ROLE_ARN, 43 | '', 44 | '' 45 | ) 46 | 47 | // get Jenkins instance 48 | Jenkins jenkins = Jenkins.get() 49 | // get credentials domain 50 | def domain = Domain.global() 51 | // get credentials store 52 | def store = jenkins.getExtensionList('com.cloudbees.plugins.credentials.SystemCredentialsProvider')[0].getStore() 53 | // add credential to store 54 | store.addCredentials(domain, unixSshCredentials) 55 | store.addCredentials(domain, windowsSshCredentials) 56 | store.addCredentials(domain, awsCredential) 57 | // save current Jenkins state to disk 58 | jenkins.save() 59 | 60 | Jenkins.instance.setInstallState(InstallState.INITIAL_SETUP_COMPLETED) 61 | -------------------------------------------------------------------------------- /bin/jenkins-unity-build.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { JenkinsUnityBuildStack } from '../lib/jenkins-unity-build-stack'; 5 | import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2'; 6 | import { Size } from 'aws-cdk-lib'; 7 | 8 | const app = new cdk.App(); 9 | new JenkinsUnityBuildStack(app, 'JenkinsUnityBuildStack', { 10 | env: { 11 | // AWS region to deploy this stack to. (Required for defining ALB access logging) 12 | region: 'us-east-2', 13 | // Aws Account ID to deploy this stack to. (Also required if you specify certificateArn or vpcId below.) 14 | // account: '123456789012', 15 | }, 16 | allowedCidrs: ['127.0.0.1/32'], 17 | 18 | // Amazon Certificate Manager certificate ARN for Jenkins Web UI ALB. 19 | // ALB can be accessed with HTTP if you don't specify this property. 20 | // certificateArn: "", 21 | 22 | // You can use an existing VPC by specifying vpcId. 23 | // vpcId: 'vpc-xxxxxxx', 24 | 25 | ec2FleetConfigurations: [ 26 | { 27 | type: 'LinuxFleet', 28 | name: 'linux-small', 29 | label: 'small', 30 | numExecutors: 5, 31 | }, 32 | { 33 | type: 'LinuxFleet', 34 | rootVolumeSize: Size.gibibytes(30), 35 | dataVolumeSize: Size.gibibytes(100), 36 | instanceTypes: [ 37 | InstanceType.of(InstanceClass.C5, InstanceSize.XLARGE), 38 | InstanceType.of(InstanceClass.C5A, InstanceSize.XLARGE), 39 | InstanceType.of(InstanceClass.C5N, InstanceSize.XLARGE), 40 | InstanceType.of(InstanceClass.C4, InstanceSize.XLARGE), 41 | ], 42 | name: 'linux-fleet', 43 | label: 'linux', 44 | fleetMinSize: 1, 45 | fleetMaxSize: 4, 46 | }, 47 | // You can add Windows fleet as well. 48 | // { 49 | // type: 'WindowsFleet', 50 | // rootVolumeSize: Size.gibibytes(50), 51 | // dataVolumeSize: Size.gibibytes(100), 52 | // instanceTypes: [ 53 | // InstanceType.of(InstanceClass.M6A, InstanceSize.XLARGE), 54 | // InstanceType.of(InstanceClass.M5A, InstanceSize.XLARGE), 55 | // InstanceType.of(InstanceClass.M5N, InstanceSize.XLARGE), 56 | // InstanceType.of(InstanceClass.M5, InstanceSize.XLARGE), 57 | // ], 58 | // name: 'windows-fleet', 59 | // label: 'windows', 60 | // fleetMinSize: 1, 61 | // fleetMaxSize: 4, 62 | // }, 63 | ], 64 | 65 | // You can add any number of Mac agents. 66 | // macInstancesCOnfigurations: [ 67 | // { 68 | // storageSize: cdk.Size.gibibytes(200), 69 | // instanceType: InstanceType.of(InstanceClass.MAC2, InstanceSize.METAL), 70 | // amiId: 'ami-038e1d574f3140013', 71 | // name: 'mac0', 72 | // subnet: (vpc) => vpc.privateSubnets[1], 73 | // }, 74 | // ], 75 | 76 | // You can deploy Unity Accelerator. 77 | // unityAccelerator: { 78 | // volumeSize: Size.gibibytes(100), 79 | // } 80 | 81 | // base url for your Unity license sever. 82 | // You can setup one using this project: https://github.com/aws-samples/unity-build-server-with-aws-cdk 83 | // licenseServerBaseUrl: 'http://10.0.0.100:8080', 84 | }); 85 | -------------------------------------------------------------------------------- /lib/construct/unity-accelerator.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 3 | import { ManagedPolicy } from 'aws-cdk-lib/aws-iam'; 4 | import * as servicediscovery from 'aws-cdk-lib/aws-servicediscovery'; 5 | import { CfnOutput } from 'aws-cdk-lib'; 6 | import { readFileSync } from 'fs'; 7 | import { join } from 'path'; 8 | 9 | export interface UnityAcceleratorProps { 10 | readonly vpc: ec2.IVpc; 11 | readonly namespace: servicediscovery.PrivateDnsNamespace; 12 | readonly storageSizeGb: number; 13 | readonly instanceType?: ec2.InstanceType; 14 | readonly subnet?: ec2.ISubnet; 15 | } 16 | 17 | export class UnityAccelerator extends Construct { 18 | public readonly endpoint: string; 19 | 20 | constructor(scope: Construct, id: string, props: UnityAcceleratorProps) { 21 | super(scope, id); 22 | 23 | const { 24 | vpc, 25 | namespace, 26 | subnet, 27 | instanceType = ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.LARGE), 28 | } = props; 29 | // The port Accelerator uses: https://docs.unity3d.com/Manual/UnityAccelerator.html#docker 30 | const servicePort = 10080; 31 | 32 | // Allow access via a domain name instead of an IP address. accelerator.build 33 | const service = namespace.createService('Service', { 34 | name: 'accelerator', 35 | }); 36 | 37 | let script = readFileSync(join(__dirname, 'resources', 'unity-accelerator-init-config.yaml'), 'utf8'); 38 | // Remove all the comments (begins with a # ) since CFn does not support letters other than ASCII. 39 | script = script 40 | .split('\n') 41 | .map((line) => line.replace(/#\s.*/g, '')) 42 | .join('\n'); 43 | const userData = ec2.UserData.custom(script); 44 | 45 | const instance = new ec2.Instance(this, 'Default', { 46 | vpc, 47 | instanceType, 48 | machineImage: ec2.MachineImage.latestAmazonLinux2023({ 49 | userData, 50 | // Uncomment this line to avoid from accidental replacement of the instance. 51 | // You need to set account and region of the stack explicitly for this to work. 52 | // cachedInContext: true, 53 | }), 54 | ssmSessionPermissions: true, 55 | blockDevices: [ 56 | { 57 | deviceName: '/dev/xvda', 58 | volume: ec2.BlockDeviceVolume.ebs(props.storageSizeGb, { 59 | volumeType: ec2.EbsDeviceVolumeType.GP3, 60 | encrypted: true, 61 | }), 62 | }, 63 | ], 64 | vpcSubnets: subnet == null ? undefined : { subnets: [subnet] }, 65 | }); 66 | // You can enable termination protection by uncommenting this line. 67 | // (instance.node.defaultChild as ec2.CfnInstance).disableApiTermination = true; 68 | 69 | instance.connections.allowFrom(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(servicePort)); 70 | 71 | service.registerIpInstance('Instance', { 72 | ipv4: instance.instancePrivateIp, 73 | }); 74 | 75 | this.endpoint = `${service.serviceName}.${namespace.namespaceName}:${servicePort}`; 76 | 77 | new CfnOutput(this, 'Endpoint', { value: this.endpoint }); 78 | new CfnOutput(this, 'InstanceId', { value: instance.instanceId }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/jenkins-unity-build.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import { JenkinsUnityBuildStack } from '../lib/jenkins-unity-build-stack'; 4 | import { readdirSync, rmSync } from 'fs'; 5 | import { join } from 'path'; 6 | import { Size } from 'aws-cdk-lib'; 7 | import { InstanceType, InstanceClass, InstanceSize } from 'aws-cdk-lib/aws-ec2'; 8 | 9 | test('Snapshot test', () => { 10 | // remove all the intermediate files that are created before the test 11 | const jenkinsResourceDir = join(__dirname, '..', 'lib', 'construct', 'jenkins', 'resources', 'config'); 12 | readdirSync(jenkinsResourceDir) 13 | .filter((path) => path.match(/^jenkins.+yaml$/) != null) 14 | .forEach((path) => rmSync(join(jenkinsResourceDir, path))); 15 | 16 | const app = new cdk.App(); 17 | const stack = new JenkinsUnityBuildStack(app, 'TestStack', { 18 | env: { 19 | region: 'us-east-2', 20 | description : "Guidance for Developing Apple Vision Pro Applications with Unity on Amazon EC2 (SO9527)" 21 | }, 22 | allowedCidrs: ['127.0.0.1/32'], 23 | licenseServerBaseUrl: 'http://10.0.0.100:8080', 24 | ec2FleetConfigurations: [ 25 | { 26 | type: 'LinuxFleet', 27 | rootVolumeSize: Size.gibibytes(30), 28 | dataVolumeSize: Size.gibibytes(100), 29 | instanceTypes: [ 30 | InstanceType.of(InstanceClass.C5, InstanceSize.XLARGE), 31 | InstanceType.of(InstanceClass.C5A, InstanceSize.XLARGE), 32 | InstanceType.of(InstanceClass.C5N, InstanceSize.XLARGE), 33 | InstanceType.of(InstanceClass.C4, InstanceSize.XLARGE), 34 | ], 35 | name: 'linux-fleet', 36 | label: 'linux', 37 | fleetMinSize: 1, 38 | fleetMaxSize: 4, 39 | }, 40 | { 41 | type: 'LinuxFleet', 42 | name: 'linux-fleet-small', 43 | label: 'small', 44 | fleetMinSize: 1, 45 | fleetMaxSize: 2, 46 | numExecutors: 5, 47 | instanceTypes: [InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM)], 48 | }, 49 | { 50 | type: 'WindowsFleet', 51 | rootVolumeSize: Size.gibibytes(50), 52 | dataVolumeSize: Size.gibibytes(100), 53 | // You may want to add several instance types to avoid from insufficient Spot capacity. 54 | instanceTypes: [ 55 | InstanceType.of(InstanceClass.M6A, InstanceSize.XLARGE), 56 | InstanceType.of(InstanceClass.M5A, InstanceSize.XLARGE), 57 | InstanceType.of(InstanceClass.M5N, InstanceSize.XLARGE), 58 | InstanceType.of(InstanceClass.M5, InstanceSize.XLARGE), 59 | ], 60 | name: 'windows-fleet', 61 | label: 'windows', 62 | fleetMinSize: 1, 63 | fleetMaxSize: 4, 64 | }, 65 | ], 66 | macInstancesCOnfigurations: [ 67 | { 68 | storageSize: Size.gibibytes(200), 69 | instanceType: InstanceType.of(InstanceClass.MAC1, InstanceSize.METAL), 70 | amiId: 'ami-013846afc111c94b0', 71 | name: 'mac0', 72 | }, 73 | ], 74 | unityAccelerator: { 75 | volumeSize: Size.gibibytes(300), 76 | subnet: (vpc) => vpc.privateSubnets[1], 77 | }, 78 | }); 79 | const template = Template.fromStack(stack); 80 | expect(template).toMatchSnapshot(); 81 | }); 82 | -------------------------------------------------------------------------------- /lib/construct/jenkins/resources/agent-userdata.sh: -------------------------------------------------------------------------------- 1 | yum update -y 2 | yum install -y git jq 3 | 4 | # allow to use /data even if no data volume is configured 5 | JENKINS_DIR="/data" 6 | mkdir $JENKINS_DIR 7 | chmod 777 $JENKINS_DIR 8 | 9 | # mount a data volume 10 | TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 600"` 11 | AZ=`curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .availabilityZone` 12 | INSTANCE_ID=`curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id` 13 | REGION=`curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/placement/region` 14 | # find a volume with the same kind tag as the instance 15 | # choose a random volume to use evenly across all volumes of the same kind 16 | VOLUME_ID=$(aws ec2 describe-volumes --filters Name=tag:Kind,Values= Name=availability-zone,Values=$AZ Name=status,Values=available --query 'Volumes[*].[VolumeId]' --output text --region $REGION | shuf | head -1) 17 | 18 | if [ "$VOLUME_ID" ];then 19 | echo "found volume $VOLUME_ID" 20 | DEVICE_NAME="/dev/xvdf" 21 | aws ec2 attach-volume --device $DEVICE_NAME --instance-id $INSTANCE_ID --volume-id $VOLUME_ID --region $REGION 22 | if [ $? -ne 0 ]; then 23 | # There is possibly a race condition between other instances (e.g. the volume is occupied by another instance). 24 | # Terminate the instance if attaching volume failed. ASG will retry booting an instance. 25 | shutdown now -h 26 | fi 27 | 28 | # we should do polling for the volume status instead, but it usually finishes in a few seconds... 29 | sleep 10 30 | 31 | # basically following this doc: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-using-volumes.html 32 | VNAME=$(readlink $DEVICE_NAME) 33 | RES=$(file -s /dev/$VNAME) 34 | 35 | if [[ "$RES" =~ .*": data" ]]; then 36 | # If the volume is not formatted yet 37 | mkfs -t xfs $DEVICE_NAME 38 | fi 39 | 40 | mount $DEVICE_NAME $JENKINS_DIR 41 | chmod 777 $JENKINS_DIR 42 | 43 | UUID=$(blkid | grep $VNAME | sed 's/.*UUID="\(\S*\)"\s.*/\1/') 44 | printf "\nUUID=${UUID} ${JENKINS_DIR} xfs defaults,nofail 0 2\n" >> /etc/fstab 45 | fi 46 | 47 | # Set linux parameters to avoid errors when trying to connect an instance via SSM during Unity build 48 | # https://stackoverflow.com/questions/69337154/aws-ec2-terminal-session-terminated-with-plugin-with-name-standard-stream-not-f 49 | echo "fs.file-max=100000" >> /etc/sysctl.conf 50 | echo "fs.inotify.max_user_watches=1048576" >> /etc/sysctl.conf 51 | echo "fs.inotify.max_user_instances=256" >> /etc/sysctl.conf 52 | 53 | # install docker 54 | yum install -y docker 55 | systemctl enable docker 56 | systemctl start docker 57 | usermod -aG docker ec2-user 58 | chmod 777 /var/run/docker.sock 59 | 60 | # install git lfs 61 | # install java after data volume is set up to avoid jenkins agent configured before /data is mounted 62 | # Set os/dist explicitly https://github.com/git-lfs/git-lfs/issues/5356 63 | curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.rpm.sh | os=fedora dist=36 bash 64 | yum install -y java-17-amazon-corretto-headless git-lfs 65 | 66 | # install tools for debug 67 | yum install -y tmux htop 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/construct/jenkins/resources/config/jenkins.yaml.ejs: -------------------------------------------------------------------------------- 1 | jenkins: 2 | numExecutors: 0 3 | slaveAgentPort: 50000 4 | systemMessage: Unity build pipeline 5 | agentProtocols: 6 | - JNLP4-connect 7 | authorizationStrategy: 8 | loggedInUsersCanDoAnything: 9 | allowAnonymousRead: false 10 | securityRealm: 11 | local: 12 | allowsSignup: false 13 | users: 14 | - id: admin 15 | password: passw0rd 16 | globalNodeProperties: 17 | - envVars: 18 | env: 19 | <% env.forEach(e => { 20 | %><%- 21 | [ 22 | ` - key: ${e}`, 23 | ` value: \${${e}}`, 24 | ].join('\n') + '\n' %><%});%> 25 | nodes: 26 | <% macAgents.forEach(agent => { 27 | %><%- 28 | [ 29 | ` - permanent:`, 30 | ` labelString: mac`, 31 | ` mode: EXCLUSIVE`, 32 | ` name: '${agent.name}'`, 33 | ` numExecutors: 1`, 34 | ` remoteFS: '/Users/ec2-user/jenkins'`, 35 | ` launcher:`, 36 | ` ssh:`, 37 | ` host: \${${agent.macHostEnv}}`, 38 | ` port: 22`, 39 | ` credentialsId: ${agent.sshCredentialsId}`, 40 | ` launchTimeoutSeconds: 60`, 41 | ` maxNumRetries: 10`, 42 | ` retryWaitTime: 15`, 43 | ` sshHostKeyVerificationStrategy: nonVerifyingKeyVerificationStrategy`, 44 | ].join('\n') + '\n' %><%});%> 45 | clouds: 46 | <% ec2FleetAgents.forEach(agent => { 47 | %><%- 48 | [ 49 | ` - eC2Fleet:`, 50 | ` addNodeOnlyIfRunning: false`, 51 | ` alwaysReconnect: true`, 52 | ` cloudStatusIntervalSec: 60`, 53 | ` computerConnector:`, 54 | ` sSHConnector:`, 55 | ` port: 22`, 56 | ` credentialsId: ${agent.sshCredentialsId}`, 57 | ` launchTimeoutSeconds: ${agent.sshConnectTimeoutSeconds}`, 58 | ` maxNumRetries: ${agent.sshConnectMaxNumRetries}`, 59 | ` retryWaitTime: ${agent.sshConnectRetryWaitTime}`, 60 | ` sshHostKeyVerificationStrategy: nonVerifyingKeyVerificationStrategy`, 61 | ` jvmOptions: '${agent.jvmOptions}'`, 62 | ` prefixStartSlaveCmd: '${agent.prefixStartSlaveCmd}'`, 63 | ` suffixStartSlaveCmd: '${agent.suffixStartSlaveCmd}'`, 64 | ` disableTaskResubmit: false`, 65 | ` fleet: \${${agent.fleetAsgNameEnv}}`, 66 | ` fsRoot: ${agent.fsRoot}`, 67 | ` idleMinutes: 5`, 68 | ` initOnlineCheckIntervalSec: 60`, 69 | ` initOnlineTimeoutSec: 60`, 70 | ` labelString: '${agent.label}'`, 71 | ` maxSize: ${agent.fleetMaxSize}`, 72 | ` maxTotalUses: -1`, 73 | ` minSize: ${agent.fleetMinSize}`, 74 | ` minSpareSize: 0`, 75 | ` name: '${agent.name}'`, 76 | ` noDelayProvision: false`, 77 | ` numExecutors: ${agent.numExecutors}`, 78 | ` privateIpUsed: true`, 79 | ` region: \${AWS_REGION}`, 80 | ` restrictUsage: false`, 81 | ` scaleExecutorsByWeight: false`, 82 | ].join('\n') + '\n' %><%});%> 83 | aws: 84 | s3: 85 | container: ${ARTIFACT_BUCKET_NAME} 86 | disableSessionToken: false 87 | prefix: 'artifacts/' 88 | useHttp: false 89 | usePathStyleUrl: false 90 | useTransferAcceleration: false 91 | unclassified: 92 | location: 93 | url: ${JENKINS_URL} 94 | adminAddress: 'example@example.com' 95 | artifactManager: 96 | artifactManagerFactories: 97 | - jclouds: 98 | provider: 's3' 99 | globalItemStorage: 100 | storage: 101 | s3: 102 | bucketName: ${ARTIFACT_BUCKET_NAME} 103 | region: ${AWS_REGION} 104 | timestamper: 105 | allPipelines: true 106 | -------------------------------------------------------------------------------- /docs/using-ami-for-caching.md: -------------------------------------------------------------------------------- 1 | ## Automatically update AMI for build agents 2 | Since EC2 Linux Spot instances are stateless, all the internal states of an instance (e.g. filesystem) are purged when an instance is terminated (e.g. by scaling activities.) This can slow down build processes because many build systems rely on caches of intermediate artifacts in a build server's filesystem, assuming that they are shared between build jobs, which is not always the case on stateless servers. 3 | 4 | However, we can share these caches between build jobs even in our Linux spot based system by using [Amazon Machine Images (AMI)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html). 5 | An AMI contains a snapshot of the filesystem of an instance ([Amazon EBS](https://aws.amazon.com/ebs/) snapshot.) If we create an AMI from an existing EC2 instance that was previously used for a Unity build job, any instance launched from the AMI will have warmed caches ready as soon as it is initialized.We can even create AMIs periodically to keep the caches updated. 6 | By using AMIs, it is possible to overcome the drawbacks of stateless instances and make the build process fast as if they were stateful. 7 | 8 | When creating an AMI from a build server, we need to be careful about the following facts: 9 | 10 | 1. An instance should be rebooted when an AMI is created from it to ensure the consistency of the snapshot ([doc](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/tkv-create-ami-from-instance.html)) 11 | 2. An instance must not belong to an auto scaling group, since it can be terminated by the ASG when it is rebooted. 12 | 3. During AMI creation, a Unity build job should not be running on the instance. This may break the cache consistency. 13 | 4. During AMI creation, [spot interruption](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-interruptions.html) can happen to the instance, causing the build to fail. To mitigate this, we need a retry mechanism. 14 | 15 | With all of the above in mind, we include a sample Jenkins job to periodically create and update AMIs for Linux Jenkins agents. 16 | 17 | ![AMI workflow](docs/imgs/ami-workflow.svg) 18 | 19 | The `detachFromAsg` job is intended to be called periodically (e.g. by using [Jenkins cron job](https://www.jenkins.io/doc/book/pipeline/syntax/#cron-syntax)) and will attempt to create an AMI and update the ASG as needed. You can reference the implementation and integrate it into your own build system. 20 | 21 | The disadvantage of using AMI for caching, however, is that it takes some time to fully fetch (hydrate) EBS snapshots, resulting in higher I/O latency during the hydration. In some situations, the hydration process takes too long to be used as a cache. One solution to this the problem is to use the Fast Snapshot Restore feature, which allows the volume to be hydrated immediately without much I/O latency ([Addressing I/O latency when restoring Amazon EBS volumes from EBS Snapshots](https://aws.amazon.com/blogs/storage/addressing-i-o-latency-when-restoring-amazon-ebs-volumes-from-ebs-snapshots/)). 22 | 23 | Note that if you are using FSR, you should be aware of [volume creation credits](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-fast-snapshot-restore.html#volume-creation-credits) and [additional charges](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-fast-snapshot-restore.html#fsr-pricing). 24 | 25 | There is another way to avoid this problem and solve the caching problem at the same time, which is described in the README.md (EBS volume pool). 26 | 27 | The latest version does not include the job definition to update AMI, preferring EBS volume pool solution. 28 | 29 | Please refer to [`create_ami` tag](https://github.com/aws-samples/jenkins-unity-build-on-aws/tree/create_ami) for the actual implementation. 30 | -------------------------------------------------------------------------------- /lib/construct/jenkins/agent-mac.ts: -------------------------------------------------------------------------------- 1 | import { CfnHost, EbsDeviceVolumeType, Instance, InstanceType, IVpc, OperatingSystemType } from 'aws-cdk-lib/aws-ec2'; 2 | import { ManagedPolicy } from 'aws-cdk-lib/aws-iam'; 3 | import { Construct } from 'constructs'; 4 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 5 | import { IBucket } from 'aws-cdk-lib/aws-s3'; 6 | import { CfnOutput, RemovalPolicy, Size } from 'aws-cdk-lib'; 7 | 8 | export interface AgentMacProps { 9 | readonly vpc: IVpc; 10 | readonly sshKey: ec2.IKeyPair; 11 | readonly amiId: string; 12 | readonly storageSize: Size; 13 | readonly instanceType: string; 14 | readonly name: string; 15 | readonly artifactBucket?: IBucket; 16 | readonly subnet: ec2.ISubnet; 17 | } 18 | 19 | /** 20 | * A mac instance with a dedicated host. 21 | */ 22 | export class AgentMac extends Construct { 23 | public readonly ipAddress: string; 24 | public readonly name: string; 25 | public readonly sshCredentialsId: string; 26 | 27 | private readonly instance: Instance; 28 | 29 | constructor(scope: Construct, id: string, props: AgentMacProps) { 30 | super(scope, id); 31 | 32 | this.name = props.name; 33 | this.sshCredentialsId = 'instance-ssh-key-unix'; 34 | 35 | const { vpc, instanceType, subnet } = props; 36 | 37 | if (subnet == null) { 38 | throw new Error( 39 | 'Invalid subnet. Please try different subnet type (privateSubnets, isolatedSubnets, or publicSubnets) or index.', 40 | ); 41 | } 42 | 43 | const host = new CfnHost(this, 'DedicatedHost', { 44 | availabilityZone: subnet.availabilityZone, 45 | instanceType, 46 | }); 47 | // In some cases, we cannot delete a dedicated host immediately (e.g. 24 hours before its creation). 48 | // That's why we set RemovalPolicy = RETAIN here to avoid CFn errors. 49 | host.applyRemovalPolicy(RemovalPolicy.RETAIN); 50 | 51 | // Brew installation path differs with mac1 (Intel) and mac2 (M1) 52 | const brewPath = instanceType == 'mac1.metal' ? '/usr/local' : '/opt/homebrew'; 53 | const userData = ec2.UserData.custom(`#!/bin/zsh 54 | #install openjdk@17 55 | su ec2-user -c '${brewPath}/bin/brew install openjdk@17 jq' 56 | ln -sfn ${brewPath}/opt/openjdk@17/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-17.jdk 57 | java -version 58 | 59 | # resize disk to match the ebs volume 60 | # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-mac-instances.html#mac-instance-increase-volume 61 | PDISK=$(diskutil list physical external | head -n1 | cut -d" " -f1) 62 | APFSCONT=$(diskutil list physical external | grep "Apple_APFS" | tr -s " " | cut -d" " -f8) 63 | yes | diskutil repairDisk $PDISK 64 | diskutil apfs resizeContainer $APFSCONT 0 65 | 66 | # Start the ARD Agent 67 | # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-mac-instances.html#connect-to-mac-instance 68 | /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -activate -configure -access -on -restart -agent -privs -all 69 | `); 70 | 71 | const instance = new Instance(this, 'Instance', { 72 | vpc, 73 | instanceType: new InstanceType(instanceType), 74 | machineImage: { 75 | getImage: (_scope) => ({ 76 | imageId: props.amiId, 77 | osType: OperatingSystemType.UNKNOWN, 78 | userData: userData, 79 | }), 80 | }, 81 | vpcSubnets: { subnets: [subnet] }, 82 | keyPair: props.sshKey, 83 | blockDevices: [ 84 | { 85 | deviceName: '/dev/sda1', 86 | volume: ec2.BlockDeviceVolume.ebs(props.storageSize.toGibibytes(), { 87 | encrypted: true, 88 | volumeType: EbsDeviceVolumeType.GP3, 89 | }), 90 | }, 91 | ], 92 | ssmSessionPermissions: true, 93 | }); 94 | // You can enable termination protection by uncommenting this line. 95 | // (instance.node.defaultChild as ec2.CfnInstance).disableApiTermination = true; 96 | 97 | instance.instance.tenancy = 'host'; 98 | instance.instance.hostId = host.attrHostId; 99 | props.artifactBucket?.grantReadWrite(instance); 100 | 101 | this.instance = instance; 102 | this.ipAddress = instance.instancePrivateIp; 103 | 104 | new CfnOutput(this, 'InstanceId', { value: instance.instanceId }); 105 | } 106 | 107 | public allowSSHFrom(other: ec2.IConnectable) { 108 | this.instance.connections.allowFrom(other, ec2.Port.tcp(22)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /docs/deployment_ja.md: -------------------------------------------------------------------------------- 1 | # デプロイ手順書 2 | 3 | 本サンプルをデプロイする手順を記載します。 4 | 5 | ## 前提条件 6 | 7 | はじめに、AWS CDK を実行できる環境を用意してください。これは、以下の条件を満たしている必要があります。 8 | 9 | * 必要なソフトウェアがインストールされていること 10 | * [Node.js](https://nodejs.org/en/download/) 11 | * v16 以上を推奨 12 | * `node -v` コマンドで確認できます 13 | * [AWS CLI](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html) 14 | * v2 を推奨 15 | * `aws --version` コマンドで確認できます 16 | * Docker 17 | * `docker --version` コマンドで確認できます 18 | * AWS CLI に適切な AWS IAM 権限 (Administrator相当が必要) が設定されていること 19 | * IAM ロールの設定をするか、 `aws configure` コマンドで IAM ユーザーの情報を入力してください 20 | * インターネットに接続され、AWS API と疎通できること 21 | * 閉域環境などでは、正常に実行できない可能性があります 22 | 23 | 上記を満たす環境であれば、ローカル端末や AWS Cloud9、EC2 インスタンス等で利用可能です。上記の条件が確認できたら、次の手順に進んでください。 24 | 25 | ## CDK の利用準備 26 | 27 | 本プロトタイプのルートディレクトリ (`README.md` のあるディレクトリです) に移動し、以下のコマンドを実行してください。 28 | なお、以降の `cdk` コマンドは全てこのルートディレクトリで実行することを想定しています。 29 | 30 | ```sh 31 | # Node の依存パッケージをインストールします 32 | npm ci 33 | # ご利用の AWS 環境で CDK を利用できるように初期化を実行します 34 | npx cdk bootstrap 35 | ``` 36 | 37 | `npm ci` は、Node の依存関係をインストールします。初回のみ必要です。 38 | 39 | `cdk bootstrap` は、ご利用の環境で CDK を利用できるように初期設定をおこないます。 40 | こちらはある AWS アカウント・あるリージョンで初めて CDK を利用する場合に必要です。2 回目以降は必要ありません。 41 | 42 | ✅ `Environment aws://xxxx/ap-northeast-1 bootstrapped` という旨が表示されていれば成功です。次の手順に進んでください。 43 | 44 | ## サンプルのデプロイ 45 | 46 | CDK でスタックをデプロイします。 47 | 48 | 1. Jenkins ビルド環境のデプロイ 49 | * Jenkins 設定ファイル (`lib/construct/jenkins/resources/config/jenkins.yaml.ejs`) の内容を確認してください 50 | * パスワード (`password`) を十分強力なものに変更してください 51 | * 管理者メールアドレス (`adminAddress`) を管理者が利用可能なものに変更してください 52 | * Jenkins の管理画面にアクセスするグローバル IP アドレス (社内 VPN 等) を CIDR 形式で `bin/jenkins-unity-build.ts` の `allowedCidrs` に記入してください 53 | * 記入例: `const allowedCidrs = ['127.0.0.1/32', '100.200.0.0/16'];` 54 | * `bin/jenkins-unity-build.ts` を設定することで、既存のVPC上にシステムをデプロイすることも可能です 55 | * 以下のコマンドを実行し、Jenkins ビルド環境をデプロイします 56 | * `npx cdk deploy JenkinsUnityBuildStack` 57 | * デプロイ後、ターミナルに Outputs: 以下に表示される Jenkins Controller の URL を控えてください 58 | * Outputs 出力例: 59 | 60 | ```sh 61 | ✅ JenkinsUnityBuildStack 62 | 63 | ✨ Deployment time: 116.96s 64 | 65 | Outputs: 66 | JenkinsUnityBuildStack.JenkinsControllerLoadBalancerDomainName543C3FE0 = http://Unity-Jenki-xxxxxxxx.us-east-2.elb.amazonaws.com 67 | JenkinsUnityBuildStack.UnityAcceleratorUrl594D8007 = http://accelerator.build:10080 68 | Stack ARN: 69 | arn:aws:cloudformation:us-east-2:012345678901:stack/JenkinsUnityBuildStack/85318840-7f3a-11ed-8c6d-0ac490c584c0 70 | 71 | ✨ Total time: 129.94s 72 | ``` 73 | 2. Macインスタンスのデプロイ 74 | * Macインスタンスはデプロイに失敗した場合の処理がやや大変なので、分けてデプロイしています 75 | * このデプロイでDedicated hostの確保もあわせて行います。必ず[Quota](https://ap-northeast-1.console.aws.amazon.com/servicequotas/home/services/ec2/quotas)を事前に確認し、Dedicated hostを追加で確保可能なことを確かめてください。 76 | * `bin/jenkins-unity-build.ts` の `macAmiId` をアンコメントし、適切なAMI ID (リージョンごとに異なります) を記入してください 77 | * その後、再度次のコマンドを実行してください: `npx cdk deploy JenkinsUnityBuildStack` 78 | * 数分程度でデプロイが完了し、Macインスタンスが起動します 79 | 80 | ```ts 81 | macAmiId: 'ami-0c24e9b8b57e79e8e', // Monterey Intel Mac @ap-northeast-1 82 | ``` 83 | 3. Mac インスタンスの初期設定 84 | * [setup-mac-instance.md](./setup-mac-instance.md) を参考に、Mac インスタンスの初期設定を実行してください 85 | 4. Jenkins 管理画面の確認 86 | * 1 で表示された Jenkins Controller の URL から Jenkins 管理画面にアクセスし、正常に表示されていることを確認してください 87 | * URL はマネジメントコンソールの [CloudFormation](https://ap-northeast-1.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks) → [JenkinsUnityBuildStack] → [出力] からも参照することができます 88 | 89 | 以上で Unity ライセンスサーバーおよび Jenkins Controller, Agent のデプロイ作業は完了です。 90 | 91 | ## リソースの削除 92 | 93 | リソースを削除する際は、以下の手順に沿ってください。 94 | 95 | 初めに、Jenkins の EC2 Fleet の インスタンス数を 0 にします。このためには、Jenkins のクラウド管理のページ (Dashboard -> Manage Jenkins -> Nodes -> Configure Clouds) から、EC2 Fleet の `Minimum Cluster Size` を0に設定してください。 96 | 97 | すべての Linux/Windows Agent が削除されたことを Jenkins の GUI から確認したら、以下の CDK コマンドを実行してください: 98 | 99 | ```sh 100 | npx cdk destroy --all 101 | ``` 102 | 103 | また、いくつかのリソースを自動で削除しないようにしています。以下の手順に沿って、手動で削除してください。 104 | 105 | * EC2 Mac Dedicated host: Dedicated host を作成した直後24時間、また Mac インスタンスを終了した後 1-3 時間 (Pending 状態) は、Dedicated host を開放することができなくなります ([参照](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-mac-instances.html#mac-instance-stop))。十分時間が経過した後にお試しください。開放するには、[こちらのページ](https://us-east-2.console.aws.amazon.com/ec2/home?region=us-east-2#Hosts:)で当該のホストを選択し、Actions → Release host をクリックします。 106 | -------------------------------------------------------------------------------- /docs/setup-mac-instance.md: -------------------------------------------------------------------------------- 1 | # Setup EC2 Mac instance 2 | This sample already automates most of the initial setup process using AWS CDK. 3 | As for macOS, however, some tasks must be done manually, such as installing Xcode. 4 | This document describes what you need to do after the CDK deployment for EC2 Mac instances. 5 | 6 | ## Steps 7 | Please run these steps after you confirmed your Mac instance is in `Running` state on the [EC2 management console](https://console.aws.amazon.com/ec2/home). You can find the instance by the Name tag as `JenkinsUnityBuildStack/JenkinsMacAgent*/Instance`. 8 | 9 | ### 1. Connect to the instance via Sessions Manager 10 | You can connect your instance (like SSH) via the session manager of AWS Systems manager. Please follow the document for the detail: [Starting a session (Amazon EC2 console)](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-sessions-start.html#start-ec2-console). 11 | 12 | ### 2. Set password for ec2-user 13 | You need GUI access such as Apple Remote Desktop (ARD) in the following steps. To use ARD, you have to set password for the user you use in ARD session. 14 | 15 | You can set password by [the below command](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-to-mac-instance.html#mac-instance-vnc) (execute it on the previous SSH session): 16 | 17 | ```sh 18 | sudo passwd ec2-user 19 | ``` 20 | 21 | ### 3. Forward ARD port to your local machine 22 | Since ARD is already enabled in userData, you can now connect to the instance via ARD. 23 | 24 | Run the following command in your local machine: 25 | 26 | ```sh 27 | # You can get target instance ID in the EC2 management console 28 | aws ssm start-session \ 29 | --target i-xxxxxxxxxxxxxx \ 30 | --document-name AWS-StartPortForwardingSession \ 31 | --parameters '{"portNumber":["5900"], "localPortNumber":["5900"]}' 32 | ``` 33 | 34 | Note that you need to install AWS CLI Session Manager plugin to run the command: [Install the Session Manager plugin for the AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html). 35 | 36 | You should see a message like `Port 5900 opened for sessionId ...` if successful. 37 | Note that you have to keep the session open while you are accessing the instance in the next step. 38 | 39 | ### 4. Connect to the instance via ARD 40 | Now you can connect to `localhost:5900` by any VNC client you like. If you are using macOS locally, you have `Screen Sharing` app installed by default. 41 | 42 | When connected to the instance, you have to enter username and password as below: 43 | 44 | * username: ec2-user 45 | * password: the password you entered on step 2 46 | 47 | If you want to change the screen resolution, please refer to this document: [Modify macOS screen resolution on Mac instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-to-mac-instance.html#mac-screen-resolution) 48 | 49 | ### 5. Install Xcode 50 | You need Xcode installed for building iOS apps. Follow this instruction ([Install Xcode and accept license](https://catalog.us-east-1.prod.workshops.aws/workshops/43e96ac6-6d4f-4d99-af97-3ac2a5987391/en-US/020-build-farms/060-labs-unity-mac/015-environment-and-ec2-mac/040-ec2-mac-setup/040-install-xcode-and-accept-license) 51 | ) and complete the installation: 52 | 53 | 1. Install Xcode (You have to login to your Apple account) 54 | 2. Launch Xcode and accept the license 55 | 56 | ### 6. (Optional) Create an AMI 57 | You do not have to repeat all the steps above every time you provision a new mac instance. 58 | Instead, you can create an [Amazon Machine Image (AMI)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html) from your existing instance and reuse it when launching another instance. 59 | 60 | To create an AMI, please follow this document: [Create an AMI from an Amazon EC2 Instance](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/tkv-create-ami-from-instance.html). When you create an AMI, you can toggle `No reboot` checkbox, but it is recommended to keep it unchecked because `No reboot` sometimes results in unstable behavior. It usually takes about an hour to create an AMI from a mac instance. 61 | 62 | After AMI creation, you can use the AMI to launch another mac instance. See the below CDK code for reference: 63 | 64 | ```ts 65 | new AgentMac(this, 'JenkinsMacAgent2', { 66 | vpc, 67 | sshKeyName: keyPair.keyPairName, 68 | availabilityZone: vpc.privateSubnets[0].availabilityZone, 69 | // replace amiId with the AMI ID you created 70 | amiId: 'ami-xxxxxxxxxxx', 71 | artifactBucket, 72 | storageSizeGb: 200, 73 | instanceType: 'mac1.metal', 74 | }); 75 | ``` 76 | 77 | Note that you do not have to change AMI for the existing mac instance. 78 | -------------------------------------------------------------------------------- /lib/construct/jenkins/resources/agent-userdata-windows.yml: -------------------------------------------------------------------------------- 1 | version: 1.1 2 | tasks: 3 | - task: executeScript 4 | inputs: 5 | - frequency: once 6 | type: powershell 7 | runAs: admin 8 | content: |- 9 | $instanceMetadata = Get-EC2InstanceMetadata -Category @("AvailabilityZone", "InstanceId", "Region") 10 | $AZ = $instanceMetadata[0] 11 | $INSTANCE_ID = $instanceMetadata[1] 12 | $REGION = $instanceMetadata[2].SystemName 13 | 14 | $VOLUME_ID = Get-EC2Volume -Filter @(@{Name="tag:Kind"; Values=""}, @{Name="availability-zone"; Values=$AZ}, @{Name="status"; Values="available"}) -Region $REGION -Select "Volumes.VolumeId" | Select-Object -First 1 15 | if ($VOLUME_ID) { 16 | $JENKINS_DRIVE = "D" 17 | 18 | Write-Output "found volume ${VOLUME_ID}" 19 | 20 | $DEVICE_NAME = "/dev/xvdf" 21 | # There is possibly a race condition between other instances. 22 | # We may want to retry attach-volume according to the return code (currently omitted). 23 | Add-EC2Volume -Device $DEVICE_NAME -InstanceId $INSTANCE_ID -VolumeId $VOLUME_ID -Region $REGION 24 | 25 | # we should do polling for the volume status instead, but it usually finishes in a few seconds... 26 | Start-Sleep 10 27 | 28 | # basically following this doc: https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2-windows-volumes.html 29 | $serialNumber = $VOLUME_ID.Replace("vol-","vol") 30 | $disk = Get-Disk | Where-Object {$_.SerialNumber -CLike "$serialNumber*"} 31 | 32 | # basically following this doc: https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ebs-using-volumes.html 33 | if ($disk.PartitionStyle -eq "RAW") { 34 | Write-Output "initialize raw disk" 35 | $disk | Initialize-Disk -PartitionStyle MBR 36 | } 37 | 38 | # Get partition information from the volume 39 | $partitions = $disk | Get-Partition 40 | if (-not $partitions) { 41 | # If no partitions present, create a new partition and format it. 42 | Write-Output "create partition" 43 | $disk | New-Partition -DriveLetter $JENKINS_DRIVE -UseMaximumSize | Format-Volume -FileSystem NTFS -NewFileSystemLabel "Data" 44 | } else { 45 | if ($disk.IsOffline) { 46 | # If the disk was offline for unknown reason, change it online and reload partition information. 47 | Write-Output "Disk is offline, change to online." 48 | $disk | Set-Disk -IsOffline $false 49 | Start-Sleep 5 50 | 51 | $disk = Get-Disk | Where-Object {$_.SerialNumber -CLike "$serialNumber*"} 52 | $partitions = $disk | Get-Partition 53 | } 54 | if ($disk.IsReadOnly) { 55 | # If the disk was read only for unknown reason, change it writable. 56 | Write-Output "Disk is read only, change to writable." 57 | $disk | Set-Disk -IsReadOnly $false 58 | } 59 | 60 | # Find drive for Jenkins 61 | $jenkinsPartition = $partitions | Where-Object {$_.DriveLetter -eq $JENKINS_DRIVE} 62 | if ($jenkinsPartition) { 63 | Write-Output "drive $JENKINS_DRIVE already mounted" 64 | } else { 65 | Write-Output "change drive letter of first partition to $JENKINS_DRIVE" 66 | $partitions | Select-Object -First 1 | Set-Partition -NewDriveLetter $JENKINS_DRIVE 67 | } 68 | } 69 | } 70 | 71 | # Install chocolatey package manager: https://chocolatey.org/install 72 | Set-ExecutionPolicy Bypass -Scope Process -Force 73 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 74 | iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) 75 | 76 | # Install git and git-lfs 77 | choco install git -y --no-progress 78 | choco install git-lfs -y --no-progress 79 | 80 | # Install java 81 | choco install corretto17jdk --install-args INSTALLDIR="C:\Java" -y --no-progress 82 | 83 | # Install Unity Hub 84 | choco install unity-hub -y --no-progress 85 | 86 | # Install Unity Editor 87 | $unityVersion = '2021.3.26f1' 88 | $unityVersionChangeset = 'a16dc32e0ff2' 89 | & "$env:ProgramFiles\Unity Hub\Unity Hub.exe" -- --no-sandbox --headless install --version $unityVersion --changeset $unityVersionChangeset | Out-String -Stream 90 | 91 | # Install iOS / Android Build Support 92 | & "$env:ProgramFiles\Unity Hub\Unity Hub.exe" -- --no-sandbox --headless install-modules --version $unityVersion --module ios android --childModules | Out-String -Stream 93 | - task: enableOpenSsh 94 | -------------------------------------------------------------------------------- /lib/jenkins-unity-build-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 3 | import * as ecr from 'aws-cdk-lib/aws-ecr'; 4 | import * as servicediscovery from 'aws-cdk-lib/aws-servicediscovery'; 5 | import { Secret } from 'aws-cdk-lib/aws-ecs'; 6 | import { Controller } from './construct/jenkins/controller'; 7 | import { Bucket, BucketEncryption, BlockPublicAccess } from 'aws-cdk-lib/aws-s3'; 8 | import { Construct } from 'constructs'; 9 | import { AgentEC2Fleet } from './construct/jenkins/agent-ec2-fleet'; 10 | import { AgentMac } from './construct/jenkins/agent-mac'; 11 | import { UnityAccelerator } from './construct/unity-accelerator'; 12 | import { Size } from 'aws-cdk-lib'; 13 | import { InstanceClass, InstanceSize } from 'aws-cdk-lib/aws-ec2'; 14 | 15 | interface AgentFleetConfiguration { 16 | type: 'LinuxFleet' | 'WindowsFleet'; 17 | 18 | /** 19 | * A unique identifier for this agent 20 | */ 21 | name: string; 22 | 23 | /** 24 | * Jenkins node label 25 | */ 26 | label: string; 27 | 28 | /** 29 | * @default Size.gibibytes(30) 30 | */ 31 | rootVolumeSize?: Size; 32 | 33 | /** 34 | * @default No data volume 35 | */ 36 | dataVolumeSize?: Size; 37 | 38 | /** 39 | * @default [ec2.InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM)] 40 | */ 41 | instanceTypes?: ec2.InstanceType[]; 42 | 43 | /** 44 | * @default 1 45 | */ 46 | fleetMinSize?: number; 47 | 48 | /** 49 | * @default 1 50 | */ 51 | fleetMaxSize?: number; 52 | 53 | /** 54 | * Jenkins numExecutors for each node 55 | * 56 | * @default 1 57 | */ 58 | numExecutors?: number; 59 | 60 | /** 61 | * @default vpc.privateSubnets 62 | */ 63 | subnets?: (vpc: ec2.IVpc) => ec2.ISubnet[]; 64 | } 65 | 66 | interface MacInstanceConfiguration { 67 | /** 68 | * Some AZs don't support Mac instances and you will see an error on CFn deployment. 69 | * In that case, please change the index of subnets (e.g. privateSubnets[0] or isolatedSubnets[1]) 70 | * @default vpc.privateSubnet[0] 71 | */ 72 | subnet?: (vpc: ec2.IVpc) => ec2.ISubnet; 73 | 74 | /** 75 | * @default Size.gigabytes(200) 76 | */ 77 | storageSize?: Size; 78 | 79 | /** 80 | * @default InstanceType.of(InstanceClass.MAC2, InstanceSize.METAL) 81 | */ 82 | instanceType?: ec2.InstanceType; 83 | 84 | /** 85 | * AMI ID to use for this mac instance. 86 | * Check https://console.aws.amazon.com/ec2/v2/home#AMICatalog: 87 | * 88 | * Please double check your region and CPU architecture matches your instance. 89 | */ 90 | amiId: string; 91 | 92 | /** 93 | * A unique name for this Jenkins agent. 94 | */ 95 | name: string; 96 | } 97 | 98 | interface UnityAcceleratorConfiguration { 99 | volumeSize: Size; 100 | 101 | /** 102 | * You can explicitly set a subnet that Unity accelerator is deployed at. 103 | * It can possibly improve the Accelerator performance to use the same Availability zone as the Jenkins agents. 104 | * 105 | * @default One of the vpc.privateSubnets 106 | */ 107 | subnet?: (vpc: ec2.IVpc) => ec2.ISubnet; 108 | } 109 | 110 | interface JenkinsUnityBuildStackProps extends cdk.StackProps { 111 | /** 112 | * IP address ranges which you can access Jenkins Web UI from 113 | */ 114 | readonly allowedCidrs: string[]; 115 | 116 | /** 117 | * @default No EC2 fleet. 118 | */ 119 | ec2FleetConfigurations?: AgentFleetConfiguration[]; 120 | 121 | /** 122 | * @default No Mac instances. 123 | */ 124 | macInstancesCOnfigurations?: MacInstanceConfiguration[]; 125 | 126 | /** 127 | * You can optionally pass a VPC to deploy the stack 128 | * 129 | * @default VPC is created automatically 130 | */ 131 | readonly vpcId?: string; 132 | 133 | /** 134 | * ARN of an ACM certificate for Jenkins controller ALB. 135 | * 136 | * @default Traffic is not encrypted (via HTTP) 137 | */ 138 | readonly certificateArn?: string; 139 | 140 | /** 141 | * The base URL for your Unity license server. 142 | * See this document for more details: https://docs.unity3d.com/licensing/manual/ClientConfig.html 143 | * 144 | * @default No license server (undefined) 145 | */ 146 | readonly licenseServerBaseUrl?: string; 147 | 148 | /** 149 | * @default No Unity Accelerator. 150 | */ 151 | readonly unityAccelerator?: UnityAcceleratorConfiguration; 152 | } 153 | 154 | export class JenkinsUnityBuildStack extends cdk.Stack { 155 | constructor(scope: Construct, id: string, props: JenkinsUnityBuildStackProps) { 156 | super(scope, id, props); 157 | 158 | const vpc = 159 | props.vpcId == null 160 | ? new ec2.Vpc(this, 'Vpc', { 161 | natGateways: 1, 162 | }) 163 | : ec2.Vpc.fromLookup(this, 'Vpc', { vpcId: props.vpcId }); 164 | 165 | // S3 bucket to store logs (e.g. ALB access log or S3 bucket access log) 166 | const logBucket = new Bucket(this, 'LogBucket', { 167 | removalPolicy: cdk.RemovalPolicy.DESTROY, 168 | autoDeleteObjects: true, 169 | encryption: BucketEncryption.S3_MANAGED, 170 | enforceSSL: true, 171 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 172 | }); 173 | 174 | // S3 bucket which can be accessed from Jenkins agents 175 | // you can use it to store artifacts or pass files between stages 176 | const artifactBucket = new Bucket(this, 'ArtifactBucket', { 177 | removalPolicy: cdk.RemovalPolicy.DESTROY, 178 | autoDeleteObjects: true, 179 | encryption: BucketEncryption.S3_MANAGED, 180 | enforceSSL: true, 181 | serverAccessLogsBucket: logBucket, 182 | serverAccessLogsPrefix: 'artifactBucketAccessLogs/', 183 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 184 | }); 185 | 186 | // If you want to use private container registry for Jenkins jobs, use this repository 187 | // By default it is not used at all. 188 | const containerRepository = new ecr.Repository(this, 'Repository', { 189 | imageScanOnPush: true, 190 | removalPolicy: cdk.RemovalPolicy.DESTROY, 191 | }); 192 | 193 | // EC2 key pair that Jenkins controller uses to connect to Jenkins agents 194 | const keyPair = new ec2.KeyPair(this, 'KeyPair'); 195 | 196 | const namespace = new servicediscovery.PrivateDnsNamespace(this, 'Namespace', { 197 | vpc, 198 | name: 'build', 199 | }); 200 | 201 | let accelerator; 202 | if (props.unityAccelerator !== undefined) { 203 | const config = props.unityAccelerator; 204 | accelerator = new UnityAccelerator(this, 'UnityAccelerator', { 205 | vpc, 206 | namespace, 207 | storageSizeGb: config.volumeSize.toGibibytes(), 208 | // You can explicitly set a subnet that Unity accelerator is deployed at. 209 | // It can possibly improve the Accelerator performance to use the same Availability zone as the Jenkins agents. 210 | subnet: config.subnet ? config.subnet(vpc) : undefined, 211 | }); 212 | } 213 | 214 | // const ec2FleetAgents = []; 215 | const ec2FleetAgents = (props.ec2FleetConfigurations ?? []).map((config) => { 216 | const ctor = config.type == 'WindowsFleet' ? AgentEC2Fleet.windowsFleet : AgentEC2Fleet.linuxFleet; 217 | return ctor(this, `${config.type}-${config.name}`, { 218 | vpc, 219 | sshKey: keyPair, 220 | artifactBucket, 221 | rootVolumeSize: config.rootVolumeSize ?? Size.gibibytes(30), 222 | dataVolumeSize: config.dataVolumeSize, 223 | instanceTypes: config.instanceTypes ?? [ec2.InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM)], 224 | name: config.name, 225 | label: config.label, 226 | fleetMinSize: config.fleetMinSize ?? 1, 227 | fleetMaxSize: config.fleetMaxSize ?? 1, 228 | numExecutors: config.numExecutors, 229 | subnets: config.subnets ? config.subnets(vpc) : undefined, 230 | }); 231 | }); 232 | 233 | const macAgents = (props.macInstancesCOnfigurations ?? []).map( 234 | (config) => 235 | new AgentMac(this, `MacAgent-${config.name}`, { 236 | vpc, 237 | artifactBucket, 238 | subnet: config.subnet ? config.subnet(vpc) : vpc.privateSubnets[0], 239 | storageSize: config.storageSize ?? Size.gibibytes(200), 240 | instanceType: config.instanceType?.toString() ?? 'mac2.metal', 241 | sshKey: keyPair, 242 | amiId: config.amiId, 243 | name: config.name, 244 | }), 245 | ); 246 | 247 | const controllerEcs = new Controller(this, 'JenkinsController', { 248 | vpc, 249 | allowedCidrs: props.allowedCidrs, 250 | logBucket, 251 | artifactBucket, 252 | certificateArn: props.certificateArn, 253 | environmentSecrets: { PRIVATE_KEY: Secret.fromSsmParameter(keyPair.privateKey) }, 254 | environmentVariables: { 255 | ...(accelerator ? { UNITY_ACCELERATOR_URL: accelerator.endpoint } : {}), 256 | UNITY_BUILD_SERVER_URL: props.licenseServerBaseUrl ?? '', 257 | }, 258 | containerRepository, 259 | macAgents: macAgents, 260 | ec2FleetAgents: ec2FleetAgents, 261 | }); 262 | ec2FleetAgents.forEach((agent) => agent.allowSSHFrom(controllerEcs.service)); 263 | macAgents.forEach((agent) => agent.allowSSHFrom(controllerEcs.service)); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /lib/construct/jenkins/controller.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import * as efs from 'aws-cdk-lib/aws-efs'; 3 | import { IVpc } from 'aws-cdk-lib/aws-ec2'; 4 | import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; 5 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 6 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 7 | import { ApplicationProtocol, SslPolicy } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 8 | import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns'; 9 | import { IBucket } from 'aws-cdk-lib/aws-s3'; 10 | import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'; 11 | import { RetentionDays } from 'aws-cdk-lib/aws-logs'; 12 | import { Platform } from 'aws-cdk-lib/aws-ecr-assets'; 13 | import { AccountRootPrincipal, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; 14 | import { join } from 'path'; 15 | import { IRepository } from 'aws-cdk-lib/aws-ecr'; 16 | import { Cluster } from 'aws-cdk-lib/aws-ecs'; 17 | import * as ejs from 'ejs'; 18 | import { writeFileSync } from 'fs'; 19 | import { ServiceLinkedRole } from 'upsert-slr'; 20 | 21 | export interface MacAgentProps { 22 | readonly ipAddress: string; 23 | readonly name: string; 24 | readonly sshCredentialsId: string; 25 | } 26 | 27 | export interface EC2FleetAgentProps { 28 | readonly fleetAsgName: string; 29 | readonly fleetMinSize: number; 30 | readonly fleetMaxSize: number; 31 | 32 | readonly label: string; 33 | readonly name: string; 34 | readonly sshCredentialsId: string; 35 | readonly fsRoot: string; 36 | 37 | readonly sshConnectTimeoutSeconds: number; 38 | readonly sshConnectMaxNumRetries: number; 39 | readonly sshConnectRetryWaitTime: number; 40 | 41 | readonly jvmOptions: string; 42 | readonly prefixStartSlaveCmd: string; 43 | readonly suffixStartSlaveCmd: string; 44 | } 45 | 46 | export interface ControllerProps { 47 | readonly vpc: IVpc; 48 | readonly logBucket: IBucket; 49 | readonly artifactBucket: IBucket; 50 | readonly environmentVariables: { [key: string]: string }; 51 | readonly environmentSecrets: { [key: string]: ecs.Secret }; 52 | readonly allowedCidrs?: string[]; 53 | readonly certificateArn?: string; 54 | readonly containerRepository?: IRepository; 55 | readonly macAgents?: MacAgentProps[]; 56 | readonly ec2FleetAgents?: EC2FleetAgentProps[]; 57 | } 58 | 59 | /** 60 | * EC2 Auto Scaling Group for Jenkins controller. 61 | * The number of instances is fixed to one since Jenkins does not support horizontal scaling. 62 | */ 63 | export class Controller extends Construct { 64 | public readonly service: ecs.FargateService; 65 | 66 | constructor(scope: Construct, id: string, props: ControllerProps) { 67 | super(scope, id); 68 | 69 | const { macAgents = [], ec2FleetAgents = [] } = props; 70 | 71 | const { vpc, allowedCidrs = [] } = props; 72 | allowedCidrs.push(vpc.vpcCidrBlock); 73 | 74 | // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using-service-linked-roles.html 75 | const slr = new ServiceLinkedRole(this, 'EcsSlr', { 76 | awsServiceName: 'ecs.amazonaws.com', 77 | }); 78 | 79 | const cluster = new Cluster(this, 'Cluster', { 80 | vpc, 81 | containerInsights: true, 82 | }); 83 | 84 | // don't create the cluster before ECS service linked role is created 85 | cluster.node.defaultChild!.node.addDependency(slr); 86 | 87 | const fileSystem = new efs.FileSystem(this, 'Storage', { 88 | vpc, 89 | performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, 90 | removalPolicy: RemovalPolicy.DESTROY, 91 | }); 92 | 93 | const protocol = props.certificateArn != null ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP; 94 | 95 | let certificate = undefined; 96 | if (props.certificateArn != null) { 97 | certificate = Certificate.fromCertificateArn(this, 'Cert', props.certificateArn); 98 | } 99 | 100 | const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', { 101 | cpu: 1024, 102 | memoryLimitMiB: 2048, 103 | }); 104 | 105 | const fleetAsgNameEnv = (agent: { name: string }) => 106 | `FLEET_ASG_NAME_${agent.name.toUpperCase().replace(/-/g, '_')}`; 107 | 108 | const macHostEnv = (agent: { name: string }) => `MAC_HOST_${agent.name.toUpperCase().replace(/-/g, '_')}`; 109 | 110 | const exportingEnvironment = { 111 | ...props.environmentVariables, 112 | AWS_REGION: Stack.of(this).region, 113 | ARTIFACT_BUCKET_NAME: props.artifactBucket.bucketName, 114 | ...Object.fromEntries(ec2FleetAgents.flatMap((agent) => [[fleetAsgNameEnv(agent), agent.fleetAsgName]])), 115 | // We need these values when we use a Docker Image from ECR repository for a Jenkins Docker Agent 116 | // https://itnext.io/how-to-run-jenkins-agents-with-cross-account-ecr-images-using-instance-roles-on-eks-2544b0fc6819 117 | ECR_REPOSITORY_URL: props.containerRepository?.repositoryUri ?? '', 118 | ECR_REGISTRY_URL: `https://${Stack.of(this).account}.dkr.ecr.${Stack.of(this).region}.amazonaws.com`, 119 | }; 120 | 121 | // avoid from overwriting jenkins.yaml of other stacks 122 | const configOutputFilename = `jenkins.${Stack.of(this).stackName}.yaml`; 123 | ejs.renderFile( 124 | join(__dirname, 'resources', 'config', 'jenkins.yaml.ejs'), 125 | { 126 | env: [...Object.keys(exportingEnvironment)], 127 | macAgents: macAgents.map((agent) => ({ 128 | ...agent, 129 | macHostEnv: macHostEnv(agent), 130 | })), 131 | ec2FleetAgents: ec2FleetAgents.map((agent) => ({ 132 | ...agent, 133 | fleetAsgNameEnv: fleetAsgNameEnv(agent), 134 | })), 135 | }, 136 | {}, 137 | function (err, str) { 138 | writeFileSync(join(__dirname, 'resources', 'config', configOutputFilename), str); 139 | }, 140 | ); 141 | 142 | const container = taskDefinition.addContainer('main', { 143 | image: ecs.ContainerImage.fromAsset(join(__dirname, 'resources'), { 144 | file: 'controller.Dockerfile', 145 | platform: Platform.LINUX_AMD64, 146 | buildArgs: { 147 | CONFIG_FILE_NAME: configOutputFilename, 148 | }, 149 | }), 150 | logging: ecs.LogDriver.awsLogs({ 151 | streamPrefix: 'jenkins-controller', 152 | logRetention: RetentionDays.SIX_MONTHS, 153 | }), 154 | portMappings: [ 155 | { 156 | // for jenkins web UI 157 | containerPort: 8080, 158 | }, 159 | ], 160 | linuxParameters: new ecs.LinuxParameters(this, 'LinuxParameters', { 161 | initProcessEnabled: true, 162 | }), 163 | environment: { 164 | ...exportingEnvironment, 165 | PLUGINS_FORCE_UPGRADE: 'true', 166 | ECR_ROLE_ARN: taskDefinition.taskRole.roleArn, 167 | ...Object.fromEntries(macAgents.flatMap((agent) => [[macHostEnv(agent), agent.ipAddress]])), 168 | }, 169 | secrets: { 170 | ...props.environmentSecrets, 171 | }, 172 | }); 173 | 174 | const controller = new ApplicationLoadBalancedFargateService(this, 'Service', { 175 | cluster, 176 | // We only need just one instance for Jenkins controller 177 | desiredCount: 1, 178 | targetProtocol: ApplicationProtocol.HTTP, 179 | openListener: false, 180 | cpu: 1024, 181 | memoryLimitMiB: 2048, 182 | taskDefinition: taskDefinition, 183 | healthCheckGracePeriod: Duration.seconds(60), 184 | protocol, 185 | certificate, 186 | sslPolicy: protocol == ApplicationProtocol.HTTPS ? SslPolicy.RECOMMENDED : undefined, 187 | circuitBreaker: { rollback: true }, 188 | minHealthyPercent: 100, 189 | maxHealthyPercent: 200, 190 | enableExecuteCommand: true, 191 | }); 192 | 193 | container.addEnvironment( 194 | 'JENKINS_URL', 195 | `${protocol.toLowerCase()}://${controller.loadBalancer.loadBalancerDnsName}`, 196 | ); 197 | 198 | // https://github.com/aws/aws-cdk/issues/4015 199 | controller.targetGroup.setAttribute('deregistration_delay.timeout_seconds', '10'); 200 | 201 | controller.targetGroup.configureHealthCheck({ 202 | interval: Duration.seconds(15), 203 | healthyThresholdCount: 2, 204 | unhealthyThresholdCount: 4, 205 | healthyHttpCodes: '200', 206 | path: '/login', // https://devops.stackexchange.com/a/9178 207 | }); 208 | 209 | // https://plugins.jenkins.io/ec2-fleet/#plugin-content-3-configure-user-permissions 210 | taskDefinition.addToTaskRolePolicy( 211 | new PolicyStatement({ 212 | actions: [ 213 | 'ec2:DescribeSpotFleetInstances', 214 | 'ec2:ModifySpotFleetRequest', 215 | 'ec2:CreateTags', 216 | 'ec2:DescribeRegions', 217 | 'ec2:DescribeInstances', 218 | 'ec2:TerminateInstances', 219 | 'ec2:DescribeInstanceStatus', 220 | 'ec2:DescribeSpotFleetRequests', 221 | "ec2:DescribeFleets", 222 | "ec2:DescribeFleetInstances", 223 | "ec2:ModifyFleet", 224 | "ec2:DescribeInstanceTypes", 225 | 'autoscaling:DescribeAutoScalingGroups', 226 | 'autoscaling:UpdateAutoScalingGroup', 227 | 'iam:ListInstanceProfiles', 228 | 'iam:ListRoles', 229 | ], 230 | resources: ['*'], 231 | }), 232 | ); 233 | 234 | taskDefinition.addToTaskRolePolicy( 235 | new PolicyStatement({ 236 | actions: ['iam:PassRole'], 237 | resources: [taskDefinition.taskRole.roleArn], 238 | conditions: { 239 | StringEquals: { 240 | 'iam:PassedToService': ['ec2.amazonaws.com'], 241 | }, 242 | }, 243 | }), 244 | ); 245 | 246 | props.containerRepository?.grantPull(taskDefinition.taskRole); 247 | props.artifactBucket.grantReadWrite(taskDefinition.taskRole); 248 | taskDefinition.taskRole.grantAssumeRole(taskDefinition.taskRole); 249 | 250 | // When Jenkins accesses an ECR repository, it tries to assume this role. 251 | // To allow the action, we want to specify the role session name as a principal, but it is not possible 252 | // because ECS uses a random name for the session. We are not allowed to use a wildcard here. 253 | // As a workaround, we use AccountRoot principal here. 254 | // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#principal-role-session 255 | (taskDefinition.taskRole as Role).assumeRolePolicy!.addStatements( 256 | new PolicyStatement({ 257 | actions: ['sts:AssumeRole'], 258 | principals: [new AccountRootPrincipal()], 259 | }), 260 | ); 261 | 262 | const port = protocol == ApplicationProtocol.HTTPS ? 443 : 80; 263 | allowedCidrs.forEach((cidr) => { 264 | controller.loadBalancer.connections.allowFrom(ec2.Peer.ipv4(cidr), ec2.Port.tcp(port)); 265 | }); 266 | 267 | controller.loadBalancer.logAccessLogs(props.logBucket, 'jenkinsAlbAccessLog'); 268 | 269 | fileSystem.connections.allowDefaultPortFrom(controller.service.connections); 270 | 271 | // https://docs.aws.amazon.com/efs/latest/ug/accessing-fs-nfs-permissions.html 272 | // https://aws.amazon.com/blogs/containers/developers-guide-to-using-amazon-efs-with-amazon-ecs-and-aws-fargate-part-2/ 273 | const efsAccessPoint = fileSystem.addAccessPoint('AccessPoint', { 274 | posixUser: { 275 | uid: '1000', 276 | gid: '1000', 277 | }, 278 | path: '/jenkins-home', 279 | createAcl: { 280 | ownerGid: '1000', 281 | ownerUid: '1000', 282 | permissions: '755', 283 | }, 284 | }); 285 | 286 | fileSystem.grant( 287 | taskDefinition.taskRole, 288 | 'elasticfilesystem:ClientMount', 289 | 'elasticfilesystem:ClientWrite', 290 | 'elasticfilesystem:ClientRootAccess', 291 | ); 292 | 293 | const volumeName = 'shared'; 294 | 295 | taskDefinition.addVolume({ 296 | name: volumeName, 297 | efsVolumeConfiguration: { 298 | fileSystemId: fileSystem.fileSystemId, 299 | transitEncryption: 'ENABLED', 300 | authorizationConfig: { 301 | accessPointId: efsAccessPoint.accessPointId, 302 | iam: 'ENABLED', 303 | }, 304 | }, 305 | }); 306 | 307 | container.addMountPoints({ 308 | // https://jenkins-le-guide-complet.github.io/html/sec-hudson-home-directory-contents.html 309 | containerPath: '/var/jenkins_home', 310 | sourceVolume: volumeName, 311 | readOnly: false, 312 | }); 313 | 314 | this.service = controller.service; 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /lib/construct/jenkins/agent-ec2-fleet.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 4 | import * as autoscaling from 'aws-cdk-lib/aws-autoscaling'; 5 | import * as s3 from 'aws-cdk-lib/aws-s3'; 6 | import * as iam from 'aws-cdk-lib/aws-iam'; 7 | import { readFileSync } from 'fs'; 8 | import { join } from 'path'; 9 | 10 | export interface AgentEC2FleetPropsBase { 11 | readonly vpc: ec2.IVpc; 12 | readonly sshKey: ec2.IKeyPair; 13 | readonly instanceTypes: ec2.InstanceType[]; 14 | 15 | /** 16 | * Name of the fleet. This must be a unique identifier across all the fleets. 17 | */ 18 | readonly name: string; 19 | 20 | /** 21 | * Label string applied to the nodes in this fleet. 22 | * You can use space separated string to apply multiple labels. (e.g. `label1 label2`) 23 | */ 24 | readonly label: string; 25 | 26 | readonly fleetMinSize: number; 27 | readonly fleetMaxSize: number; 28 | 29 | readonly rootVolumeSize: cdk.Size; 30 | 31 | /** 32 | * A S3 bucket this fleet has access (read/write) to. 33 | * @default No bucket. 34 | */ 35 | readonly artifactBucket?: s3.IBucket; 36 | 37 | /** 38 | * The size of a data volume that is attached as a secondary volume to an instance. 39 | * A data volume will be not deleted when an instance is terminated and reattached by new instances. 40 | * 41 | * @default No data volume is created. 42 | */ 43 | readonly dataVolumeSize?: cdk.Size; 44 | 45 | /** 46 | * @default deployed in vpc.privateSubnets 47 | */ 48 | readonly subnets?: ec2.ISubnet[]; 49 | 50 | /** 51 | * @default No additional policies added. 52 | */ 53 | readonly policyStatements?: iam.PolicyStatement[]; 54 | 55 | /** 56 | * Iops for gp3 volumes. Set integer from 3000 to 16000. 57 | * @default 3000 58 | */ 59 | readonly volumeIops?: number; 60 | 61 | /** 62 | * Throughput for gp3 volumes. Set integer from 125 to 1000. 63 | * @default 125 MiB/s 64 | */ 65 | readonly volumeThroughput?: number; 66 | 67 | /** 68 | * The number of executors in a single Jenkins agent node. 69 | * @default 1 70 | */ 71 | readonly numExecutors?: number; 72 | } 73 | 74 | export interface AgentEC2FleetProps extends AgentEC2FleetPropsBase { 75 | readonly machineImage: ec2.IMachineImage; 76 | readonly userData: ec2.UserData; 77 | readonly rootVolumeDeviceName: string; 78 | readonly fsRoot: string; 79 | readonly sshCredentialsId: string; 80 | 81 | readonly sshConnectTimeoutSeconds: number; 82 | readonly sshConnectMaxNumRetries: number; 83 | readonly sshConnectRetryWaitTime: number; 84 | 85 | readonly jvmOptions: string; 86 | readonly prefixStartSlaveCmd: string; 87 | readonly suffixStartSlaveCmd: string; 88 | } 89 | 90 | export interface AgentEC2FleetPropsCommon extends AgentEC2FleetPropsBase { 91 | /** 92 | * @default the latest OS image. 93 | */ 94 | readonly amiId?: string; 95 | readonly fsRoot?: string; 96 | 97 | readonly sshConnectTimeoutSeconds?: number; 98 | readonly sshConnectMaxNumRetries?: number; 99 | readonly sshConnectRetryWaitTime?: number; 100 | 101 | readonly jvmOptions?: string; 102 | readonly prefixStartSlaveCmd?: string; 103 | readonly suffixStartSlaveCmd?: string; 104 | } 105 | 106 | export interface AgentEC2FleetLinuxProps extends AgentEC2FleetPropsCommon {} 107 | 108 | export interface AgentEC2FleetWindowsProps extends AgentEC2FleetPropsCommon {} 109 | 110 | /** 111 | * Fleet of EC2 instances for Jenkins agents. 112 | * The number of instances is supposed to be controlled by Jenkins EC2 Fleet plugin. 113 | */ 114 | export class AgentEC2Fleet extends Construct implements iam.IGrantable { 115 | public readonly grantPrincipal: iam.IPrincipal; 116 | 117 | public readonly fleetAsgName: string; 118 | public readonly launchTemplate: ec2.LaunchTemplate; 119 | 120 | public readonly fleetMinSize: number; 121 | public readonly fleetMaxSize: number; 122 | public readonly numExecutors: number; 123 | 124 | public readonly name: string; 125 | public readonly label: string; 126 | public readonly sshCredentialsId: string; 127 | public readonly fsRoot: string; 128 | public readonly rootVolumeDeviceName: string; 129 | 130 | public readonly sshConnectTimeoutSeconds: number; 131 | public readonly sshConnectMaxNumRetries: number; 132 | public readonly sshConnectRetryWaitTime: number; 133 | 134 | public readonly jvmOptions: string; 135 | public readonly prefixStartSlaveCmd: string; 136 | public readonly suffixStartSlaveCmd: string; 137 | 138 | constructor(scope: Construct, id: string, props: AgentEC2FleetProps) { 139 | super(scope, id); 140 | 141 | this.fleetMinSize = props.fleetMinSize; 142 | this.fleetMaxSize = props.fleetMaxSize; 143 | this.numExecutors = props.numExecutors ?? 1; 144 | 145 | this.name = props.name; 146 | this.label = props.label; 147 | this.sshCredentialsId = props.sshCredentialsId; 148 | this.fsRoot = props.fsRoot; 149 | this.rootVolumeDeviceName = props.rootVolumeDeviceName; 150 | 151 | this.sshConnectTimeoutSeconds = props.sshConnectTimeoutSeconds; 152 | this.sshConnectMaxNumRetries = props.sshConnectMaxNumRetries; 153 | this.sshConnectRetryWaitTime = props.sshConnectRetryWaitTime; 154 | 155 | this.jvmOptions = props.jvmOptions; 156 | this.prefixStartSlaveCmd = props.prefixStartSlaveCmd; 157 | this.suffixStartSlaveCmd = props.suffixStartSlaveCmd; 158 | 159 | const { vpc, subnets = vpc.privateSubnets, instanceTypes, dataVolumeSize } = props; 160 | 161 | if (subnets.length == 0) { 162 | throw new Error('No subnet is available. Please specify one or more valid subnets to deploy the fleet.'); 163 | } 164 | 165 | const launchTemplate = new ec2.LaunchTemplate(this, 'LaunchTemplate', { 166 | machineImage: props.machineImage, 167 | blockDevices: [ 168 | { 169 | deviceName: props.rootVolumeDeviceName, 170 | volume: ec2.BlockDeviceVolume.ebs(props.rootVolumeSize.toGibibytes(), { 171 | volumeType: ec2.EbsDeviceVolumeType.GP3, 172 | encrypted: true, 173 | iops: props.volumeIops, 174 | }), 175 | }, 176 | ], 177 | keyPair: props.sshKey, 178 | userData: props.userData, 179 | role: new iam.Role(this, 'Role', { 180 | assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), 181 | managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')], 182 | }), 183 | securityGroup: new ec2.SecurityGroup(this, 'SecurityGroup', { 184 | vpc, 185 | }), 186 | }); 187 | 188 | // You can adjust throughput (MB/s) of the gp3 EBS volume, which is currently not exposed to the L2 construct. 189 | // https://github.com/aws/aws-cdk/issues/16213 190 | // https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/824 191 | (launchTemplate.node.defaultChild as cdk.CfnResource).addPropertyOverride( 192 | 'LaunchTemplateData.BlockDeviceMappings.0.Ebs.Throughput', 193 | props.volumeThroughput, 194 | ); 195 | 196 | props.artifactBucket?.grantReadWrite(launchTemplate); 197 | props.policyStatements?.forEach((policy) => launchTemplate.role!.addToPrincipalPolicy(policy)); 198 | 199 | const fleet = new autoscaling.AutoScalingGroup(this, 'Fleet', { 200 | vpc, 201 | mixedInstancesPolicy: { 202 | instancesDistribution: { 203 | onDemandBaseCapacity: 0, 204 | onDemandPercentageAboveBaseCapacity: 0, 205 | // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-fleet-allocation-strategy.html 206 | spotAllocationStrategy: autoscaling.SpotAllocationStrategy.PRICE_CAPACITY_OPTIMIZED, 207 | }, 208 | launchTemplate, 209 | launchTemplateOverrides: instanceTypes.map((type) => ({ instanceType: type })), 210 | }, 211 | vpcSubnets: { subnets }, 212 | }); 213 | 214 | 215 | if (dataVolumeSize != null) { 216 | const kind = `${cdk.Stack.of(this).stackName}-${id}`; 217 | const volumesPerAz = Math.ceil(props.fleetMaxSize / subnets.length); 218 | 219 | // create a pool of EBS volumes 220 | subnets 221 | .flatMap((subnet, azIndex) => 222 | Array.from({ length: volumesPerAz }, (_, volumeIndex) => ({ 223 | az: subnet.availabilityZone, 224 | azIndex, 225 | volumeIndex, 226 | })), 227 | ) 228 | .forEach((info) => { 229 | const volume = new ec2.Volume(this, `Volume-v1-${info.azIndex}-${info.volumeIndex}`, { 230 | availabilityZone: info.az, 231 | size: cdk.Size.gibibytes(dataVolumeSize.toGibibytes()), 232 | volumeType: ec2.EbsDeviceVolumeType.GP3, 233 | throughput: props.volumeThroughput, 234 | iops: props.volumeIops, 235 | encrypted: true, 236 | removalPolicy: cdk.RemovalPolicy.DESTROY, 237 | }); 238 | 239 | const tags = cdk.Tags.of(volume); 240 | tags.add('Name', `${kind}-${info.azIndex}-${info.volumeIndex}`); 241 | tags.add('Kind', kind); 242 | 243 | volume.grantAttachVolume(launchTemplate); 244 | volume.grantDetachVolume(launchTemplate); 245 | }); 246 | launchTemplate.role!.addToPrincipalPolicy( 247 | new iam.PolicyStatement({ 248 | actions: ['ec2:DescribeVolumes'], 249 | resources: ['*'], 250 | }), 251 | ); 252 | } 253 | 254 | this.grantPrincipal = fleet.role; 255 | this.launchTemplate = launchTemplate; 256 | this.fleetAsgName = fleet.autoScalingGroupName; 257 | } 258 | 259 | public allowSSHFrom(other: ec2.IConnectable) { 260 | this.launchTemplate.connections.allowFrom(other, ec2.Port.tcp(22)); 261 | } 262 | 263 | public static linuxFleet(scope: Construct, id: string, props: AgentEC2FleetLinuxProps) { 264 | const script = readFileSync(join(__dirname, 'resources', 'agent-userdata.sh'), 'utf8'); 265 | 266 | const commands = script.replace('', `${cdk.Stack.of(scope).stackName}-${id}`).split('\n'); 267 | 268 | const userData = ec2.UserData.forLinux(); 269 | userData.addCommands(...commands); 270 | return new AgentEC2Fleet(scope, id, { 271 | machineImage: props.amiId 272 | ? ec2.MachineImage.genericLinux({ [cdk.Stack.of(scope).region]: props.amiId }) 273 | : ec2.MachineImage.latestAmazonLinux2023(), 274 | userData: userData, 275 | rootVolumeDeviceName: '/dev/xvda', 276 | fsRoot: props.fsRoot ?? '/data/jenkins-agent', 277 | jvmOptions: props.jvmOptions ?? '', 278 | prefixStartSlaveCmd: props.prefixStartSlaveCmd ?? '', 279 | suffixStartSlaveCmd: props.suffixStartSlaveCmd ?? '', 280 | sshCredentialsId: 'instance-ssh-key-unix', 281 | sshConnectTimeoutSeconds: props.sshConnectTimeoutSeconds ?? 60, 282 | sshConnectMaxNumRetries: props.sshConnectMaxNumRetries ?? 10, 283 | sshConnectRetryWaitTime: props.sshConnectRetryWaitTime ?? 15, 284 | ...(props as AgentEC2FleetPropsBase), 285 | }); 286 | } 287 | 288 | public static windowsFleet(scope: Construct, id: string, props: AgentEC2FleetWindowsProps) { 289 | const script = readFileSync(join(__dirname, 'resources', 'agent-userdata-windows.yml'), 'utf8'); 290 | const userDataContent = script.replace('', `${cdk.Stack.of(scope).stackName}-${id}`); 291 | const userData = ec2.UserData.custom(userDataContent); 292 | 293 | return new AgentEC2Fleet(scope, id, { 294 | machineImage: props.amiId 295 | ? ec2.MachineImage.genericWindows({ [cdk.Stack.of(scope).region]: props.amiId }) 296 | : ec2.MachineImage.latestWindows(ec2.WindowsVersion.WINDOWS_SERVER_2022_ENGLISH_FULL_BASE), 297 | userData: userData, 298 | rootVolumeDeviceName: '/dev/sda1', 299 | 300 | jvmOptions: props.jvmOptions ?? '-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8', 301 | ...(props.dataVolumeSize != null 302 | ? { 303 | fsRoot: props.fsRoot ?? 'D:\\Jenkins', 304 | prefixStartSlaveCmd: props.prefixStartSlaveCmd ?? 'cd /d D:\\ && ', 305 | suffixStartSlaveCmd: props.suffixStartSlaveCmd ?? '', 306 | } 307 | : { 308 | fsRoot: props.fsRoot ?? 'C:\\Jenkins', 309 | prefixStartSlaveCmd: props.prefixStartSlaveCmd ?? '', 310 | suffixStartSlaveCmd: props.suffixStartSlaveCmd ?? '', 311 | }), 312 | 313 | sshCredentialsId: 'instance-ssh-key-windows', 314 | sshConnectTimeoutSeconds: props.sshConnectTimeoutSeconds ?? 60, 315 | sshConnectMaxNumRetries: props.sshConnectMaxNumRetries ?? 30, 316 | sshConnectRetryWaitTime: props.sshConnectRetryWaitTime ?? 15, 317 | ...(props as AgentEC2FleetPropsBase), 318 | }); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guidance for developing Apple Vision Pro / iOS applications with Unity on Amazon EC2 on AWS 2 | 3 | AWS blog: [Implementing a Build Pipeline for Unity Mobile Apps](https://aws.amazon.com/blogs/gametech/implementing-a-build-pipeline-for-unity-mobile-apps/) 4 | 5 | #### Table of Contents 6 | 7 | List the top-level sections of the README template, along with a hyperlink to the specific section. 8 | 9 | 10 | 1. [Overview](#overview) 11 | * [Cost](#cost) 12 | 2. [Prerequisites](#prerequisites) 13 | * [Operating System](#build-requirements) 14 | 3. [Deployment Steps](#deployment-steps) 15 | 4. [Deployment Validation](#deployment-validation) 16 | 5. [Running the Guidance](#running-the-guidance) 17 | 6. [Next Steps](#next-steps) 18 | 7. [Cleanup](#cleanup) 19 | 20 | 21 | #### Overview 22 | 23 | This guidance describes architectural considerations and configuration steps for developing and building Unity-based Apple Vision Pro / iOS applications using AWS services. It demonstrates how to implement a resilient, scalable, cost efficient and secure build pipeline on AWS, leveraging services such as Amazon EC2, Amazon ECS, and Amazon S3. 24 | 25 | The intended audience of this guidance are DevOps engineers, Cloud architects, system administrators, and platform engineers who would like to get hands-on experience architecting cloud-native applications for Apple Vision Pro / iOS in the AWS Cloud and are familiar with Unity development. 26 | 27 | 28 | #### Architecture overview 29 | 30 | Below is an architecture diagram of the Unity build pipeline for Apple Vision Pro / iOS applications using AWS services. 31 | 32 | ##### Architecture diagram and workflow steps 33 | 34 | ![architecture](docs/imgs/Reference-Architecture.png) 35 | 36 | 37 | 38 | 1. Source code is stored in a git code repository and is pulled by Jenkins on a build start. Developers access Jenkins Controller interface via an Application Load Balancer 39 | 2. Developers and System Administrators access EC2 Mac instances via Apple Remote Desktop (ARD), Linux agents via SSH and Unity Accelerator via HTTP using Systems Manager 40 | 3. System Administrators deploy and manage the infrastructure using AWS Cloud Development Kit 41 | 4. Jenkins Controller is deployed on Amazon Elastic Containers Service (ECS) Fargate via Amazon CDK. To guarantee redundancy, Amazon Elastic File Service (EFS) is used. 42 | 5. The first build stage (generating Xcode project from Unity source code), is run on Amazon Elastic Compute Cloud (Amazon EC2) Spot Instances. These are placed into an Auto Scaling group for scalability and redundancy. 43 | 6. Jenkins agent instances utilize Amazon Elastic Block Storage (EBS) Volumes and Amazon Simple Storage Service (S3) for repository and build asset caching mechanics. Also Unity Accelerator can be used for Unity asset caching 44 | 7. The resulting XCode project is transferred to a Jenkins worker on one of Amazon EC2 Mac Instances to finalize and sign the build and export .ipa file. 45 | 8. An .ipa archive file is exported as a Jenkins artifact and stored in Amazon Simple Storage Service (Amazon S3) bucket. 46 | 9. Certificates, private keys, and provisioning profiles are stored in AWS Secrets Manager and dynamically pulled onto the Mac during a build. 47 | 48 | Following this architecture, you should be able to: 49 | 50 | 51 | * Develop and build Unity applications for Apple Vision Pro / iOS using AWS services 52 | * Create an infrastructure that provides a scalable and cost-effective build pipeline for Vision Pro / iOS applications 53 | 54 | 55 | 56 | #### AWS services used in this Guidance 57 | 58 | | AWS service | Role | Description | 59 | | --------------------| ----------------- | -------------------------------| 60 | | Amazon EC2 | Core service | Hosts Linux and Mac instances for Unity builds and iOS compilation | 61 | | Amazon ECS | Core service | Runs the Jenkins Controller on Fargate | 62 | | Amazon S3 | Core service | Stores build artifacts and intermediary files | 63 | | Amazon VPC | Core Service | Provides network isolation and security | 64 | | Amazon Autoscaling | Core Service | Provides flexifility and cost efficiency | 65 | | Systems Manager | Supporting service | Manages EC2 instances and provides secure access | 66 | | NAT Gateway | Supporting service | Enables outbound internet access for private resources | 67 | | Elastic Load Balancing | Supporting service | Provides load balancing for the Jenkins Controller | 68 | 69 | 70 | #### Plan your deployment 71 | 72 | This guidance is based on using Amazon EC2 instances to host Unity builds and iOS compilation. It leverages Amazon ECS Fargate to run the Jenkins Controller for orchestrating the build pipeline and Amazon Autoscaling to adjust solution flexibility. You can extend the solutionby using containers to build Unity images, different EC2 Linux instances to adjust assets compilation phase and different EC2 Mac instances in combination with regular capacity adjustments for optimal cost control, security and build speed. 73 | 74 | #### Cost 75 | 76 | You are responsible for the cost of the AWS services used while running this solution guidance. As of July 2024, the cost for running this guidance with the default settings in the US East (N. Virginia) Region is approximately $688 per month for processing (1000 builds). 77 | We recommend creating a Budget through AWS Cost Explorer to help manage costs. Prices are subject to change. For full details, refer to the pricing webpage for each AWS service used in this Guidance. 78 | 79 | 80 | ##### Estimated monthly cost breakdown 81 | 82 | The following table provides a sample cost breakdown for deploying this guidance in the US East (N. Virginia) Region for one month. 83 | 84 | 85 | | AWS service | Dimensions | Cost [USD] | 86 | | --------------------| ----------------- | -------------------------------| 87 | | Amazon ECS (Fargate) | 1 task running 24/7 | $36 | 88 | | Amazon EC2 (Linux Spot) | c5.xlarge spot instances, 24/7 usage | $48 | 89 | | Amazon EC2 (Mac) | 1 mac2.metal instance, 24/7 usage | $491 | 90 | | Amazon S3 | 100 GB storage, 1000 PUT/COPY/POST/LIST requests | $3 | 91 | | NAT Gateway | 2 NAT Gateways, 100 GB data processed | $70 | 92 | | Application Load Balancer | 1GB processed bytes | $16 | 93 | | Amazon EBS | 3x 100GB volumes, 24/7 usage | $24 | 94 | | Amazon EFS | 1GB storage | $0.20 | 95 | | TOTAL | estimate | $688 | 96 | 97 | #### Prerequisites 98 | 99 | Before you begin deploying this guidance, ensure you have the following prerequisites in place: 100 | 101 | AWS Account and Permissions 102 | 103 | 1. An AWS account with permissions to create and manage the required resources. 104 | 2. AWS CLI installed and configured with appropriate credentials. 105 | 106 | ##### Build requirements: 107 | 108 | First, prepare an environment where AWS CDK can be executed. This environment must meet the following conditions: 109 | 110 | * Required software is installed 111 | * Node.js 112 | * Version 16 or higher is recommended 113 | * Can be checked with the node -v command 114 | * AWS CLI 115 | * Version 2 is recommended 116 | * Can be checked with the aws --version command 117 | * Docker 118 | * Can be checked with the docker --version command 119 | * Appropriate AWS IAM permissions (equivalent to Administrator) are set for AWS CLI 120 | * Configure IAM role settings or enter IAM user information using the aws configure command 121 | * Connected to the internet and able to communicate with AWS API 122 | * May not work properly in closed environments 123 | 124 | If your environment meets the above conditions, it can be used on local terminals, AWS Cloud9, EC2 instances, etc. 125 | 126 | #### iOS Development Requirements 127 | 128 | 1. Apple Developer Account: Required for signing and deploying Vision Pro / iOS applications. 129 | 2. Xcode: Latest version compatible with Vision Pro / iOS development. 130 | 3. Provisioning profiles and certificates for Vision Pro / iOS development. 131 | 132 | ##### Networking 133 | 134 | Ensure you have the necessary network access to create and manage AWS resources, including VPCs, subnets, and security groups. 135 | 136 | ##### Service Quotas 137 | 138 | Verify that your AWS account has sufficient service quotas, especially for: 139 | 140 | 141 | * Amazon EC2 instances (including Mac instances) 142 | * Amazon ECS tasks 143 | * VPCs and subnets 144 | * NAT Gateways 145 | 146 | 147 | 148 | ##### Security 149 | 150 | * Familiarity with AWS IAM for managing permissions and roles. 151 | * Understanding of network security concepts for configuring VPCs and security groups. 152 | 153 | ##### Knowledge Prerequisites 154 | 155 | * Basic understanding of Unity development for Apple Vision Pro / iOS. 156 | * Familiarity with AWS services, especially EC2, ECS, and S3. 157 | * Experience with CI/CD pipelines, preferably Jenkins. 158 | 159 | By ensuring these prerequisites are met, you'll be well-prepared to deploy and use this guidance effectively. 160 | 161 | 162 | ##### Preparing to Use CDK 163 | 164 | Navigate to the root directory of this prototype (the directory containing README.md) and execute the following commands. Note that all subsequent cdk commands are assumed to be executed in this root directory. 165 | 166 | ```shell 167 | # Install Node dependencies 168 | npm ci 169 | 170 | # Initialize CDK for use in your AWS environment 171 | 172 | npx cdk bootstrap 173 | ``` 174 | 175 | npm ci installs Node dependencies. This is only necessary the first time. 176 | cdk bootstrap performs initial setup to use CDK in your environment. This is necessary when using CDK for the first time in a particular AWS account and region. It's not needed for subsequent uses. 177 | ✅ If you see a message like "Environment aws://xxxx/ap-northeast-1 bootstrapped", it's successful. Proceed to the next step. 178 | 179 | ##### Supported AWS Regions 180 | 181 | The AWS services used for this guidance are supported in all AWS regions where Amazon EC2 Mac instances are available. Please check the AWS Regional Services List for the most up-to-date information. 182 | 183 | #### Deployment Steps 184 | 185 | We assume that you have already built a build pipeline, but for detailed instructions on how to build it, please refer to the following link: Build Pipeline CDK Project Deployment Steps. You can deploy using AWS CDK, and it only takes a few CLI commands to deploy. 186 | 187 | ##### 1. Set parameters 188 | 189 | Before you deploy it, you need to set several parameters. 190 | 191 | The Jenkins controller's initial admin password is set in [jenkins.yaml.ejs](lib/construct/jenkins/resources/config/jenkins.yaml.ejs). 192 | It is recommended to update the password to a sufficiently strong one (the default is passw0rd.) 193 | 194 | ```shell 195 | users: 196 | - id: admin 197 | password: passw0rd 198 | ``` 199 | 200 | Please open [bin/jenkins-unity-build.ts](./bin/jenkins-unity-build.ts). There are a few parameters you can configure. 201 | 202 | ```shell 203 | new JenkinsUnityBuildStack(app, 'JenkinsUnityBuildStack', { 204 | env: { 205 | region: 'us-east-2', 206 | // account: '123456789012', 207 | }, 208 | allowedCidrs: ['127.0.0.1/32'], 209 | // certificateArn: "", 210 | }); 211 | ``` 212 | The allowedCidrs property specifies IP address ranges that can access the Jenkins web UI ALB. 213 | You should set these ranges as narrowly as possible to prevent unwanted users from accessing your Jenkins UI. 214 | 215 | To change the AWS region (the default is us-east-2, Ohio), please replace region property. 216 | 217 | For additional security, you can create an AWS Certificate Manager certificate, and import it by setting certificateArn and env.account in the above code to encrypt the data transferred through the ALB with TLS. By default, Jenkins Web GUI is accessed via HTTP. 218 | 219 | ##### 2. Run cdk deploy 220 | 221 | After CDK bootstrapping, you are ready to deploy the CDK project by the following command: 222 | 223 | ```shell 224 | npx cdk deploy 225 | ``` 226 | The first deployment should take about 15 minutes. You can also use the npx cdk deploy command to deploy when you change your CDK templates in the future. 227 | 228 | After a successful deployment, you will get a CLI output as below: 229 | 230 | ```shell 231 | ✅ JenkinsUnityBuildStack 232 | 233 | ✨ Deployment time: 67.1s 234 | 235 | Outputs: 236 | JenkinsUnityBuildStack.JenkinsControllerServiceLoadBalancerDNS8A32739E = Jenki-Jenki-1234567890.us-east-2.elb.amazonaws.com 237 | JenkinsUnityBuildStack.JenkinsControllerServiceServiceURL6DCB4BEE = http://Jenki-Jenki-1234567890.us-east-2.elb.amazonaws.com 238 | ``` 239 | By opening the URL in JenkinsControllerServiceServiceURL output, you can now access to Jenkins Web GUI. Please login with the username and password you entered in jenkins.yaml.ejs. 240 | 241 | You can also configure to deploy EC2 Mac instances or a Unity accelerator instance. For further details, please refer to the document here. 242 | 243 | #### Deployment Validation 244 | 245 | To validate the deployment: 246 | 247 | 248 | 1. Open the CloudFormation console and verify the status of the stack named "JenkinsUnityBuildStack". 249 | 2. Check the ECS console to ensure the Jenkins master task is running. 250 | 3. Verify that the EC2 instances (Linux and Mac) are created and running. 251 | 4. Access the Jenkins UI using the URL provided in the CloudFormation outputs. 252 | 253 | 254 | 255 | ##### Running the Guidance 256 | 257 | To run the Guidance: 258 | 259 | 260 | 1. In the Jenkins UI, navigate to the pipeline you created. 261 | 2. Click "Build Now" to start the build process. 262 | 3. Monitor the build progress in the Jenkins console output. 263 | 4. Once complete, you can download the built artifacts from Jenkins. 264 | 265 | Expected output: A successfully built Xcode archive for your Vision Pro / iOS application. Please refer to the blog article for further details. 266 | 267 | If you want to test the pipeline with a sample Unity project including Jenkinsfile, you can use the following: 268 | 269 | * Vision Pro sample app: https://github.com/aws-samples/unity-vision-os-sample-app 270 | * iOS sample app: https://github.com/tmokmss/com.unity.multiplayer.samples.coop 271 | 272 | 273 | 274 | #### Next Steps 275 | 276 | * Customize the Unity project and build settings to fit your specific Vision Pro / iOS application needs. 277 | * Integrate additional testing steps in the Jenkins pipeline. 278 | * Implement automatic deployment to TestFlight or the App Store. 279 | * Optimize the build process by fine-tuning EC2 instance types and sizes. 280 | 281 | #### Cleanup 282 | 283 | When deleting resources, please follow these steps: 284 | First, set the number of instances in the Jenkins EC2 Fleet to 0. To do this, go to the Jenkins cloud management page (Dashboard → Manage Jenkins → Nodes → Configure Clouds) and set the Minimum Cluster Size of EC2 Fleet to 0. 285 | After confirming that all Linux/Windows Agents have been removed from the Jenkins GUI, execute the following CDK command: 286 | 287 | 288 | ```shell 289 | npx cdk destroy --all 290 | ``` 291 | 292 | Also, we've configured some resources not to be deleted automatically. Please follow these steps to delete them manually: 293 | 294 | 295 | * EC2 Mac Dedicated host: You cannot release the Dedicated host for 24 hours after creating it, and for 1-3 hours (Pending state) after terminating the Mac instance (reference). Please try after sufficient time has passed. To release, select the host on this page and click Actions → Release host. 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /docs/imgs/ebs-pool.svg: -------------------------------------------------------------------------------- 1 | 2 |
Pool of EBS volumes
<div><b>Pool of EBS volumes</b></div>
Auto Scaling Group
<div><b>Auto Scaling Group</b></div>
Attach any available volume
as a secondary volume
Attach any available volume<br>as a secondary volume
EC2 Instance
<div><b>EC2 Instance</b></div>
Detach the volume
when an instance is terminated
Detach the volume<br>when an instance is terminated
EC2 Instance
<div><b>EC2 Instance</b></div>

[Not supported by viewer]
Volume 2
[Not supported by viewer]
Volume 1
[Not supported by viewer]


Reserve N volumes
where N = maxSize of the ASG
[Not supported by viewer]
-------------------------------------------------------------------------------- /docs/imgs/ami-workflow.svg: -------------------------------------------------------------------------------- 1 | 2 |
No
No
Image.State == 'Ready' ?
<div><b>Image.State == 'Ready' ?</b></div>
InstanceId: i-xxx
InstanceId: i-xxx
Invoke
createAMI job
<b>Invoke</b><br>createAMI job
ImageId: ami-xxx
ImageId: ami-xxx
ec2.CreateImage
<b>ec2.CreateImage<br></b>
Image.State
Image.State
ec2.DescribeImage
<b>ec2.DescribeImage<br></b>
ec2.CreateLaunchTemplateVersion
<b>ec2.CreateLaunchTemplateVersion<br></b>
ImageId, LaunchTemplateId
<span>ImageId, LaunchTemplateId</span>
AutoScaling.DescribeAutoScalingGroups
<b>AutoScaling.DescribeAutoScalingGroups<br></b>
AutoScaling.UpdateAutoScalingGroup
<b>AutoScaling.UpdateAutoScalingGroup<br></b>
Finish
[Not supported by viewer]
AutoScaling.DetachInstances
<b>AutoScaling.DetachInstances<br></b>
Invoke
detachFromAsg job
<b>Invoke</b><br>detachFromAsg job
Image.State == 'Ready' ?
<div><b>Image.State == 'Ready' ?</b></div>
No
No
LastImage.CreatedAt is too old?
<div><b>LastImage.CreatedAt is too old?</b><br></div>
ec2.DescribeImage
<b>ec2.DescribeImage<br></b>
Finish
[Not supported by viewer]
No
No
Caches in local filesystem is updated?
<div><b>Caches in local filesystem is updated?</b></div>
Yes
Yes<br>
Yes
Yes<br>
Invoke createAmi job
<b>Invoke createAmi job<br></b>
No image creation is in progress?
<div><b>No image creation is in progress?</b></div>
No
No
Yes
Yes<br>
ec2.TerminateInstances
<b>ec2.TerminateInstances<br></b>
Yes
Yes
Trigger this job periodically
Trigger this job periodically
--------------------------------------------------------------------------------