Skip to content

Commit

Permalink
Merge branch 'main' into signpayload
Browse files Browse the repository at this point in the history
  • Loading branch information
AMZN-hgoffin authored Mar 21, 2023
2 parents 97b00c4 + 413b643 commit 91b2955
Show file tree
Hide file tree
Showing 21 changed files with 631 additions and 198 deletions.
42 changes: 42 additions & 0 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,48 @@ new ec2.Instance(this, 'Instance', {
});
```

`InitCommand` can not be used to start long-running processes. At deploy time,
`cfn-init` will always wait for the process to exit before continuing, causing
the CloudFormation deployment to fail because the signal hasn't been received
within the expected timeout.

Instead, you should install a service configuration file onto your machine `InitFile`,
and then use `InitService` to start it.

If your Linux OS is using SystemD (like Amazon Linux 2 or higher), the CDK has
helpers to create a long-running service using CFN Init. You can create a
SystemD-compatible config file using `InitService.systemdConfigFile()`, and
start it immediately. The following examples shows how to start a trivial Python
3 web server:

```ts
declare const vpc: ec2.Vpc;
declare const instanceType: ec2.InstanceType;

new ec2.Instance(this, 'Instance', {
vpc,
instanceType,
machineImage: ec2.MachineImage.latestAmazonLinux({
// Amazon Linux 2 uses SystemD
generation: ec2.AmazonLinuxGeneration: AMAZON_LINUX_2,
}),

init: ec2.CloudFormationInit.fromElements([
// Create a simple config file that runs a Python web server
ec2.InitService.systemdConfigFile('simpleserver', {
command: '/usr/bin/python3 -m http.server 8080',
cwd: '/var/www/html',
}),
// Start the server using SystemD
ec2.InitService.enable('simpleserver', {
serviceManager: ec2.ServiceManager.SYSTEMD,
}),
// Drop an example file to show the web server working
ec2.InitFile.fromString('/var/www/html/index.html', 'Hello! It\'s working!'),
]),
});
```

You can have services restarted after the init process has made changes to the system.
To do that, instantiate an `InitServiceRestartHandle` and pass it to the config elements
that need to trigger the restart and the service itself. For example, the following
Expand Down
138 changes: 136 additions & 2 deletions packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,16 @@ export interface InitServiceOptions {
* @default - No files trigger restart
*/
readonly serviceRestartHandle?: InitServiceRestartHandle;

/**
* What service manager to use
*
* This needs to match the actual service manager on your Operating System.
* For example, Amazon Linux 1 uses SysVinit, but Amazon Linux 2 uses Systemd.
*
* @default ServiceManager.SYSVINIT for Linux images, ServiceManager.WINDOWS for Windows images
*/
readonly serviceManager?: ServiceManager;
}

/**
Expand All @@ -806,6 +816,39 @@ export class InitService extends InitElement {
return new InitService(serviceName, { enabled: false, ensureRunning: false });
}

/**
* Install a systemd-compatible config file for the given service
*
* This is a helper function to create a simple systemd configuration
* file that will allow running a service on the machine using `InitService.enable()`.
*
* Systemd allows many configuration options; this function does not pretend
* to expose all of them. If you need advanced configuration options, you
* can use `InitFile` to create exactly the configuration file you need
* at `/etc/systemd/system/${serviceName}.service`.
*/
public static systemdConfigFile(serviceName: string, options: SystemdConfigFileOptions): InitFile {
if (!options.command.startsWith('/')) {
throw new Error(`SystemD executables must use an absolute path, got '${options.command}'`);
}

const lines = [
'[Unit]',
...(options.description ? [`Description=${options.description}`] : []),
...(options.afterNetwork ?? true ? ['After=network.target'] : []),
'[Service]',
`ExecStart=${options.command}`,
...(options.cwd ? [`WorkingDirectory=${options.cwd}`] : []),
...(options.user ? [`User=${options.user}`] : []),
...(options.group ? [`Group=${options.user}`] : []),
...(options.keepRunning ?? true ? ['Restart=always'] : []),
'[Install]',
'WantedBy=multi-user.target',
];

return InitFile.fromString(`/etc/systemd/system/${serviceName}.service`, lines.join('\n'));
}

public readonly elementType = InitElementType.SERVICE.toString();

private constructor(private readonly serviceName: string, private readonly serviceOptions: InitServiceOptions) {
Expand All @@ -814,11 +857,12 @@ export class InitService extends InitElement {

/** @internal */
public _bind(options: InitBindOptions): InitElementConfig {
const serviceManager = options.platform === InitPlatform.LINUX ? 'sysvinit' : 'windows';
const serviceManager = this.serviceOptions.serviceManager
?? (options.platform === InitPlatform.LINUX ? ServiceManager.SYSVINIT : ServiceManager.WINDOWS);

return {
config: {
[serviceManager]: {
[serviceManagerToString(serviceManager)]: {
[this.serviceName]: {
enabled: this.serviceOptions.enabled,
ensureRunning: this.serviceOptions.ensureRunning,
Expand Down Expand Up @@ -970,3 +1014,93 @@ function standardS3Auth(role: iam.IRole, bucketName: string) {
},
};
}

/**
* The service manager that will be used by InitServices
*
* The value needs to match the service manager used by your operating
* system.
*/
export enum ServiceManager {
/**
* Use SysVinit
*
* This is the default for Linux systems.
*/
SYSVINIT,

/**
* Use Windows
*
* This is the default for Windows systems.
*/
WINDOWS,

/**
* Use systemd
*/
SYSTEMD,
}

function serviceManagerToString(x: ServiceManager): string {
switch (x) {
case ServiceManager.SYSTEMD: return 'systemd';
case ServiceManager.SYSVINIT: return 'sysvinit';
case ServiceManager.WINDOWS: return 'windows';
}
}

/**
* Options for creating a SystemD configuration file
*/
export interface SystemdConfigFileOptions {
/**
* The command to run to start this service
*/
readonly command: string;

/**
* The working directory for the command
*
* @default Root directory or home directory of specified user
*/
readonly cwd?: string;

/**
* A description of this service
*
* @default - No description
*/
readonly description?: string;

/**
* The user to execute the process under
*
* @default root
*/
readonly user?: string;

/**
* The group to execute the process under
*
* @default root
*/
readonly group?: string;

/**
* Keep the process running all the time
*
* Restarts the process when it exits for any reason other
* than the machine shutting down.
*
* @default true
*/
readonly keepRunning?: boolean;

/**
* Start the service after the networking part of the OS comes up
*
* @default true
*/
readonly afterNetwork?: boolean;
}
45 changes: 45 additions & 0 deletions packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,51 @@ describe('InitService', () => {
});
});

test('can request systemd service', () => {
// WHEN
const service = ec2.InitService.enable('httpd', {
serviceManager: ec2.ServiceManager.SYSTEMD,
});

// THEN
const bindOptions = defaultOptions(InitPlatform.LINUX);
const rendered = service._bind(bindOptions).config;

// THEN
expect(rendered.systemd).toEqual({
httpd: {
enabled: true,
ensureRunning: true,
},
});
});

test('can create simple systemd config file', () => {
// WHEN
const file = ec2.InitService.systemdConfigFile('myserver', {
command: '/start/my/service',
cwd: '/my/dir',
user: 'ec2-user',
group: 'ec2-user',
description: 'my service',
});

// THEN
const bindOptions = defaultOptions(InitPlatform.LINUX);
const rendered = file._bind(bindOptions).config;
expect(rendered).toEqual({
'/etc/systemd/system/myserver.service': expect.objectContaining({
content: expect.any(String),
}),
});

const capture = rendered['/etc/systemd/system/myserver.service'].content;
expect(capture).toContain('ExecStart=/start/my/service');
expect(capture).toContain('WorkingDirectory=/my/dir');
expect(capture).toContain('User=ec2-user');
expect(capture).toContain('Group=ec2-user');
expect(capture).toContain('Description=my service');
});
});

describe('InitSource', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-elasticloadbalancing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@aws-cdk/cdk-build-tools": "0.0.0",
"@aws-cdk/integ-runner": "0.0.0",
"@aws-cdk/integ-tests": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/cfn2ts": "0.0.0",
"@aws-cdk/pkglint": "0.0.0",
"@types/jest": "^27.5.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "29.0.0",
"version": "31.0.0",
"files": {
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
"source": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"version": "29.0.0",
"version": "31.0.0",
"files": {
"11ca0111a871a53be970c5db0c5a24d4146213fd59f6d172b6fc1bc3de206cf9": {
"c8ab3e4e4503281b1f7df3028abab9a0ca3738640d31201b5118a18aaa225eab": {
"source": {
"path": "aws-cdk-elb-instance-target-integ.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "11ca0111a871a53be970c5db0c5a24d4146213fd59f6d172b6fc1bc3de206cf9.json",
"objectKey": "c8ab3e4e4503281b1f7df3028abab9a0ca3738640d31201b5118a18aaa225eab.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down
Loading

0 comments on commit 91b2955

Please sign in to comment.