src/providers/ec2.ts
Rama main · réplica de lectura del repositorio original.
import * as cdk from 'aws-cdk-lib';
import { aws_ec2 as ec2, aws_iam as iam, aws_logs as logs, aws_stepfunctions as stepfunctions, RemovalPolicy, Stack } from 'aws-cdk-lib';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';
import {
amiRootDevice,
Architecture,
BaseProvider,
IParameterizedRunnerProvider,
IRunnerProvider,
IRunnerProviderStatus,
Os,
providerParam,
RUNNER_INPUT,
RunnerAmi,
RunnerConfig,
runnerDefaultLabels,
RunnerProviderProps,
RunnerVersion,
StorageOptions,
} from './common';
import {
AwsImageBuilderRunnerImageBuilder,
BaseImage,
IRunnerImageBuilder,
RunnerImageBuilder,
RunnerImageBuilderProps,
RunnerImageBuilderType,
RunnerImageComponent,
} from '../image-builders';
import { ReservedTags } from '../lambda-common';
import { isGpuInstanceType, MINIMAL_EC2_SSM_SESSION_MANAGER_POLICY_STATEMENT } from '../utils';
// this script is specifically made so `poweroff` is absolutely always called
// each `{}` is a variable coming from `params` below
const linuxUserDataTemplate = `#!/bin/bash
trap 'sleep 10; poweroff' EXIT # give cloudwatch agent 10 seconds to upload logs and ALWAYS poweroff on any type of exit
set -x -o pipefail
echo -n "{}" > /tmp/task-token # aws cli reads --task-token from here so set -x doesn't log it
chmod 600 /tmp/task-token
logGroupName="{}"
runnerNamePath="{}"
githubDomainPath="{}"
ownerPath="{}"
repoPath="{}"
export ACTIONS_RUNNER_INPUT_JITCONFIG="{}" # run.sh reads --jitconfig from here so set -x doesn't log it
labels="{}"
export AWS_RETRY_MODE=standard # better retry
touch /var/log/runner.log
heartbeat () {
while true; do
SPOT_ACTION=$(curl -s -f -H "X-aws-ec2-metadata-token: $(curl -s -f -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 1" 2>/dev/null)" "http://169.254.169.254/latest/meta-data/spot/instance-action" 2>/dev/null) || true
if [ -n "$SPOT_ACTION" ]; then
aws stepfunctions send-task-failure --task-token file:///tmp/task-token --error SpotInterrupted --cause "EC2 Spot instance interruption: $SPOT_ACTION" || true
exit 0
fi
aws stepfunctions send-task-heartbeat --task-token file:///tmp/task-token
sleep 60
done
}
setup_logs () {
cat <<EOF > /tmp/log.conf || return 1
{
"logs": {
"log_stream_name": "unknown",
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/log/runner.log",
"log_group_name": "$logGroupName",
"log_stream_name": "$runnerNamePath",
"timezone": "UTC"
},
{
"file_path": "/var/log/workflow.log",
"log_group_name": "$logGroupName",
"log_stream_name": "$runnerNamePath-workflow",
"timezone": "UTC"
}
]
}
}
}
}
EOF
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/tmp/log.conf || return 2
}
action () {
# Report workflow id for stolen runner detection (to separate log so it doesn't hang this bash function)
/home/runner/job-reporter.sh "$runnerNamePath" /var/log/workflow.log
# Execute the run command
sudo --preserve-env=ACTIONS_RUNNER_INPUT_JITCONFIG,AWS_REGION -Hu runner /home/runner/run.sh || exit 2
# Retrieve the status
STATUS=$(grep -Phors "finish job request for job [0-9a-f-]+ with result: .*" /home/runner/_diag/ | tail -n1 | awk '{print $NF}')
# Check and print the job status
if [ -n "$STATUS" ]; then
echo CDKGHA JOB DONE "$labels" "$STATUS"
fi
}
heartbeat &
setup_logs
setup_logs_status=$?
if [ $setup_logs_status -ne 0 ]; then
aws stepfunctions send-task-failure --task-token file:///tmp/task-token --error Runner.SetupLogs.$setup_logs_status --cause "Failed to configure CloudWatch agent (exit $setup_logs_status), no runner log was uploaded"
elif action |& tee /var/log/runner.log; then
aws stepfunctions send-task-success --task-token file:///tmp/task-token --task-output '{"ok": true}' |& tee -a /var/log/runner.log
else
aws stepfunctions send-task-failure --task-token file:///tmp/task-token --error Runner.Error.$? --cause "Check CloudWatch for full log -- $logGroupName/$runnerNamePath -- $(tail -n 1 /var/log/runner.log)" |& tee -a /var/log/runner.log
fi
`.replace(/\\/g, '\\\\').replace(/{/g, '\\{').replace(/}/g, '\\}').replace(/\\{\\}/g, '{}');
// this script is specifically made so `poweroff` is absolutely always called
// each `{}` is a variable coming from `params` below and their order should match the linux script
const windowsUserDataTemplate = `<powershell>
$TASK_TOKEN = "{}"
$logGroupName="{}"
$runnerNamePath="{}"
$githubDomainPath="{}"
$ownerPath="{}"
$repoPath="{}"
$jitConfig="{}"
$labels="{}"
$Env:AWS_RETRY_MODE = "standard" # better retry
# EC2Launch only starts ssm agent after user data is done, so we need to start it ourselves (it is disabled by default)
Set-Service -StartupType Manual AmazonSSMAgent
Start-Service AmazonSSMAgent
$HeartbeatParentPid = $PID
Start-Job -ScriptBlock {
while ($true) {
try {
$spot = Invoke-RestMethod -Uri "http://169.254.169.254/latest/meta-data/spot/instance-action" -Headers @{"X-aws-ec2-metadata-token"=(Invoke-RestMethod -Method PUT -Uri "http://169.254.169.254/latest/api/token" -Headers @{"X-aws-ec2-metadata-token-ttl-seconds"="1"} -TimeoutSec 2)} -TimeoutSec 2
$spotJson = if ($spot -is [string]) { $spot } else { $spot | ConvertTo-Json -Compress }
aws stepfunctions send-task-failure --task-token "$using:TASK_TOKEN" --error SpotInterrupted --cause "EC2 Spot instance interruption: $spotJson"
break
} catch {
}
aws stepfunctions send-task-heartbeat --task-token "$using:TASK_TOKEN"
Start-Sleep -Seconds 60
}
}
function setup_logs () {
# the runner report can't go in runner.log: run.cmd's Out-File holds that file open for as long as the runner is
# running, which is exactly when the report is written. one file per stream, so it gets its own stream. the
# subscription filter is on the log group, so the detector doesn't care.
echo "{
\`"logs\`": {
\`"log_stream_name\`": \`"unknown\`",
\`"logs_collected\`": {
\`"files\`": {
\`"collect_list\`": [
{
\`"file_path\`": \`"/actions/runner.log\`",
\`"log_group_name\`": \`"$logGroupName\`",
\`"log_stream_name\`": \`"$runnerNamePath\`",
\`"timezone\`": \`"UTC\`"
},
{
\`"file_path\`": \`"/actions/workflow.log\`",
\`"log_group_name\`": \`"$logGroupName\`",
\`"log_stream_name\`": \`"$runnerNamePath-workflow\`",
\`"timezone\`": \`"UTC\`"
}
]
}
}
}
}" | Out-File -Encoding ASCII $Env:TEMP/log.conf
& "C:/Program Files/Amazon/AmazonCloudWatchAgent/amazon-cloudwatch-agent-ctl.ps1" -a fetch-config -m ec2 -s -c file:$Env:TEMP/log.conf
}
function action () {
cd /actions
& ./job-reporter.ps1 "\${runnerNamePath}" /actions/workflow.log 2>&1 | Out-File -Encoding ASCII -Append /actions/runner.log
./run.cmd --jitconfig "\${jitConfig}" 2>&1 | Out-File -Encoding ASCII -Append /actions/runner.log
if ($LASTEXITCODE -ne 0) { return 2 }
$STATUS = Select-String -Path './_diag/*.log' -Pattern 'finish job request for job [0-9a-f\\-]+ with result: (.*)' | %{$_.Matches.Groups[1].Value} | Select-Object -Last 1
if ($STATUS) {
echo "CDKGHA JOB DONE \${labels} $STATUS" | Out-File -Encoding ASCII -Append /actions/runner.log
}
return 0
}
try {
setup_logs
$setupLogsStatus = $LASTEXITCODE
if ($setupLogsStatus -ne 0) {
aws stepfunctions send-task-failure --task-token "$TASK_TOKEN" --error Runner.SetupLogs.$setupLogsStatus --cause "Failed to configure CloudWatch agent (exit $setupLogsStatus), no runner log was uploaded"
return
}
$r = action
if ($r -eq 0) {
aws stepfunctions send-task-success --task-token "$TASK_TOKEN" --task-output '{ }' 2>&1 | Out-File -Encoding ASCII -Append /actions/runner.log
} else {
$lastLine = Get-Content -Path C:/actions/runner.log -Tail 1 -ErrorAction SilentlyContinue
aws stepfunctions send-task-failure --task-token "$TASK_TOKEN" --error Runner.Error.$r --cause "Check CloudWatch for full log -- $logGroupName/$runnerNamePath -- $lastLine" 2>&1 | Out-File -Encoding ASCII -Append /actions/runner.log
}
} finally {
Start-Sleep -Seconds 10 # give cloudwatch agent its default 5 seconds buffer duration to upload logs
Stop-Computer -ComputerName localhost -Force
}
</powershell>
`.replace(/\\/g, '\\\\').replace(/{/g, '\\{').replace(/}/g, '\\}').replace(/\\{\\}/g, '{}');
/**
* Properties for {@link Ec2RunnerProvider} construct.
*/
export interface Ec2RunnerProviderProps extends RunnerProviderProps {
/**
* Runner image builder used to build AMI containing GitHub Runner and all requirements.
*
* The image builder determines the OS and architecture of the runner.
*
* @default Ec2RunnerProvider.imageBuilder()
*/
readonly imageBuilder?: IRunnerImageBuilder;
/**
* GitHub Actions labels used for this provider.
*
* These labels are used to identify which provider should spawn a new on-demand runner. Every job sends a webhook with the labels it's looking for
* based on runs-on. We match the labels from the webhook with the labels specified here. If all the labels specified here are present in the
* job's labels, this provider will be chosen and spawn a new runner.
*
* @default ['ec2']
*/
readonly labels?: string[];
/**
* GitHub Actions runner group name.
*
* If specified, the runner will be registered with this group name. Setting a runner group can help managing access to self-hosted runners. It
* requires a paid GitHub account and organization level runner registration.
*
* The group must exist or the runner will not start.
*
* Users will still be able to trigger this runner with the correct labels. But the runner will only be able to run jobs from repos allowed to use the group.
*
* @default undefined
*/
readonly group?: string;
/**
* Instance type for launched runner instances.
*
* For GPU instance types (g4dn, g5, p3, etc.), we automatically use a GPU base image (AWS Deep Learning AMI)
* with NVIDIA drivers pre-installed. If you provide your own image builder, use
* `baseAmi: BaseImage.fromGpuBase(os, architecture)` or another image preloaded with NVIDIA drivers, or use
* an image component to install NVIDIA drivers.
*
* @default m6i.large
*/
readonly instanceType?: ec2.InstanceType;
/**
* Size of volume available for launched runner instances. This modifies the boot volume size and doesn't add any additional volumes.
*
* @default 30GB
*/
readonly storageSize?: cdk.Size;
/**
* Options for runner instance storage volume.
*/
readonly storageOptions?: StorageOptions;
/**
* Security groups to assign to launched runner instances.
*
* @default a new security group
*/
readonly securityGroups?: ec2.ISecurityGroup[];
/**
* VPC where runner instances will be launched.
*
* @default default account VPC
*/
readonly vpc?: ec2.IVpc;
/**
* Where to place the network interfaces within the VPC. Only the first matched subnet will be used.
*
* @default default VPC subnet
*/
readonly subnetSelection?: ec2.SubnetSelection;
/**
* Use spot instances to save money. Spot instances are cheaper but not always available and can be stopped prematurely.
*
* @default false
*/
readonly spot?: boolean;
/**
* Set a maximum price for spot instances.
*
* @default no max price (you will pay current spot price)
*/
readonly spotMaxPrice?: string;
/**
* Additional tags to apply to launched runner instances and their volumes.
*
* These additional tags are set on top of `Name`, `GitHubRunners:Provider`, `GitHubRunners:Repo`, and `GitHubRunners:Labels`, and
* `GitHubRunners:Runner`. You may only override `Name`. Tags prefixed `GitHubRunners:` are reserved and rejected. Some of our reserved tags are
* used for house-keeping purposes.
*
* @default no additional tags
*/
readonly tags?: { [key: string]: string };
}
/**
* Runner config for the Ec2 family fragment.
*
* @internal
*/
interface Ec2RunnerConfig extends RunnerConfig {
readonly launchTemplateId: string;
readonly instanceType: string;
readonly securityGroupIds: string[];
readonly instanceProfileArn: string;
readonly logGroupName: string;
readonly blockDeviceMappings: any[];
readonly instanceMarketOptions: any;
readonly userDataTemplateIdx: number;
readonly subnet: string;
}
/**
* GitHub Actions runner provider using EC2 to execute jobs.
*
* This construct is not meant to be used by itself. It should be passed in the providers property for GitHubRunners.
*/
export class Ec2RunnerProvider extends BaseProvider implements IRunnerProvider, IParameterizedRunnerProvider {
/** @internal */
public static readonly _FAMILY = 'ec2';
/**
* Big static strings the EC2 fragment reads from `$.config`. We always include both userdata templates because
* the fragment puts both in a States.Array() and picks one by index at runtime.
*
* @internal
*/
public static _stateMachineConstants(): Record<string, string> {
return {
ec2UserDataLinux: linuxUserDataTemplate,
ec2UserDataWindows: windowsUserDataTemplate,
};
}
/**
* The fragment that runs any EC2 provider. Reads the config {@link _runnerConfig} generated for it from
* `$.providerParams`, and launches into whatever subnet is at `$.providerParams.subnet`.
*
* Multiple subnets are a chain of fallback configs instead of the chain of states we used to render, see
* {@link Ec2RunnerProvider._runnerConfig}.
*
* @internal
*/
public static _stateMachineFragment(scope: Construct): stepfunctions.IChainable {
const p = providerParam<Ec2RunnerConfig>;
return new stepfunctions.CustomState(scope, 'EC2 Runner', {
stateJson: {
Type: 'Task',
Resource: `arn:${cdk.Aws.PARTITION}:states:::aws-sdk:ec2:runInstances.waitForTaskToken`,
HeartbeatSeconds: cdk.Duration.minutes(10).toSeconds(),
Parameters: {
'LaunchTemplate': {
'LaunchTemplateId.$': p('launchTemplateId'),
},
'MinCount': 1,
'MaxCount': 1,
'InstanceType.$': p('instanceType'),
'UserData.$': 'States.Base64Encode(States.Format(' +
`States.ArrayGetItem(States.Array($ec2UserDataLinux, $ec2UserDataWindows), ${p('userDataTemplateIdx')}), ` +
`$$.Task.Token, ${p('logGroupName')}, ${RUNNER_INPUT.name}, ${RUNNER_INPUT.domain}, ${RUNNER_INPUT.owner}, ${RUNNER_INPUT.repo}, ` +
`${RUNNER_INPUT.jitConfig}, ${RUNNER_INPUT.labels}))`,
'InstanceInitiatedShutdownBehavior': 'terminate',
'IamInstanceProfile': {
'Arn.$': p('instanceProfileArn'),
},
'MetadataOptions': {
HttpTokens: 'required',
},
'SecurityGroupIds.$': p('securityGroupIds'),
'SubnetId.$': p('subnet'),
'BlockDeviceMappings.$': p('blockDeviceMappings'),
'InstanceMarketOptions.$': p('instanceMarketOptions'),
// the provider's tags, already merged with the standard runner tags by the orchestrator
'TagSpecifications': [
{ 'ResourceType': 'instance', 'Tags.$': p('tags') },
{ 'ResourceType': 'volume', 'Tags.$': p('tags') },
],
},
},
});
}
/**
* Create new image builder that builds EC2 specific runner images.
*
* You can customize the OS, architecture, VPC, subnet, security groups, etc. by passing in props.
*
* You can add components to the image builder by calling `imageBuilder.addComponent()`.
*
* The default OS is Ubuntu running on x64 architecture.
*
* Included components:
* * `RunnerImageComponent.requiredPackages()`
* * `RunnerImageComponent.cloudWatchAgent()`
* * `RunnerImageComponent.runnerUser()`
* * `RunnerImageComponent.git()`
* * `RunnerImageComponent.githubCli()`
* * `RunnerImageComponent.awsCli()`
* * `RunnerImageComponent.docker()`
* * `RunnerImageComponent.githubRunner()`
*/
public static imageBuilder(scope: Construct, id: string, props?: RunnerImageBuilderProps) {
return RunnerImageBuilder.new(scope, id, {
os: Os.LINUX_UBUNTU,
architecture: Architecture.X86_64,
builderType: RunnerImageBuilderType.AWS_IMAGE_BUILDER,
components: [
RunnerImageComponent.requiredPackages(),
RunnerImageComponent.cloudWatchAgent(),
RunnerImageComponent.runnerUser(),
RunnerImageComponent.git(),
RunnerImageComponent.githubCli(),
RunnerImageComponent.awsCli(),
RunnerImageComponent.docker(),
RunnerImageComponent.githubRunner(props?.runnerVersion ?? RunnerVersion.latest()),
],
...props,
});
}
/**
* Labels associated with this provider.
*/
readonly labels: string[];
/**
* Grant principal used to add permissions to the runner role.
*/
readonly grantPrincipal: iam.IPrincipal;
/**
* Log group where provided runners will save their logs.
*
* Note that this is not the job log, but the runner itself. It will not contain output from the GitHub Action but only metadata on its execution.
*/
readonly logGroup: logs.ILogGroup;
private readonly group?: string;
private readonly amiBuilder: IRunnerImageBuilder;
private readonly ami: RunnerAmi;
private readonly role: iam.Role;
private readonly instanceType: ec2.InstanceType;
private readonly storageSize: cdk.Size;
private readonly storageOptions?: StorageOptions;
private readonly spot: boolean;
private readonly spotMaxPrice: string | undefined;
private readonly vpc: ec2.IVpc;
private readonly subnets: ec2.ISubnet[];
private readonly securityGroups: ec2.ISecurityGroup[];
private readonly defaultLabels: boolean;
private readonly tags: { [key: string]: string };
private readonly instanceProfile: iam.CfnInstanceProfile;
private readonly rootDeviceResource: cdk.CustomResource;
constructor(scope: Construct, id: string, props?: Ec2RunnerProviderProps) {
super(scope, id, props);
this.labels = props?.labels ?? ['ec2'];
this.group = props?.group;
this.vpc = props?.vpc ?? ec2.Vpc.fromLookup(this, 'Default VPC', { isDefault: true });
this.securityGroups = props?.securityGroups ?? [new ec2.SecurityGroup(this, 'SG', { vpc: this.vpc })];
this.subnets = this.vpc.selectSubnets(props?.subnetSelection).subnets;
this.instanceType = props?.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.M6I, ec2.InstanceSize.LARGE);
this.storageSize = props?.storageSize ?? cdk.Size.gibibytes(30); // 30 is the minimum for Windows
this.storageOptions = props?.storageOptions;
this.spot = props?.spot ?? false;
this.spotMaxPrice = props?.spotMaxPrice;
this.defaultLabels = props?.defaultLabels ?? true;
this.tags = props?.tags ?? {};
// reserved, because cleanup terminates instances by a tag with this prefix
Object.keys(this.tags).filter(key => key.startsWith(ReservedTags.PREFIX)).forEach(tag => {
cdk.Annotations.of(this).addError(
`Tag "${tag}" can't be set: the "${ReservedTags.PREFIX}" prefix is reserved.`,
);
});
if (this.subnets.length === 0) {
cdk.Annotations.of(this).addError('At least one subnet is required');
}
const arch = this.instanceType.architecture === ec2.InstanceArchitecture.ARM_64 ? Architecture.ARM64 : Architecture.X86_64;
this.amiBuilder = props?.imageBuilder ?? Ec2RunnerProvider.imageBuilder(this, 'Ami Builder', {
vpc: props?.vpc,
subnetSelection: props?.subnetSelection,
securityGroups: this.securityGroups,
baseAmi: isGpuInstanceType(this.instanceType) ? BaseImage.fromGpuBase(Os.LINUX_UBUNTU, arch) : undefined,
architecture: arch,
awsImageBuilderOptions: {
instanceType: arch.is(Architecture.ARM64) ? ec2.InstanceType.of(ec2.InstanceClass.M6G, ec2.InstanceSize.LARGE) : undefined,
},
});
this.ami = this.amiBuilder.bindAmi();
if (this.amiBuilder instanceof AwsImageBuilderRunnerImageBuilder) {
if (this.amiBuilder.storageSize && this.storageSize.toBytes() < this.amiBuilder.storageSize.toBytes()) {
cdk.Annotations.of(this).addError(`Runner storage size (${this.storageSize.toGibibytes()} GiB) must be at least the same as the image builder storage size (${this.amiBuilder.storageSize.toGibibytes()} GiB)`);
}
}
if (!this.ami.architecture.instanceTypeMatch(this.instanceType)) {
cdk.Annotations.of(this).addError(`AMI architecture (${this.ami.architecture.name}) doesn't match runner instance type (${this.instanceType} / ${this.instanceType.architecture})`);
}
this.grantPrincipal = this.role = new iam.Role(this, 'Role', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
});
this.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['states:SendTaskFailure', 'states:SendTaskSuccess', 'states:SendTaskHeartbeat'],
resources: ['*'], // no support for stateMachine.stateMachineArn but task tokens are very long and totally random so not the end of the world
}));
this.grantPrincipal.addToPrincipalPolicy(MINIMAL_EC2_SSM_SESSION_MANAGER_POLICY_STATEMENT);
this.logGroup = new logs.LogGroup(
this,
'Logs',
{
retention: props?.logRetention ?? RetentionDays.ONE_MONTH,
removalPolicy: RemovalPolicy.DESTROY,
},
);
this.logGroup.grantWrite(this);
this.instanceProfile = new iam.CfnInstanceProfile(this, 'Instance Profile', {
roles: [this.role.roleName],
});
this.rootDeviceResource = amiRootDevice(this, this.ami.launchTemplate.launchTemplateId, this.ami.cacheKey);
if (Construct.isConstruct(this.amiBuilder)) {
// if the user didn't create a static image builder and it's a real construct, we need to wait for it
this.rootDeviceResource.node.addDependency(this.amiBuilder);
}
}
/**
* @internal
*/
_grantStateMachine(stateMachineRole: iam.IGrantable) {
// we use ec2:RunInstances because we must
// we can't use fleets because they don't let us override user data, security groups or even disk size
// we can't use requestSpotInstances because it doesn't support launch templates, and it's deprecated
// ec2:RunInstances also seemed like the only one to immediately return an error when spot capacity is not available
stateMachineRole.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['ec2:runInstances'],
resources: ['*'],
}));
stateMachineRole.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['iam:PassRole'],
resources: [this.role.roleArn],
conditions: {
StringEquals: {
'iam:PassedToService': 'ec2.amazonaws.com',
},
},
}));
stateMachineRole.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['ec2:createTags'],
resources: [Stack.of(this).formatArn({
service: 'ec2',
resource: '*',
})],
}));
stateMachineRole.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['iam:CreateServiceLinkedRole'],
resources: ['*'],
conditions: {
StringEquals: {
'iam:AWSServiceName': 'spot.amazonaws.com',
},
},
}));
}
/**
* Config for the shared EC2 fragment.
*
* ec2:RunInstances can only try one subnet at a time, so every subnet gets a config that falls back to the next
* one. Same chain we used to render as states.
*
* @internal
*/
_runnerConfig(): Ec2RunnerConfig {
const base = {
family: Ec2RunnerProvider._FAMILY,
provider: this.node.path,
launchTemplateId: this.ami.launchTemplate.launchTemplateId,
instanceType: this.instanceType.toString(),
securityGroupIds: this.securityGroups.map(sg => sg.securityGroupId),
instanceProfileArn: this.instanceProfile.attrArn,
logGroupName: this.logGroup.logGroupName,
// already in the shape ec2:runInstances wants, so the fragment can splice it in as-is
blockDeviceMappings: [{
DeviceName: this.rootDeviceResource.ref,
Ebs: {
DeleteOnTermination: true,
VolumeSize: this.storageSize.toGibibytes(),
VolumeType: this.storageOptions?.volumeType,
Iops: this.storageOptions?.iops,
Throughput: this.storageOptions?.throughput,
},
}],
// empty and not missing for on-demand
// the state reads this path no matter what, and a missing reference path fails at runtime
instanceMarketOptions: this.spot ? {
MarketType: 'spot',
SpotOptions: {
MaxPrice: this.spotMaxPrice,
SpotInstanceType: 'one-time',
},
} : {},
// index into States.Array($ec2UserDataLinux, $ec2UserDataWindows)
userDataTemplateIdx: this.ami.os.is(Os.WINDOWS) ? 1 : 0,
// always here, even when empty, because that's what opts us into the standard runner tags
// see selectProviderParams() in runner.ts, which merges them in at runtime
tags: Object.entries(this.tags).map(([Key, Value]) => ({ Key, Value })),
runnerGroup: this.group ?? '',
defaultLabels: runnerDefaultLabels(this.ami.os, this.ami.architecture, this.defaultLabels),
};
let config: any = undefined;
for (let i = this.subnets.length - 1; i >= 0; i--) {
config = {
...base,
subnet: this.subnets[i].subnetId,
...config ? { fallback: config } : {},
};
}
return config;
}
/**
* @internal
*/
_status(statusFunctionRole: iam.IGrantable): IRunnerProviderStatus {
statusFunctionRole.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['ec2:DescribeLaunchTemplateVersions'],
resources: ['*'],
}));
return {
type: this.constructor.name,
labels: this.labels,
constructPath: this.node.path,
securityGroups: this.securityGroups.map(sg => sg.securityGroupId),
roleArn: this.role.roleArn,
logGroup: this.logGroup.logGroupName,
ami: {
launchTemplate: this.ami.launchTemplate.launchTemplateId || 'unknown',
amiBuilderLogGroup: this.ami.logGroup?.logGroupName,
},
};
}
/**
* The network connections associated with this resource.
*/
public get connections(): ec2.Connections {
return new ec2.Connections({ securityGroups: this.securityGroups });
}
}