Aws-cdk: Support MIME multipart format in UserData

Created on 2 Jun 2020  路  3Comments  路  Source: aws/aws-cdk

Support generation of MIME multipart format user data scripts via the CDK UserData class/implementations.

Use Case

We are using user data (via launch templates) to configure our AWS Batch instances. This has to be in MIME multi-part archive format as stated here: https://docs.aws.amazon.com/batch/latest/userguide/launch-templates.html

However, that format does not seem to be supported by the current UserData implementations. We currently end up using a combination of CustomUserData to define the MIME multi-part format and LinuxUserData with addS3DownloadCommand to pull in Assets (reusable parts of the user data).

This quickly gets and looks messy.

Proposed Solution

Add support for properly MIME multi-part archive formatted user data that can be used in LaunchTemplates

Other

Note that we are quite new to AWS CDK, so if we have missed something or there are alternative better approaches, we'd be more than happy to hear about it!

Our current approach looks something like this:

umccrise_wrapper_asset = assets.Asset(
    self,
    'UmccriseWrapperAsset',
    path=os.path.join(dirname, '..', 'assets', "umccrise-wrapper.sh")
)
umccrise_wrapper_asset.grant_read(batch_instance_role)

user_data_asset = assets.Asset(
    self,
    'UserDataAsset',
    path=os.path.join(dirname, '..', 'assets', "batch-user-data.sh")
)
user_data_asset.grant_read(batch_instance_role)

user_data = ec2.UserData.for_linux()
local_path = user_data.add_s3_download_command(
    bucket=user_data_asset.bucket,
    bucket_key=user_data_asset.s3_object_key
)
user_data.add_execute_file_command(
    file_path=local_path,
    arguments=f"s3://{umccrise_wrapper_asset.bucket.bucket_name}/{umccrise_wrapper_asset.s3_object_key}"
)

# create wrapper for MIME multi-part archive format
user_data_wrapper = ec2.UserData.custom('MIME-Version: 1.0')
user_data_wrapper.add_commands('Content-Type: multipart/mixed; boundary="==MYBOUNDARY=="')
user_data_wrapper.add_commands('')
user_data_wrapper.add_commands('--==MYBOUNDARY==')
user_data_wrapper.add_commands('Content-Type: text/x-shellscript; charset="us-ascii"')
user_data_wrapper.add_commands('')
user_data_wrapper.add_commands(user_data.render())
user_data_wrapper.add_commands('--==MYBOUNDARY==--')

launch_template = ec2.CfnLaunchTemplate(
    self,
    'UmccriseBatchComputeLaunchTemplate',
    launch_template_name='UmccriseBatchComputeLaunchTemplate',
    launch_template_data={
        'userData': core.Fn.base64(user_data_wrapper.render())
    }
)


This is a :rocket: Feature Request

@aws-cdaws-ec2 efformedium feature-request p2

Most helpful comment

I just thought this is a common/valid enough use case (as that format is required by LaunchTemplates) to perhaps think about supporting it within the framework rather than letting each end user having to build their own solution.

There are already 3 implementations of UserData, one doing nothing more than joining strings with '\n'. If that's supported, why not a MIME wrapper, which has more potential for screwing things up and therefore would benefit from a default implementation?

I may miss the point here and I am not an expert on the topic (hence would benefit from a solution that helps me deal with it), so would be happy to hear about reasons this should not be included.

All 3 comments

You're on the right track, but you probably want to define your own MimeUserData class (rather than filling an existing UserData class with MIME content).

I just thought this is a common/valid enough use case (as that format is required by LaunchTemplates) to perhaps think about supporting it within the framework rather than letting each end user having to build their own solution.

There are already 3 implementations of UserData, one doing nothing more than joining strings with '\n'. If that's supported, why not a MIME wrapper, which has more potential for screwing things up and therefore would benefit from a default implementation?

I may miss the point here and I am not an expert on the topic (hence would benefit from a solution that helps me deal with it), so would be happy to hear about reasons this should not be included.

I'm a bit interested in this change, as I would like to use it to configure AWS Batch (AWS Batch requires UserData to be in MultiPart format).

Can I propose following factoring:

/**
 * The single part of data, which can be added to {@link MultipartUserData}.
 */
export class UserDataMimePart {
  /** The body of this MIME part. */
  public readonly body: string;

  /** Represents `Content-Type` header of this part */
  public readonly contentType: string;
}

/**
 * Interface indicating that a class can produce {@link UserDataMimePart}.
 */
export interface IMultipartUserDataProducer {
  /**
   * Called to create the {@link UserDataMimePart}.
   */
  renderAsMimePart(): UserDataMimePart;
}

/**
 * Mime multipart user data.
 */
export class MultipartUserData extends UserData {
  public addPart(producer: IMultipartUserDataProducer): this {}

  public addRawPart(part: UserDataMimePart): this {}
}

Plus changes to existing UserData classes

class LinuxUserData extends UserData implements IMultipartUserDataProducer 

In order to use MultipartUserData users will have to:

  1. Create instance of MultipartUserData
  2. Add the existing UserData as part to MultipartUserData

The CDK will:

  1. Generate raw parts by calling IMultipartUserDataProducer.renderAsMimePart
  2. Add MIME header and concatenate parts with MIME separator (boundary)

For Linux user data raw part will look like

{
contentType: 'text/x-shellscript',
body: '#!/bin/bash\necho Hello world'
}

With this factoring users still will be able to use very useful, current implementations of UserData, and feature will be backward compatible. From the other hand, there still will be fallback path to build arbitrary part by directly constructing MultipartUserData and adding it MultipartUserData?

Was this page helpful?
0 / 5 - 0 ratings