├── .dockerignore ├── .env.example ├── .env.example.zip ├── .gitignore ├── CdkImageGenerator ├── .eslintignore ├── .eslintrc.js ├── .npmignore ├── README.md ├── bin │ └── app.ts ├── cdk.json ├── cdk.json.example ├── jest.config.js ├── lib │ ├── ecs.ts │ ├── index.ts │ └── vpc.ts ├── package.json ├── test │ └── cdk_image_generator.test.ts └── tsconfig.json ├── Dockerfile ├── LICENSE ├── README.md ├── kubernetes-manifests ├── apisecret.yaml ├── application.yaml └── namespace.yaml ├── package.json ├── pages ├── api │ └── generate.js ├── index.js └── index.module.css └── public ├── dog.png └── index.png /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.env* 2 | **/.git 3 | **/.gitignore 4 | **/.dockerignore 5 | **/node_modules/ 6 | **/kubernetes-manifests/ 7 | **/README.md 8 | **/package-lock.json 9 | **/.next -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /.env.example.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youngjeong46/OpenAIImageGenerator/ce34e0868988926d2126986bb32d8e87d26ad806/.env.example.zip -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/cdk.out/ 3 | .env 4 | .env.zip 5 | .next/ 6 | package-lock.json 7 | cdk.context.json 8 | -------------------------------------------------------------------------------- /CdkImageGenerator/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | cdk.out 3 | jest.config.js 4 | .eslintrc.js -------------------------------------------------------------------------------- /CdkImageGenerator/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | rules: { 12 | "@typescript-eslint/no-explicit-any": "off", 13 | "@typescript-eslint/explicit-module-boundary-types": "off", 14 | "@typescript-eslint/no-non-null-assertion": "off", 15 | "@typescript-eslint/no-unused-vars": [1, {"argsIgnorePattern": "^_"}], 16 | "prefer-const": "off", 17 | "semi": [1,"always"] 18 | }, 19 | }; -------------------------------------------------------------------------------- /CdkImageGenerator/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /CdkImageGenerator/README.md: -------------------------------------------------------------------------------- 1 | # CDK Project for Image Generator 2 | This is the CDK project that will deploy the Image Generator application. It will deploy the application, along with AWS Distro for OpenTelemetry (ADOT) containers for observability of your application, on Amazon Elastic Container Service (ECS). ECS Service is ran using Fargate, a Serverless compute option. 3 | 4 | ## Prerequisites 5 | To get started you will need the following: 6 | - [AWS CDK](https://docs.docker.com/install/) installed on your local machine, version 2.51.1 as of this writing. 7 | - [awscli](https://aws.amazon.com/cli/) installed and configured with `aws configure`, version 2.7.35 or later. 8 | - [Node](https://nodejs.org/en/) and [NPM](https://www.npmjs.com/) 9 | - zip your `.env` file in the top directory and name it `.env.zip` 10 | - A SecretsManager secret holding your Docker credentials. This is optional, but helpful to increase the pull limit for the Docker Hub hosted images. See [here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html#private-auth-enable) for the Secret format to store your Docker credentials. ***This must be stored in the same account/region as the CDK project.*** 11 | 12 | ## Project Components 13 | This CDK project will deploy the following resources: 14 | - A VPC with user-provided CIDR block (in its own stack) 15 | - S3 Bucket that stores the environment variable (i.e. your OpenAI API key) 16 | - ECS Cluster, Task Definition with your application container, and containers needed for Amazon Distro for OpenTelemetry (ADOT) 17 | - IAM roles and policies needed for the ECS Cluster and Task Definition 18 | - ECS Service within the ECS Cluster created using the Task Definition, and 19 | - Load Balancer fronting the ECS Service with DNS provided as output 20 | 21 | ## Deploying 22 | 23 | To get started, first install your dependencies: 24 | 25 | ```sh 26 | npm i 27 | ``` 28 | 29 | Looking at your `bin/app.ts`, you will notice some parameters you can set: 30 | - env: You can set your account and regions (by default they are CDK-default based on your AWS profile) 31 | - id: Your CDK application ID 32 | - cidrBlock: CIDR block for your VPC which hosts the ECS Fargate 33 | - enableDockerAuth: To use your docker credentials, set this to True. 34 | - dockerSecretName: Name of your secrets in Secrets Manager that stores your docker credentials. 35 | 36 | Once finished, save and then list all available CDK Stacks by running the following command: 37 | 38 | ```sh 39 | cdk list 40 | ``` 41 | 42 | You should get an output like the following: 43 | 44 | ``` 45 | cdk-image-gen-app-vpc-stack 46 | cdk-image-gen-app-ecs-stack 47 | ``` 48 | 49 | You will need both stacks deployed to be able to access the application. run the following command, and enter `y` when prompted to deploy both stacks: 50 | 51 | ```sh 52 | cdk deploy --all 53 | ``` 54 | 55 | Once deployed, at the end of the ECS stack deployment, you will get an Output printed on your Terminal (similar to the below) that shows the DNS for the load balancer, which you can use to access the application page: 56 | 57 | ``` 58 | Outputs: 59 | cdk-image-gen-app-ecs-stack.loadbalanceroutput = cdk-i-XXXXX-AWEBWEBQA-2309480930.us-west-2.elb.amazonaws.com 60 | ``` -------------------------------------------------------------------------------- /CdkImageGenerator/bin/app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | 5 | import ImageGeneratorConstruct from '../lib/index'; 6 | 7 | const app = new cdk.App(); 8 | 9 | const account = process.env.CDK_DEFAULT_ACCOUNT!; 10 | const region = process.env.CDK_DEFAULT_REGION!; 11 | const env = { 12 | account: account, 13 | region: region, 14 | }; 15 | 16 | new ImageGeneratorConstruct(app, 17 | { 18 | id: 'cdk-image-gen-app', 19 | cidrBlock: '128.46.0.0/16', 20 | enableDockerAuth: true, 21 | dockerSecretName: 'dockerSecret', 22 | }, 23 | { env } 24 | ); -------------------------------------------------------------------------------- /CdkImageGenerator/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/app.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CdkImageGenerator/cdk.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/app.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CdkImageGenerator/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | } 7 | }; -------------------------------------------------------------------------------- /CdkImageGenerator/lib/ecs.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 4 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 5 | import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 6 | import * as iam from 'aws-cdk-lib/aws-iam'; 7 | import * as logs from 'aws-cdk-lib/aws-logs'; 8 | import * as secrets from 'aws-cdk-lib/aws-secretsmanager'; 9 | import * as s3 from 'aws-cdk-lib/aws-s3'; 10 | import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment'; 11 | import * as path from 'path'; 12 | 13 | export interface EcsStackProps { 14 | vpc: ec2.Vpc, 15 | enableDockerAuth: boolean 16 | secretName: string 17 | } 18 | 19 | export class EcsStack extends cdk.Stack { 20 | constructor(scope: Construct, id: string, stackProps: EcsStackProps, props?: cdk.StackProps) { 21 | super(scope, id, props); 22 | 23 | const vpc = stackProps.vpc; 24 | const docker = stackProps.enableDockerAuth; 25 | const secret = stackProps.secretName; 26 | 27 | // Creates S3 bucket and uploads the env file to be used. 28 | const envFileBucket = new s3.Bucket(this, 'env-file-bucket',{ 29 | autoDeleteObjects: true, 30 | removalPolicy: cdk.RemovalPolicy.DESTROY 31 | }); 32 | new s3deploy.BucketDeployment(this, 'deploy-env-file', { 33 | sources: [s3deploy.Source.asset(path.join(__dirname,'../../.env.zip'))], 34 | destinationBucket: envFileBucket 35 | }); 36 | 37 | // Retrieve the Docker credential secret; 38 | let dockerSecret; 39 | if (docker) { 40 | dockerSecret = secrets.Secret.fromSecretNameV2(this, 'docker-secret', secret); 41 | } 42 | 43 | const dockerCredential = { credentials: dockerSecret } || {}; 44 | 45 | // Set up ADOT Roles 46 | const [adotRole, adotExecutionRole] = this.setUpAdotRoles(docker, dockerSecret, envFileBucket); 47 | 48 | // Create the ECS Cluster 49 | const cluster = new ecs.Cluster(this, id, { 50 | enableFargateCapacityProviders: true, 51 | vpc: stackProps.vpc, 52 | containerInsights: true, 53 | }); 54 | 55 | // ECS task definition 56 | const def = new ecs.FargateTaskDefinition( 57 | this, id + '-taskdef', { 58 | family: 'image-generator-task-family', 59 | cpu: 1024, 60 | memoryLimitMiB: 4096, 61 | runtimePlatform: { 62 | operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, 63 | cpuArchitecture: ecs.CpuArchitecture.ARM64 64 | }, 65 | taskRole: adotRole, 66 | executionRole: adotExecutionRole 67 | } 68 | ); 69 | 70 | // Add application container to the task definition 71 | def.addContainer(id + '-appcon', { 72 | containerName: 'chatgpt-image-generator', 73 | image: ecs.ContainerImage.fromRegistry("youngjeong46/chatgpt-image-generator:alpine", dockerCredential), 74 | environmentFiles: [ecs.EnvironmentFile.fromBucket(envFileBucket, '/.env')], 75 | portMappings: [ 76 | { 77 | containerPort: 3000 78 | } 79 | ], 80 | }); 81 | 82 | // ADOT Containers to the task definition 83 | const collectorContainerDef = def.addContainer('collector', { 84 | containerName: 'aws-otel-collector', 85 | image: ecs.ContainerImage.fromRegistry('amazon/aws-otel-collector', dockerCredential), 86 | command:[ 87 | '--config=/etc/ecs/container-insights/otel-task-metrics-config.yaml' 88 | ], 89 | essential: true, 90 | logging: new ecs.AwsLogDriver({ 91 | logGroup: new logs.LogGroup(this,'collector-log-group',{ 92 | logGroupName: '/ecs/ecs-aws-otel-sidecar-collector' 93 | }), 94 | streamPrefix: 'ecs', 95 | }), 96 | }); 97 | 98 | const emitterContainerDef = def.addContainer('emitter', { 99 | containerName: 'aws-otel-emitter', 100 | image: ecs.ContainerImage.fromRegistry('public.ecr.aws/aws-otel-test/aws-otel-goxray-sample-app'), 101 | essential: false, 102 | logging: new ecs.AwsLogDriver({ 103 | logGroup: new logs.LogGroup(this, 'emitter-log-group', { 104 | logGroupName: '/ecs/ecs-aws-otel-sidecar-app' 105 | }), 106 | streamPrefix: 'ecs', 107 | }) 108 | }); 109 | emitterContainerDef.addContainerDependencies({container: collectorContainerDef, condition: ecs.ContainerDependencyCondition.START}); 110 | 111 | const nginxDef = def.addContainer('nginx', { 112 | containerName: 'nginx', 113 | image: ecs.ContainerImage.fromRegistry('nginx:latest', dockerCredential), 114 | essential: false 115 | }); 116 | nginxDef.addContainerDependencies({container: collectorContainerDef, condition: ecs.ContainerDependencyCondition.START}); 117 | 118 | const statsdDef = def.addContainer('socat', { 119 | containerName: 'aoc-statsd-emmitter', 120 | image: ecs.ContainerImage.fromRegistry('alpine/socat:latest', dockerCredential), 121 | essential: false, 122 | cpu: 256, 123 | memoryLimitMiB: 512, 124 | logging: new ecs.AwsLogDriver({ 125 | logGroup: new logs.LogGroup(this, 'statsd-log-group', { 126 | logGroupName: '/ecs/statsd-emitter' 127 | }), 128 | streamPrefix: 'ecs', 129 | }), 130 | entryPoint: [ 131 | "/bin/sh", 132 | "-c", 133 | "while true; do echo 'statsdTestMetric:1|c' | socat -v -t 0 - UDP:127.0.0.1:8125; sleep 1; done" 134 | ], 135 | }); 136 | statsdDef.addContainerDependencies({container: collectorContainerDef, condition: ecs.ContainerDependencyCondition.START}); 137 | 138 | // Start the application service 139 | const appService = new ecs.FargateService(this, id + '-appsvc', { 140 | serviceName: 'image-generator-svc', 141 | cluster: cluster, 142 | taskDefinition: def, 143 | desiredCount: 2, 144 | }); 145 | 146 | // Load Balancer for the app 147 | const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, internetFacing: true }); 148 | const listener = lb.addListener('Listener', { port: 80 }); 149 | listener.addTargets('lb-app-target', { 150 | port: 80, 151 | targets: [ 152 | appService.loadBalancerTarget({ 153 | containerName: 'chatgpt-image-generator', 154 | containerPort: 3000, 155 | }) 156 | ], 157 | healthCheck: { 158 | healthyThresholdCount: 5, 159 | path: '/', 160 | interval: cdk.Duration.seconds(5), 161 | timeout: cdk.Duration.seconds(2) 162 | } 163 | }); 164 | 165 | new cdk.CfnOutput(this, 'loadbalancer-output', { 166 | exportName: 'ApplicationLoadBalancerDNS', 167 | value: lb.loadBalancerDnsName 168 | }); 169 | } 170 | 171 | private setUpAdotRoles(docker: boolean, dockerSecret: secrets.ISecret | undefined, 172 | bucket: s3.Bucket): [iam.Role, iam.Role] { 173 | // Create IAM Policy 174 | const AdotPolicyDoc = new iam.PolicyDocument({ 175 | statements: [new iam.PolicyStatement( 176 | { 177 | actions: [ 178 | "logs:PutLogEvents", 179 | "logs:CreateLogGroup", 180 | "logs:CreateLogStream", 181 | "logs:DescribeLogStreams", 182 | "logs:DescribeLogGroups", 183 | "xray:PutTraceSegments", 184 | "xray:PutTelemetryRecords", 185 | "xray:GetSamplingRules", 186 | "xray:GetSamplingTargets", 187 | "xray:GetSamplingStatisticSummaries", 188 | "cloudwatch:PutMetricData", 189 | "ec2:DescribeVolumes", 190 | "ec2:DescribeTags", 191 | "ssm:GetParameters" 192 | ], 193 | resources:["*"], 194 | effect: iam.Effect.ALLOW 195 | } 196 | )] 197 | }); 198 | const AdotPolicy = new iam.Policy(this, 'adot-policy', { 199 | document: AdotPolicyDoc, 200 | }); 201 | 202 | // Create Task Role for Adot 203 | const AdotTaskRole = new iam.Role(this, 'adot-task-role', { 204 | assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), 205 | }); 206 | // Create Task Execution Role for Adot 207 | const AdotTaskExecutionRole = new iam.Role(this, 'adot-task-exec-role', { 208 | assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), 209 | managedPolicies: [ 210 | iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'), 211 | iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess'), 212 | iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMReadOnlyAccess'), 213 | ] 214 | }); 215 | 216 | AdotTaskRole.attachInlinePolicy(AdotPolicy); 217 | AdotTaskExecutionRole.attachInlinePolicy(AdotPolicy); 218 | 219 | // Set up Docker Credential permissions if enabled 220 | if (docker){ 221 | const secret = dockerSecret!; 222 | const dockerDoc = new iam.PolicyDocument({ 223 | statements: [new iam.PolicyStatement( 224 | { 225 | actions: [ 226 | "secretsmanager:GetSecretValue", 227 | ], 228 | resources:[secret.secretArn], 229 | effect: iam.Effect.ALLOW 230 | } 231 | )] 232 | }); 233 | 234 | const dockerPolicy = new iam.Policy(this, 'docker-policy', { 235 | document: dockerDoc, 236 | }); 237 | 238 | AdotTaskExecutionRole.attachInlinePolicy(dockerPolicy); 239 | } 240 | 241 | //Set up S3 permissions for env variable retrieval 242 | const s3Doc = new iam.PolicyDocument({ 243 | statements: [ 244 | new iam.PolicyStatement( 245 | { 246 | actions: [ 247 | "s3:GetObject", 248 | ], 249 | resources:[bucket.bucketArn+'/.env'], 250 | effect: iam.Effect.ALLOW 251 | }, 252 | ), 253 | new iam.PolicyStatement( 254 | { 255 | actions: ["s3:GetBucketLocation"], 256 | resources: [bucket.bucketArn], 257 | effect: iam.Effect.ALLOW 258 | } 259 | ) 260 | ] 261 | }); 262 | 263 | const s3Policy = new iam.Policy(this, 's3-policy', { 264 | document: s3Doc, 265 | }); 266 | 267 | AdotTaskExecutionRole.attachInlinePolicy(s3Policy); 268 | 269 | return [AdotTaskRole, AdotTaskExecutionRole]; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /CdkImageGenerator/lib/index.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { EcsStack } from './ecs'; 4 | import { vpcStack } from './vpc'; 5 | import assert from 'assert'; 6 | 7 | export interface ImageGeneratorConstructProps { 8 | id: string, 9 | cidrBlock: string, 10 | enableDockerAuth: boolean, 11 | dockerSecretName?: string, 12 | } 13 | 14 | export default class ImageGeneratorConstruct { 15 | constructor(scope: Construct, constructProps: ImageGeneratorConstructProps, props: cdk.StackProps){ 16 | const auth = constructProps.enableDockerAuth; 17 | const secretName = constructProps.dockerSecretName || ''; 18 | 19 | assert(auth && (secretName.length > 0), 'You must include the name of the Secret for Docker auth.'); 20 | 21 | const vpc = new vpcStack(scope, constructProps.id + '-vpc-stack', 22 | { 23 | id: constructProps.id + '-vpc', 24 | cidrBlock: constructProps.cidrBlock 25 | }, 26 | { 27 | env: props.env 28 | }); 29 | new EcsStack(scope, constructProps.id + '-ecs-stack', 30 | { 31 | vpc: vpc.vpc, 32 | enableDockerAuth: auth, 33 | secretName: secretName 34 | }, 35 | { 36 | env: props.env 37 | }); 38 | } 39 | } -------------------------------------------------------------------------------- /CdkImageGenerator/lib/vpc.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 4 | 5 | export interface vpcStackProps { 6 | id: string, 7 | cidrBlock: string, 8 | } 9 | 10 | export class vpcStack extends cdk.Stack { 11 | 12 | readonly vpc: ec2.Vpc; 13 | 14 | constructor(scope: Construct, id: string, vpcProps: vpcStackProps, props?: cdk.StackProps) { 15 | super(scope, id, props); 16 | 17 | // VPC 18 | this.vpc = new ec2.Vpc(this, vpcProps.id,{ 19 | ipAddresses: ec2.IpAddresses.cidr(vpcProps.cidrBlock) 20 | }); 21 | } 22 | } -------------------------------------------------------------------------------- /CdkImageGenerator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk_image_generator", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk_image_generator": "bin/cdk_image_generator.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk", 12 | "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx" 13 | }, 14 | "devDependencies": { 15 | "@types/jest": "^27.5.2", 16 | "@types/node": "^10.17.27", 17 | "@types/prettier": "2.6.0", 18 | "@typescript-eslint/eslint-plugin": "^5.47.1", 19 | "aws-cdk": "2.51.1", 20 | "eslint": "^8.30.0", 21 | "eslint-config-standard-with-typescript": "^24.0.0", 22 | "eslint-plugin-import": "^2.26.0", 23 | "eslint-plugin-n": "^15.6.0", 24 | "eslint-plugin-promise": "^6.1.1", 25 | "jest": "^27.5.1", 26 | "ts-jest": "^27.1.4", 27 | "ts-node": "^10.9.1", 28 | "typescript": "^4.9.4" 29 | }, 30 | "dependencies": { 31 | "aws-cdk-lib": "2.51.1", 32 | "constructs": "^10.0.0", 33 | "source-map-support": "^0.5.21" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CdkImageGenerator/test/cdk_image_generator.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as CdkImageGenerator from '../lib/cdk_image_generator-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/cdk_image_generator-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new CdkImageGenerator.CdkImageGeneratorStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /CdkImageGenerator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "ES2020", 5 | "module": "commonjs", 6 | "lib": [ 7 | "es2020" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "esModuleInterop": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": false, 20 | "inlineSourceMap": true, 21 | "inlineSources": true, 22 | "experimentalDecorators": true, 23 | "skipLibCheck": true, 24 | "strictPropertyInitialization": false, 25 | "typeRoots": [ 26 | "./node_modules/@types" 27 | ] 28 | }, 29 | "exclude": [ 30 | "node_modules", 31 | "cdk.out", 32 | "dist", 33 | "test", 34 | "cdk.context.json" 35 | ], 36 | "include": [ 37 | "lib", 38 | "bin", 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH= 2 | FROM ${ARCH}node:18-alpine 3 | ENV OPENAI_API_KEY "" 4 | WORKDIR /usr/src/app 5 | COPY . /usr/src/app/ 6 | RUN npm install 7 | 8 | EXPOSE 3000 9 | CMD "npm" "start" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAI DALL·E Image Generator 2 | This is a sample NodeJS application for generating images. The user is prompted to write a simple description, which the application will use to generate a corresponding image. 3 | 4 | ![a cat and a dog image](./public/index.png) 5 | 6 | The generation is provided using OpenAI's [Images API](https://beta.openai.com/docs/guides/images) and [DALL·E models](https://openai.com/dall-e-2/). 7 | 8 | ## Prerequisites 9 | To get started you will need: 10 | - [Docker](https://docs.docker.com/install/) installed on your local machine. 11 | - [Node](https://nodejs.org/en/), version v16.16 or later. 12 | - [NPM](https://www.npmjs.com/) package manager, version 8.11 or later. 13 | - An IDE for building the application such as [Visual Studio](https://visualstudio.microsoft.com/) 14 | - An [account with OpenAI](https://beta.openai.com/signup) to use their API service. You can get a free account for personal use (with limited API calls). 15 | - An [OpenAI API Key](https://beta.openai.com/account/api-keys) is required to access the API needed for image generation. Store it under `.env` file in the format provided under `.env.example` (assigned under `OPENAI_API_KEY` variable) 16 | 17 | ## Running Locally (using npm) 18 | 19 | To get started, first install dependencies: 20 | 21 | ```sh 22 | npm i 23 | ``` 24 | 25 | Then, start the application: 26 | 27 | ```sh 28 | npm run start 29 | ``` 30 | 31 | ## Running Locally (using docker build) 32 | 33 | You can build your own Docker image as provided below. Since node provide multiarch option, we use `buildx` to build architectures available for `node` container image. You may choose to build an individual arch otherwise. 34 | 35 | ```sh 36 | docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag {your image name}:{your tag} . 37 | ``` 38 | 39 | You can now run the built image using the following command: 40 | 41 | ```sh 42 | docker run -p 3000:3000 --env-file ./.env {your image name}:{your tag} 43 | ``` 44 | 45 | You will then be able to access the application on your browser under the following URL: `http://localhost:3000` 46 | 47 | ## Running Locally (using public image) 48 | 49 | The image already exists publicly in DockerHub: 50 | 51 | ```sh 52 | docker run -p 3000:3000 --env-file ./.env youngjeong46/chatgpt-image-generator:alpine 53 | ``` 54 | 55 | You will then be able to access the application on your browser under the following URL: `http://localhost:3000` 56 | 57 | ## Running on ECS Fargate 58 | 59 | The [AWS CDK](https://aws.amazon.com/cdk/) for Typescript is used to deploy the application to [ECS Fargate](https://aws.amazon.com/fargate/). Follow the instructions in the [README.md](CdkImageGenerator/README.md). 60 | 61 | ## Running on Kubernetes 62 | 63 | First, you will need to generate a Base64 encoded string of your OpenAI API key: 64 | 65 | ```sh 66 | echo -n | base64 67 | ``` 68 | 69 | Take the output and place it in the secret data inside `kubernetes-manifests/apisecret.yaml`. 70 | 71 | To deploy to an existing Kubernetes cluster, run the following command to apply the manifests: 72 | 73 | ```sh 74 | kubectl apply -f kubernetes-manifests/ 75 | ``` 76 | 77 | The [application.yaml](./kubernetes-manifests/application.yaml) manifest doesn't expose the service via a load balancer so in order to test do something like this: 78 | 79 | ```sh 80 | kubectl -n chatgpt-apps port-forward svc/image-generator 3000:80 81 | ``` 82 | 83 | You can access the application via `http://localhost:3000`. -------------------------------------------------------------------------------- /kubernetes-manifests/apisecret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: openapi-key-secret 5 | namespace: chatgpt-apps 6 | type: Opaque 7 | data: 8 | OPENAI_API_KEY: 9 | --- -------------------------------------------------------------------------------- /kubernetes-manifests/application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: image-generator 6 | name: image-generator 7 | namespace: chatgpt-apps 8 | spec: 9 | ports: 10 | - 11 | name: http 12 | port: 80 13 | protocol: TCP 14 | targetPort: 3000 15 | selector: 16 | app: image-generator 17 | --- 18 | apiVersion: apps/v1 19 | kind: Deployment 20 | metadata: 21 | labels: 22 | app: image-generator 23 | name: image-generator 24 | namespace: chatgpt-apps 25 | spec: 26 | replicas: 2 27 | selector: 28 | matchLabels: 29 | app: image-generator 30 | strategy: 31 | type: Recreate 32 | template: 33 | metadata: 34 | labels: 35 | app: image-generator 36 | spec: 37 | containers: 38 | - image: "youngjeong46/chatgpt-image-generator:alpine" 39 | name: image-generator 40 | env: 41 | - name: OPENAI_API_KEY 42 | valueFrom: 43 | secretKeyRef: 44 | name: openapi-key-secret 45 | key: OPENAI_API_KEY 46 | ports: 47 | - containerPort: 3000 48 | name: image-generator 49 | livenessProbe: 50 | httpGet: 51 | path: / 52 | port: 3000 53 | initialDelaySeconds: 5 54 | periodSeconds: 30 55 | readinessProbe: 56 | httpGet: 57 | path: / 58 | port: 3000 59 | initialDelaySeconds: 5 60 | timeoutSeconds: 1 61 | tolerations: 62 | - key: "workload" 63 | operator: "Exists" 64 | effect: "NoSchedule" -------------------------------------------------------------------------------- /kubernetes-manifests/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: chatgpt-apps 5 | labels: 6 | name: chatgpt-apps 7 | --- -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-app", 3 | "version": "0.1.0", 4 | "description": "Chat GPT application", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "next": "^12.1.6", 13 | "openai": "^3.0.0", 14 | "react": "17.0.2", 15 | "react-dom": "17.0.2" 16 | }, 17 | "author": "Young Jeong", 18 | "license": "ISC" 19 | } -------------------------------------------------------------------------------- /pages/api/generate.js: -------------------------------------------------------------------------------- 1 | import { Configuration, OpenAIApi } from "openai"; 2 | 3 | const configuration = new Configuration({ 4 | apiKey: process.env.OPENAI_API_KEY, 5 | }); 6 | const openai = new OpenAIApi(configuration); 7 | 8 | export default async function (req, res) { 9 | const response = await openai.createImage({ 10 | prompt: req.body.question, 11 | n:1, 12 | size:"256x256", 13 | }); 14 | res.status(200).json({ result: response.data.data[0].url }); 15 | } 16 | 17 | // function generatePrompt(question) { 18 | // const capitalizedAnimal = 19 | // animal[0].toUpperCase() + animal.slice(1).toLowerCase(); 20 | // return `Suggest three names for an animal that is a superhero. 21 | 22 | // Animal: Cat 23 | // Names: Captain Sharpclaw, Agent Fluffball, The Incredible Feline 24 | // Animal: Dog 25 | // Names: Ruff the Protector, Wonder Canine, Sir Barks-a-Lot 26 | // Animal: ${capitalizedAnimal} 27 | // Names:`; 28 | // } 29 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { useState } from "react"; 3 | import styles from "./index.module.css"; 4 | 5 | export default function Home() { 6 | const [questionInput, setQuestionInput] = useState(""); 7 | const [result, setResult] = useState(); 8 | 9 | async function onSubmit(event) { 10 | event.preventDefault(); 11 | const response = await fetch("/api/generate", { 12 | method: "POST", 13 | headers: { 14 | "Content-Type": "application/json", 15 | }, 16 | body: JSON.stringify({ question: questionInput }), 17 | }); 18 | const data = await response.json(); 19 | setResult(data.result); 20 | } 21 | 22 | return ( 23 |
24 | 25 | ChatGPT Image Generator App 26 | 27 | 28 | 29 |
30 | 31 |

What would you like to see?

32 |
33 |