Develop a GenAI Solution with CDK CI/CD Workbench Part 2
In this section, we will continue developing the GenAI solution using the Workbench. We will create an AWS ECS Fargate task to run a Streamlit application that will host a Python web application to generate images using Amazon Bedrock.
Enhance the Solution
Step 1: Get your IP address
Before proceeding, we need to obtain your IP address to ensure that access to the application is restricted to only you.
-
Open the following URL: https://checkip.amazonaws.com/.
This will display your current IP address.
-
Note down your IP address, as you will need it later to configure access restrictions for the application.
Step 2: Create the Fargate Task Definition
We’ll start by enhancing the solution with an ECS Fargate task definition to run the Streamlit application.
- Open the file
lib/demo-stack.ts
and add the following code after the ECS Cluster definition:
const taskRole = new iam.Role(this, 'TaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
inlinePolicies: {
BedrockInvokeModel: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['bedrock:InvokeModel'],
resources: [`arn:aws:bedrock:${cdk.Aws.REGION}::foundation-model/amazon.titan-image-generator-v1`],
}),
],
}),
},
});
// Define a Fargate task definition
const fargateTaskDefinition = new ecs.FargateTaskDefinition(this, 'StreamlitAppTaskDef', {
taskRole,
});
// Add container to task definition
const container = fargateTaskDefinition.addContainer('StreamlitContainer', {
image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'image_prompts'), { networkMode: ecrAsset.NetworkMode.custom('sagemaker') }),
memoryLimitMiB: 512,
cpu: 256,
logging: new ecs.AwsLogDriver({ streamPrefix: 'streamlit-app' }),
});
// Expose the container on port 8501 (Streamlit default port)
container.addPortMappings({
containerPort: 8501,
protocol: ecs.Protocol.TCP,
});
// Create security group for ALB
const albSecurityGroup = new ec2.SecurityGroup(this, 'ALBSecurityGroup', {
vpc: cluster.vpc,
allowAllOutbound: true,
description: 'Security group for ALB with restricted ingress',
});
for (const ip of allowedIPs) {
albSecurityGroup.addIngressRule(
ec2.Peer.ipv4(ip),
ec2.Port.tcp(80),
`Allow HTTP from ${ip}`,
);
}
// Create Fargate service in ECS
const loadBalancer = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'StreamlitFargateService', {
cluster: cluster, // Required
taskDefinition: fargateTaskDefinition,
desiredCount: 1,
publicLoadBalancer: true,
openListener: false,
});
loadBalancer.loadBalancer.addSecurityGroup(albSecurityGroup);
nag.NagSuppressions.addResourceSuppressions(
loadBalancer,
[
{
id: 'AwsSolutions-ELB2',
reason: 'ELB access logs',
},
],
true,
);
nag.NagSuppressions.addResourceSuppressions(fargateTaskDefinition, [{
id: 'AwsSolutions-IAM5',
reason: 'Allow * permissions.',
appliesTo: ['Resource::*']
}], true);
- Add the following import statements at the top of the file:
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecrAsset from 'aws-cdk-lib/aws-ecr-assets';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as path from 'path';
import * as nag from 'cdk-nag';
- Add the following code to the
allowedIPs
array in thelib/demo-stack.ts
file:
const allowedIPs = ['<your-ip-address>/32'];
Congratulations! You've successfully defined the Fargate task to run the Streamlit app.
Step 3: Create the Streamlit Application
Let’s create the Streamlit application that will interact with Amazon Bedrock for generating images.
Step 3.1: Create the Docker Setup
-
Create a new directory called
image_prompts
in the lib folder. -
Create a new file called
Dockerfile
in theimage_prompts
folder. -
Add the following content to the
Dockerfile
:
FROM python:3.12-slim
# Set the working directory
WORKDIR /app
# Install pipenv to manage dependencies
RUN pip install pipenv
# Copy the Pipfile and Pipfile.lock first to leverage Docker cache
COPY Pipfile Pipfile.lock ./
# Install project dependencies from Pipfile
RUN pipenv install --system --deploy --ignore-pipfile
# Copy the rest of the application code
COPY . .
# Expose the port on which Streamlit runs
EXPOSE 8501
# Command to run Streamlit app
CMD ["streamlit", "run", "image_prompts_app.py", "--server.port", "8501", "--server.address", "0.0.0.0"]
Well done! The Docker setup for the Streamlit app is complete.
Step 3.2: Define Dependencies and Application Code
- Create a new file called
Pipfile
in theimage_prompts
folder. - Add the following content to the
Pipfile
:
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
streamlit = "*"
botocore = "*"
boto3 = "*"
[requires]
python_version = "3"
- Create a new file called
Pipfile.lock
in theimage_prompts
folder. Open a terminal and run the following command to generate thePipfile.lock
file:
cd lib/image_prompts && pipenv lock && cd ../..
Nice work! The dependencies for the Streamlit app have been set up.
Step 3.3: Add the Streamlit Application Logic
- Create a new file called
image_prompts_app.py
in theimage_prompts
folder. - Add the following content to
image_prompts_app.py
:
import streamlit as st
import image_prompts_lib as glib
st.set_page_config(layout="wide", page_title="Image Generation")
st.title("Image Generation")
col1, col2 = st.columns(2)
with col1:
st.subheader("Image parameters")
prompt_text = st.text_area("What you want to see in the image:", height=100, help="The prompt text")
negative_prompt = st.text_input("What shoud not be in the image:", help="The negative prompt")
generate_button = st.button("Generate", type="primary")
with col2:
st.subheader("Result")
if generate_button:
with st.spinner("Drawing..."):
generated_image = glib.get_image_from_model(
prompt_content=prompt_text,
negative_prompt=negative_prompt,
)
st.image(generated_image)
Awesome! The Streamlit application is ready to generate images.
Step 3.4: Create the Helper Library
- Create a new file called
image_prompts_lib.py
in theimage_prompts
folder. - Add the following content to
image_prompts_lib.py
:
import boto3
import json
import base64
from io import BytesIO
from random import randint
#get the stringified request body for the InvokeModel API call
def get_titan_image_generation_request_body(prompt, negative_prompt=None):
body = { #create the JSON payload to pass to the InvokeModel API
"taskType": "TEXT_IMAGE",
"textToImageParams": {
"text": prompt,
},
"imageGenerationConfig": {
"numberOfImages": 1, # Number of images to generate
"quality": "premium",
"height": 512,
"width": 512,
"cfgScale": 8.0,
"seed": randint(0, 100000), #nosec Use a random seed
},
}
if negative_prompt:
body['textToImageParams']['negativeText'] = negative_prompt
return json.dumps(body)
#get a BytesIO object from the Titan Image Generator response
def get_titan_response_image(response):
response = json.loads(response.get('body').read())
images = response.get('images')
image_data = base64.b64decode(images[0])
return BytesIO(image_data)
#generate an image using Amazon Titan Image Generator
def get_image_from_model(prompt_content, negative_prompt=None):
session = boto3.Session()
bedrock = session.client(service_name='bedrock-runtime') #creates a Bedrock client
body = get_titan_image_generation_request_body(prompt_content, negative_prompt=negative_prompt)
response = bedrock.invoke_model(body=body, modelId="amazon.titan-image-generator-v1", contentType="application/json", accept="application/json")
output = get_titan_response_image(response)
return output
Fantastic! The helper library is in place to interact with AWS Bedrock.
Step 4: Deploy the Workbench
Run the following command to deploy the Workbench environment:
npm run workbench deploy -- --all
Well done! The Workbench has been successfully deployed.
Step 5: Verify the Deployment
The deployment process may take a few minutes. Once it's complete, the LoadBalancer URL will be displayed in the terminal.
- Open the LoadBalancer URL in your browser to verify the deployment. The Streamlit application should be accessible.
Congratulations! Your application is up and running.
Introduce Feature Flag
Now, we need to introduce a feature flag to control whether the Streamlit application should be deployed. This allows us to work on the feature without immediately deploying it through the pipeline.
Step 6: Add the Feature Flag to Control the Streamlit App
-
Open the
lib/demo-stack.ts
file and wrap the Streamlit app logic inside a feature flag check. The feature flag allows us to selectively enable or disable the deployment of the Streamlit app:if (cdk.FeatureFlags.of(this).isEnabled('feature-streamlit')) { wrapper.logger.info('Feature Streamlit is enabled'); const taskRole = new iam.Role(this, 'TaskRole', { // ... Streamlit app definition }); nag.NagSuppressions.addResourceSuppressions(fargateTaskDefinition.executionRole!, [{ id: 'AwsSolutions-IAM5', reason: 'Allow * permissions.', appliesTo: ['Resource::*'] }], true); }
-
Import the CDK CI/CD Wrapper module at the top of the file:
import * as wrapper from '@cdklabs/cdk-cicd-wrapper';
Show Solution
The lib/demo-stack.ts
file should look like this:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecrAsset from 'aws-cdk-lib/aws-ecr-assets';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as path from 'path';
import * as nag from 'cdk-nag';
import * as wrapper from '@cdklabs/cdk-cicd-wrapper';
const allowedIPs = ['<your-ip-address>/32'];
export class DemoStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create an ECS Cluster
const cluster = new ecs.Cluster(this, 'DemoCluster', {
clusterName: cdk.Names.uniqueResourceName(this, {
maxLength: 50,
}),
enableFargateCapacityProviders: true,
containerInsights: true,
});
cluster.vpc.addFlowLog('demo-flow-log');
if (cdk.FeatureFlags.of(this).isEnabled('feature-streamlit')) {
wrapper.logger.info('Feature Streamlit is enabled');
const taskRole = new iam.Role(this, 'TaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
inlinePolicies: {
BedrockInvokeModel: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['bedrock:InvokeModel'],
resources: [`arn:aws:bedrock:${cdk.Aws.REGION}::foundation-model/amazon.titan-image-generator-v1`],
}),
],
}),
},
});
// Define a Fargate task definition
const fargateTaskDefinition = new ecs.FargateTaskDefinition(this, 'StreamlitAppTaskDef', {
taskRole,
});
// Add container to task definition
const container = fargateTaskDefinition.addContainer('StreamlitContainer', {
image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'image_prompts'), { networkMode: ecrAsset.NetworkMode.custom('sagemaker') }),
memoryLimitMiB: 512,
cpu: 256,
logging: new ecs.AwsLogDriver({ streamPrefix: 'streamlit-app' }),
});
// Expose the container on port 8501 (Streamlit default port)
container.addPortMappings({
containerPort: 8501,
protocol: ecs.Protocol.TCP,
});
// Create security group for ALB
const albSecurityGroup = new ec2.SecurityGroup(this, 'ALBSecurityGroup', {
vpc: cluster.vpc,
allowAllOutbound: true,
description: 'Security group for ALB with restricted ingress',
});
for (const ip of allowedIPs) {
albSecurityGroup.addIngressRule(
ec2.Peer.ipv4(ip),
ec2.Port.tcp(80),
`Allow HTTP from ${ip}`,
);
}
// Create Fargate service in ECS
const loadBalancer = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'StreamlitFargateService', {
cluster: cluster, // Required
taskDefinition: fargateTaskDefinition,
desiredCount: 1,
publicLoadBalancer: true,
openListener: false,
});
loadBalancer.loadBalancer.addSecurityGroup(albSecurityGroup);
nag.NagSuppressions.addResourceSuppressions(
loadBalancer,
[
{
id: 'AwsSolutions-ELB2',
reason: 'ELB access logs',
},
],
true,
);
nag.NagSuppressions.addResourceSuppressions(fargateTaskDefinition, [{
id: 'AwsSolutions-IAM5',
reason: 'Allow * permissions.',
appliesTo: ['Resource::*'],
}], true);
}
}
}
Great job! The Streamlit app is now controlled by a feature flag.
Step 7: Test the Feature Flag
Let’s test if the feature flag works as expected.
-
Run the following command to synthesize the CDK code without the feature flag:
npm run workbench synth
-
Notice there is no log about the Streamlit app in the console.
-
Now enable the feature flag by running this command:
npm run workbench synth -- --context feature-streamlit=true
-
You should see a log message indicating that the Streamlit app feature is enabled:
[Info at /sagemaker-user-cdk-cicd-example] Feature Streamlit is enabled
Fantastic! The feature flag works as expected.
Step 8: Enable the Feature Flag by Default in Workbench
If we want to enable the feature flag by default in the Workbench environment, we can set it in the pipeline configuration.
-
Open the
bin/cdk-cicd-example.ts
file and update the Workbench configuration:.workbench({ provide(context) { context.scope.node.setContext('feature-streamlit', true); new DemoStack(context.scope, 'DemoStack', { env: context.environment }); }, })
Well done! The feature flag is now enabled by default in the Workbench.
Show Solution
The bin/cdk-cicd-example.ts
file should look like this:
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import * as wrapper from '@cdklabs/cdk-cicd-wrapper';
import { DemoStack } from '../lib/demo-stack';
import { DemoProvider } from './demo-provider';
const app = new cdk.App();
wrapper.PipelineBlueprint.builder()
.defineStages([
{ stage: wrapper.Stage.RES, account: process.env.AWS_ACCOUNT_ID },
{ stage: wrapper.Stage.DEV, account: process.env.AWS_ACCOUNT_ID },
])
.workbench({
provide(context) {
context.scope.node.setContext('feature-streamlit', true);
new DemoStack(context.scope, 'DemoStack', { env: context.environment });
},
})
.addStack(new DemoProvider())
.synth(app);
Step 9: Commit and Push the Changes
-
Run the following command to fix any linting issues:
npm run lint -- --fix
-
Run the following commands to update the license and validate the package.json:
npm run audit:license -- --fix npm run validate -- --fix
-
Add the changes to Git:
git add .
-
Commit the changes with a meaningful message:
git commit -m "feat: add feature flag for Streamlit"
-
Push the changes to the repository:
git push
You’ve successfully prepared the Streamlit app for deployment using a feature flag.
Click Next to continue to the next section.