Deploy to EC2 from S3 with CDK

I had a need to use CDK Pipelines to deploy an EC2 service. The code for the pipeline is part of the app repo. In building the construct I quickly ran into some issues:

This makes things tricky. Do I create a shared SSH keypair and add it to the repo (and put the private key somewhere)? Do I use Personal Access Tokens (my least preferred option because of the "blast radius")? Do I make the repo public?

All I wanted was to use a source artifact as my source for the EC2 instance. Fortunately I found an initial workaround here: Bootstrapping an Amazon EC2 instance using user-data to run a Python web app. Not my goal, but an insight to a solution!

The steps:

Just remember to allow the EC2 instance to read the bucket.

Okay! Lets do this...

// cdk/lib/my-stack.ts
// This assumes you already created an `aws-cdk-lib/aws-ec2` Instance object.
// Also, this is CDKv2.

import {
// ...
aws_ec2 as ec2,
aws_s3_assets as s3assets,
// ...
} from 'aws-cdk-lib';
import * as path from 'path';
import { Construct } from 'constructs';

export class MyStack extends ec2.Stack {
constructor(scope: Construct, id: string, props: ec2.StackProps) {
super(scope, id, props);

// ... above create an object called ec2Instance.

// Some files should be excluded... .gitignore is a good source.
const excludePattern = [ "node_modules", "dist", ".cdk.staging", "cdk.out" ];

// Step 1: Lets upload the project. Path is relative to this file.
const projectAsset = new s3assets.Asset(this, "ProjectFiles", {
path: path.join(__dirname, "../.."),
exclude: excludePattern
// Allow EC2 to access the asset.

// Step 2: Download the project zip file on the instance and get the reference.
const projectZipFilePath = ec2Instance.userData.addS3DownloadCommand({
bucket: projectAsset.bucket,
bucketKey: projectAsset.s3ObjectKey,

// Step 3: Upload the configuration user script. Path is relative to this file.
const configScriptAsset = new s3assets.Asset(this, "ProjectInstanceConfig", {
path: path.join(__dirname, ""),
// Allow EC2 instance to read the file

// Step 4: Download the project config file get the reference.
const configScriptFilePath = ec2Instance.userData.addS3DownloadCommand({
bucket: configScriptAsset.bucket,
bucketKey: configScriptAsset.s3ObjectKey,

// Step 5: Execute the script on the instance passing in the zip reference.
filePath: configScriptFilePath,
arguments: projectZipFilePath,

// ... rest of the stack.

And now the config file...

#!/bin/bash -xe

# Read the first parameter into $PROJECT_ZIP
if [[ "$1" != "" ]]; then
echo "No project to deploy."
exit 1

# Instance apps and dependencies.
yum update -y
yum groupinstall -y "Development Tools"

# Extract the project zip at desired location.
mkdir -p /opt/the-project
cp $PROJECT_ZIP /opt/the-project/
cd /opt/the-project
unzip -o

# Any other commands you want to execute.

Hope this was as useful to you as it was for me.

[ "$1" != "" ]: -$1-= " "$1" != "" " [//end]: # "Autogenerated link references"