diff --git a/typescript/aws-codepipeline-ecs-lambda/.gitignore b/typescript/aws-codepipeline-ecs-lambda/.gitignore new file mode 100644 index 0000000000..3a671bb3bd --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/.gitignore @@ -0,0 +1,9 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out +cdk.context.json diff --git a/typescript/aws-codepipeline-ecs-lambda/.npmignore b/typescript/aws-codepipeline-ecs-lambda/.npmignore new file mode 100644 index 0000000000..c1d6d45dcf --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/typescript/aws-codepipeline-ecs-lambda/DO_NOT_AUTOTEST b/typescript/aws-codepipeline-ecs-lambda/DO_NOT_AUTOTEST new file mode 100644 index 0000000000..e69de29bb2 diff --git a/typescript/aws-codepipeline-ecs-lambda/README.md b/typescript/aws-codepipeline-ecs-lambda/README.md new file mode 100644 index 0000000000..0c97fce8e3 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/README.md @@ -0,0 +1,59 @@ +# AWS Codepipeline CI/CD Solution for ECS Fargate and Lambda + +## Architect Design: +![](./static_images/Architecture_diagram.png) + +## Overview + +This CDK package provides a production-grade template for setting up AWS resources to enable smooth migration from monolithic EC2-based architectures to cloud-native solutions on AWS. It's designed for startups looking to scale their infrastructure efficiently. + +Key features: +- CICD pipeline using AWS CodePipeline +- Lambda functions (async triggered and REST endpoints behind API Gateway) +- ECS Fargate based service with automatic deployment +- Integration with private GitHub repositories +- Reference to CDK created VPCs +- Creates RDS Instances with credentials managed by AWS Secrets Manager +- Event-driven architecture using SQS, SNS, and EventBridge + +## Getting Started + +### Prerequisites + +- Create a Private Github Repository with source code inside 'aws-codepipeline-ecs-lambda' directory. +- Create a connection to GitHub or GitHub Enterprise Cloud, see [Create a connection to GitHub](https://docs.aws.amazon.com/dtconsole/latest/userguide/connections-create-github.html). +- Modify the `connectionArn` in `pipeline-stack.ts` file. +- Modify `githubOrg`, `githubRepo`, `githubBranch` with your private repository details. + +## Project Structure + +The project is organized into six main stacks: + +1. `VpcStack`: Network infrastructure resources +2. `DataStoresStack`: Database resources +3. `PubSubStack`: Event-based infrastructure (SQS, SNS, EventBridge) +4. `AsyncLambdasStack`: Asynchronously triggered Lambda functions +5. `LambdaApisStack`: Lambda functions as REST endpoints behind API Gateway +6. `EcsFargateStack`: ECS Fargate Service, Cluster, Tasks, and Containers + +You can easily customize the infrastructure by modifying or removing specific stacks in the `lib/pipeline-stage.ts` file. + +## Solution overview + +The CDK application is structured as follows: + +`lib/pipeline-stack.ts` contains the definition of the CI/CD pipeline. The main component here is the CodePipeline construct that creates the pipeline for us + +`lib/stage-app.ts` contains definitions of all the six stacks which the pipeline will deploy. + +`lib/stage-app-vpc-stack.ts` creates new vpc resource along with subnets, nat gateways and remaining networking infrastructure. + +`lib/stage-app-datastore-stack.ts` creates a Aurora Serverless V2 Cluster along with KMS key to encrypt the database. + +`lib/stage-app-ecs-fargate-stack.ts` builds the `Dockerfile` and creates ecs fargate service along with loadbalancer. + +`lib/stage-app-lambda-api-stack.ts` creates lambda functions as REST endpoints behind API Gateway resource. + +`lib/PubSubStack.ts` creates sns, eventbridge and lambda function. + +--- diff --git a/typescript/aws-codepipeline-ecs-lambda/assets/lambda-functions/events_handler.ts b/typescript/aws-codepipeline-ecs-lambda/assets/lambda-functions/events_handler.ts new file mode 100644 index 0000000000..cf779c1068 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/assets/lambda-functions/events_handler.ts @@ -0,0 +1,7 @@ +import { EventBridgeEvent, Context } from 'aws-lambda'; + +export const handler = async (event: EventBridgeEvent) => { + console.log('LogEvent'); + console.log('Received event:', JSON.stringify(event, null, 2)); + return 'Finished'; +}; diff --git a/typescript/aws-codepipeline-ecs-lambda/assets/lambda-functions/queue_message_handler.ts b/typescript/aws-codepipeline-ecs-lambda/assets/lambda-functions/queue_message_handler.ts new file mode 100644 index 0000000000..dc0f05aabf --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/assets/lambda-functions/queue_message_handler.ts @@ -0,0 +1,18 @@ +import { SQSEvent, SQSBatchResponse, SQSBatchItemFailure } from 'aws-lambda'; + +export const handler = (event: SQSEvent): SQSBatchResponse | void => { + if (event) { + const batchItemFailures: SQSBatchItemFailure[] = []; + event?.Records.forEach(record => { + try { + // process record + } catch (err) { + batchItemFailures.push({ + itemIdentifier: record.messageId + }); + } + }); + + return { batchItemFailures }; + } +}; diff --git a/typescript/aws-codepipeline-ecs-lambda/assets/lambda-functions/topic_message_handler.ts b/typescript/aws-codepipeline-ecs-lambda/assets/lambda-functions/topic_message_handler.ts new file mode 100644 index 0000000000..74951b72bc --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/assets/lambda-functions/topic_message_handler.ts @@ -0,0 +1,19 @@ +import { SNSEvent, SNSEventRecord } from 'aws-lambda'; + +export const handler = async (event: SNSEvent) => { + for (const record of event.Records) { + await processMessageAsync(record); + } + console.info("done"); +}; + +async function processMessageAsync(record: SNSEventRecord) { + try { + const message = JSON.stringify(record.Sns.Message); + console.log(`Processed message ${message}`); + await Promise.resolve(1); //Placeholder for actual async work + } catch (err) { + console.error("An error occurred"); + throw err; + } +} diff --git a/typescript/aws-codepipeline-ecs-lambda/cdk.json b/typescript/aws-codepipeline-ecs-lambda/cdk.json new file mode 100644 index 0000000000..50556dcaf0 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/cdk.json @@ -0,0 +1,72 @@ +{ + "app": "npx ts-node --prefer-ts-exts lib/app.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false + } +} diff --git a/typescript/aws-codepipeline-ecs-lambda/lib/app.ts b/typescript/aws-codepipeline-ecs-lambda/lib/app.ts new file mode 100644 index 0000000000..bbfdfb0099 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/lib/app.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { pipelineStack } from './pipeline-stack'; + +const app = new cdk.App(); +const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION } +const pipelineAccountId = process.env.PIPELINE_ACCOUNT_ID || "111111111111"; // replace with your pipeline account id +const pipelineRegion = process.env.PIPELINE_REGION || "us-east-1"; // replace with your pipeline region +const githubOrg = process.env.GITHUB_ORG || "aws-6w8hnx"; // replace with your GitHub Org +const githubRepo = process.env.GITHUB_REPO || "aws-codepipeline-ecs-lambda"; // replace with your GitHub Repo +const githubBranch = process.env.GITHUB_BRANCH || "main"; // replace with your GitHub repo branch +const devEnv = process.env.DEV_ENV || "dev"; // replace with your environment +const devAccountId = process.env.DEV_ACCOUNT_ID || "222222222222"; // replace with your dev account id +const primaryRegion = process.env.PRIMARY_REGION || "us-west-2"; // replace with your primary region +const secondaryRegion = process.env.SECONDARY_REGION || "eu-west-1"; // replace with your secondary region + + +const pipeline_stack = new pipelineStack(app, 'aws-codepipeline-stack', { + env, + pipelineAccountId, + pipelineRegion, + githubOrg, + githubRepo, + githubBranch, + devEnv, + devAccountId, + primaryRegion, + secondaryRegion, +}); +cdk.Tags.of(pipeline_stack).add('managedBy', 'cdk'); +cdk.Tags.of(pipeline_stack).add('environment', 'dev'); + +app.synth(); diff --git a/typescript/aws-codepipeline-ecs-lambda/lib/pipeline-stack.ts b/typescript/aws-codepipeline-ecs-lambda/lib/pipeline-stack.ts new file mode 100644 index 0000000000..314bfab308 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/lib/pipeline-stack.ts @@ -0,0 +1,63 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { CodePipeline, CodePipelineSource, ManualApprovalStep, ShellStep, Wave } from 'aws-cdk-lib/pipelines'; +import { pipelineAppStage } from './stage-app'; +import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; + +export interface pipelineProps extends cdk.StackProps { + readonly pipelineAccountId: string; + readonly pipelineRegion: string; + readonly githubOrg: string; + readonly githubRepo: string; + readonly githubBranch: string; + readonly devEnv: string; + readonly devAccountId: string; + readonly primaryRegion: string; + readonly secondaryRegion: string; +} + +export class pipelineStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: pipelineProps) { + super(scope, id, props); + + const pipeline = new CodePipeline(this, 'pipeline', { + selfMutation: true, + crossAccountKeys: true, + reuseCrossRegionSupportStacks: true, + synth: new ShellStep('Synth', { + input: CodePipelineSource.connection(`${props.githubOrg}/${props.githubRepo}`, `${props.githubBranch}`,{ + // You need to replace the below code connection arn: + connectionArn: `arn:aws:codestar-connections:${props.pipelineRegion}:${props.pipelineAccountId}:connection/0ce75950-a29b-4ee4-a9d3-b0bad3b2c0a6` + }), + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth' + ] + }), + synthCodeBuildDefaults: { + rolePolicy: [ + new PolicyStatement({ + resources: [ '*' ], + actions: [ 'ec2:DescribeAvailabilityZones' ], + }), + ]} + }); + + const devStage = pipeline.addStage(new pipelineAppStage(this, `${props.devEnv}`, { + env: { account: `${props.pipelineAccountId}`, region: `${props.pipelineRegion}`} + })); + devStage.addPost(new ManualApprovalStep('approval')); + + // add waves: + const devWave = pipeline.addWave(`${props.devEnv}Wave`); + + devWave.addStage(new pipelineAppStage(this, `${props.devEnv}-${props.primaryRegion}`, { + env: { account: `${props.devAccountId}`, region: `${props.primaryRegion}`} + })); + + devWave.addStage(new pipelineAppStage(this, `${props.devEnv}-${props.secondaryRegion}`, { + env: { account: `${props.devAccountId}`, region: `${props.secondaryRegion}`} + })); + } +} diff --git a/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-async-lambda-stack.ts b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-async-lambda-stack.ts new file mode 100644 index 0000000000..7fbb7087a0 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-async-lambda-stack.ts @@ -0,0 +1,75 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as eventsTargets from 'aws-cdk-lib/aws-events-targets'; +import * as sns from 'aws-cdk-lib/aws-sns'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import { SnsEventSource, SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; +import * as path from 'path'; +import { Function, Code, Runtime } from 'aws-cdk-lib/aws-lambda'; + +export class asyncLambdaStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // EventBridge event as an event source for a SNS topic and a SQS queue + const rule = new events.Rule(this, 'Rule', { + eventPattern: { + source: ['aws.ecs'] + } + }); + + // Shared code asset + const code = Code.fromAsset(path.join(__dirname, '../assets/lambda-functions')); + + // Lambda Function that will be invoked asynchronously when there is any event that matches the rule + const eventsFunction = new Function(this, 'EventFunction', { + runtime: Runtime.NODEJS_20_X, + code, + handler: 'events_handler.handler' + }); + const eventsDLQ = new sqs.Queue(this, 'LambdaDLQ'); + rule.addTarget(new eventsTargets.LambdaFunction(eventsFunction, { + deadLetterQueue: eventsDLQ, + maxEventAge: cdk.Duration.minutes(2), + retryAttempts: 2 + })); + + // Lambda Function that will be invoked asynchronously by a SNS topic + const topic = new sns.Topic(this, 'Topic'); + rule.addTarget(new eventsTargets.SnsTopic(topic, { + deadLetterQueue: eventsDLQ, + maxEventAge: cdk.Duration.minutes(2), + retryAttempts: 2 + })); + + const topicFunction = new Function(this, 'TopicLambdaFunction', { + runtime: Runtime.NODEJS_20_X, + code, + handler: 'topic_message_handler.handler' + }); + const topicDLQ = new sqs.Queue(this, 'TopicDLQ'); + topicFunction.addEventSource(new SnsEventSource(topic, { + deadLetterQueue: topicDLQ + })); + + // Lambda Function that will be invoked asynchronously with the event source of a SQS queue + const queue = new sqs.Queue(this, 'JobQueue'); + rule.addTarget(new eventsTargets.SqsQueue(queue, { + deadLetterQueue: eventsDLQ, + maxEventAge: cdk.Duration.minutes(2), + retryAttempts: 2 + })); + + const queueFunction = new Function(this, 'QueueLambdaFunction', { + runtime: Runtime.NODEJS_20_X, + code, + handler: 'queue_message_handler.handler' + }); + queueFunction.addEventSource(new SqsEventSource(queue, { + batchSize: 5, + maxBatchingWindow: cdk.Duration.seconds(5), + reportBatchItemFailures: true + })); + } +} \ No newline at end of file diff --git a/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-datastore-stack.ts b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-datastore-stack.ts new file mode 100644 index 0000000000..91fdc97875 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-datastore-stack.ts @@ -0,0 +1,40 @@ +import * as cdk from 'aws-cdk-lib'; +import { InstanceClass, InstanceSize, InstanceType, Vpc } from 'aws-cdk-lib/aws-ec2'; +import { Key } from 'aws-cdk-lib/aws-kms'; +import { AuroraMysqlEngineVersion, ClusterInstance, DatabaseCluster, DatabaseClusterEngine } from 'aws-cdk-lib/aws-rds'; +import { Construct } from 'constructs'; + +interface vpcStackProps extends cdk.StackProps { + readonly vpc: Vpc; +} + +export class rdsAuroraStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: vpcStackProps) { + super(scope, id, props); + + const kmskey = new Key(this, 'MyKey', { + enableKeyRotation: true, + rotationPeriod: cdk.Duration.days(180), // Default is 365 days + }); + + const cluster = new DatabaseCluster(this, 'Database', { + engine: DatabaseClusterEngine.auroraMysql({ version: AuroraMysqlEngineVersion.VER_3_03_0 }), + writer: ClusterInstance.provisioned('writer', { + instanceType: InstanceType.of(InstanceClass.R6G, InstanceSize.XLARGE4), + }), + serverlessV2MinCapacity: 6.5, + serverlessV2MaxCapacity: 64, + readers: [ + // will be put in promotion tier 1 and will scale with the writer + ClusterInstance.serverlessV2('reader1', { scaleWithWriter: true }), + // will be put in promotion tier 2 and will not scale with the writer + ClusterInstance.serverlessV2('reader2'), + ], + credentials: { username: 'clusteradmin' }, + cloudwatchLogsExports: ["error"], + vpc: props.vpc, + storageEncrypted: true, + storageEncryptionKey: kmskey + }); +} +} \ No newline at end of file diff --git a/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-ecs-fargate-stack.ts b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-ecs-fargate-stack.ts new file mode 100644 index 0000000000..ee77fe188b --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-ecs-fargate-stack.ts @@ -0,0 +1,57 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns'; +import { IVpc, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2'; +import { Cluster, ContainerImage, EcrImage } from 'aws-cdk-lib/aws-ecs'; +import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; +import path = require('path'); + +interface vpcStackProps extends cdk.StackProps { + readonly vpc: Vpc; +} + +export class ecsFargateStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: vpcStackProps) { + super(scope, id, props); + + const cluster = new Cluster(this, 'ecsCluster', { + vpc: props.vpc, + containerInsights: true, + }); + const asset = new DockerImageAsset(this, 'AppImage', { + directory: path.join(__dirname, '..', 'src') + }); + + // 👇 create a new ecs pattern with an alb 👇 + const loadBalancedFargateService = new ApplicationLoadBalancedFargateService (this, 'ecsPattern', { + cluster, + cpu: 256, + memoryLimitMiB: 512, + desiredCount: 1, + publicLoadBalancer: true, + taskSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS }, + taskImageOptions: { + image: EcrImage.fromDockerImageAsset(asset), + }, + enableExecuteCommand: true, + }); + + // 👇 auto scale task count 👇 + const scalableTarget = loadBalancedFargateService.service.autoScaleTaskCount({ + minCapacity: 1, + maxCapacity: 6, + }); + // 👇 auto scale cpu trigger 👇 + scalableTarget.scaleOnCpuUtilization('CpuScaling', { + targetUtilizationPercent: 50, + }); + // 👇 auto scale memory trigger 👇 + scalableTarget.scaleOnMemoryUtilization('MemoryScaling', { + targetUtilizationPercent: 50, + }); + // 👇 load balancer health check 👇 + loadBalancedFargateService.targetGroup.configureHealthCheck({ + path: "/", + }); + } +} diff --git a/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-lambda-api-stack.ts b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-lambda-api-stack.ts new file mode 100644 index 0000000000..e037f788a6 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-lambda-api-stack.ts @@ -0,0 +1,33 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { Function, InlineCode, Runtime } from 'aws-cdk-lib/aws-lambda'; +import { LambdaRestApi } from 'aws-cdk-lib/aws-apigateway' + +export class lambdaApiStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // create a lambda function + const lambdaFunction = new Function(this, 'lambdaFunction', { + runtime: Runtime.NODEJS_20_X, + handler: 'index.handler', + code: new InlineCode(`exports.handler = async (event) => { + const response = { + statusCode: 200, + body: JSON.stringify('Hello from Lambda!'), + }; + return response; +};`) + }); + + // create an API Gateway + const api = new LambdaRestApi(this, 'ApiGateway', { + handler: lambdaFunction, + proxy: false, + }); + + // connect the lambda function to API Gateway + api.root.addMethod("GET"); + + } +} diff --git a/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-vpc-stack.ts b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-vpc-stack.ts new file mode 100644 index 0000000000..4c42302500 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app-vpc-stack.ts @@ -0,0 +1,37 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { IpAddresses, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2'; + +export class vpcStack extends cdk.Stack { + + public readonly vpc: Vpc; + + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // 👇 assign a vpc to the class property + this.vpc = new Vpc(this, 'vpc', { + maxAzs: 3, + natGateways: 1, + enableDnsHostnames: true, + enableDnsSupport: true, + ipAddresses: IpAddresses.cidr('10.0.0.0/16'), + subnetConfiguration: [ + { + cidrMask: 24, + name: 'public', + subnetType: SubnetType.PUBLIC, + }, + { + cidrMask: 24, + name: 'private', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + }, + { + cidrMask: 28, + name: 'ioslated', + subnetType: SubnetType.PRIVATE_ISOLATED, + }] + }) + } +} diff --git a/typescript/aws-codepipeline-ecs-lambda/lib/stage-app.ts b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app.ts new file mode 100644 index 0000000000..d296568dc5 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/lib/stage-app.ts @@ -0,0 +1,42 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from "constructs"; +import { vpcStack } from './stage-app-vpc-stack'; +import { lambdaApiStack } from './stage-app-lambda-api-stack'; +import { ecsFargateStack } from './stage-app-ecs-fargate-stack'; +import { asyncLambdaStack } from './stage-app-async-lambda-stack'; +import { rdsAuroraStack } from './stage-app-datastore-stack'; + +export class pipelineAppStage extends cdk.Stage { + + constructor(scope: Construct, id: string, props?: cdk.StageProps) { + super(scope, id, props); + + // 👇 vpc stack 👇 + const vpc_stack = new vpcStack(this, 'VpcStack'); + cdk.Tags.of(vpc_stack).add('managedBy', 'cdk'); + cdk.Tags.of(vpc_stack).add('environment', 'dev'); + + // 👇 lambda api stack 👇 + const lambda_api_stack = new lambdaApiStack(this, 'LambdaApisStack'); + cdk.Tags.of(lambda_api_stack).add('managedBy', 'cdk'); + cdk.Tags.of(lambda_api_stack).add('environment', 'dev'); + + // 👇 ecs fargate stack 👇 + const ecs_fargate_stack = new ecsFargateStack(this, 'EcsFargateStack', { + vpc: vpc_stack.vpc, + }); + cdk.Tags.of(ecs_fargate_stack).add('managedBy', 'cdk'); + cdk.Tags.of(ecs_fargate_stack).add('environment', 'dev'); + + // 👇 async lambda stack 👇 + const async_lambda_stack = new asyncLambdaStack(this, 'AsyncLambdasStack'); + cdk.Tags.of(async_lambda_stack).add('managedBy', 'cdk'); + cdk.Tags.of(async_lambda_stack).add('environment', 'dev'); + + const rds_aurora_stack = new rdsAuroraStack(this, 'rdsAuroraStack', { + vpc: vpc_stack.vpc, + }); + cdk.Tags.of(rds_aurora_stack).add('managedBy', 'cdk'); + cdk.Tags.of(rds_aurora_stack).add('environment', 'dev'); + } +} \ No newline at end of file diff --git a/typescript/aws-codepipeline-ecs-lambda/package.json b/typescript/aws-codepipeline-ecs-lambda/package.json new file mode 100644 index 0000000000..f5353766df --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/package.json @@ -0,0 +1,25 @@ +{ + "name": "aws-codepipeline-ecs-lambda", + "version": "0.1.0", + "bin": { + "aws-codepipeline-ecs-lambda": "bin/aws-codepipeline-ecs-lambda.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.140", + "@types/node": "20.14.2", + "aws-cdk": "2.162.1", + "ts-node": "^10.9.2", + "typescript": "~5.4.5" + }, + "dependencies": { + "aws-cdk-lib": "2.162.1", + "@types/aws-lambda": "^8.10.140", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} diff --git a/typescript/aws-codepipeline-ecs-lambda/src/Dockerfile b/typescript/aws-codepipeline-ecs-lambda/src/Dockerfile new file mode 100644 index 0000000000..6a8f56a0ff --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/src/Dockerfile @@ -0,0 +1,12 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.12-slim-buster + +WORKDIR /flask-app + +COPY flask-app/requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +COPY flask-app/ . + +CMD [ "python3", "app.py"] diff --git a/typescript/aws-codepipeline-ecs-lambda/src/flask-app/__init__.py b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/__init__.py new file mode 100644 index 0000000000..7e9609681c --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/__init__.py @@ -0,0 +1 @@ +name = 'flask-app' \ No newline at end of file diff --git a/typescript/aws-codepipeline-ecs-lambda/src/flask-app/app.py b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/app.py new file mode 100644 index 0000000000..45e894bad1 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/app.py @@ -0,0 +1,18 @@ +from flask import Flask, render_template + +import datetime +import os + +app = Flask(__name__) + + +@app.route('/') +def index(): + now = datetime.datetime.now() + env = os.getenv('CUSTOM_ENVVAR') if os.getenv('CUSTOM_ENVVAR') else 'Have a nice day!' + bg = os.getenv('BG_COLOR') if os.getenv('BG_COLOR') else '#7b7b7b' + return render_template('index.html', context=now, env=env, bg=bg) + + +if __name__ == "__main__": + app.run(host='0.0.0.0', port=80) diff --git a/typescript/aws-codepipeline-ecs-lambda/src/flask-app/requirements.txt b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/requirements.txt new file mode 100644 index 0000000000..63287bc1f5 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/requirements.txt @@ -0,0 +1,6 @@ +click==8.0.3 +Flask==2.0.2 +itsdangerous==2.0.1 +Jinja2==3.0.2 +MarkupSafe==2.0.1 +Werkzeug==2.0.2 \ No newline at end of file diff --git a/typescript/aws-codepipeline-ecs-lambda/src/flask-app/static/css/style.css b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/static/css/style.css new file mode 100644 index 0000000000..d6d85c1eb0 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/static/css/style.css @@ -0,0 +1,6 @@ +h1 { + color: #fff; + text-align: center; + padding: 10px; + font-family: Arial, Helvetica, sans-serif; +} diff --git a/typescript/aws-codepipeline-ecs-lambda/src/flask-app/static/favicon.ico b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/static/favicon.ico new file mode 100644 index 0000000000..07392b4332 Binary files /dev/null and b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/static/favicon.ico differ diff --git a/typescript/aws-codepipeline-ecs-lambda/src/flask-app/templates/index.html b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/templates/index.html new file mode 100644 index 0000000000..cfa7be6922 --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/src/flask-app/templates/index.html @@ -0,0 +1,19 @@ + + + + + + + Flask App + + + +

Welcome to Flask App

+

This page was rendered on {{ context.day }}/{{ context.month }}/{{ context.year }} at {{ context.strftime("%H") }}:{{ context.strftime("%M") }}:{{ context.strftime("%S") }} UTC

+

{{ env }}

+ + \ No newline at end of file diff --git a/typescript/aws-codepipeline-ecs-lambda/static_images/Architecture_diagram.png b/typescript/aws-codepipeline-ecs-lambda/static_images/Architecture_diagram.png new file mode 100644 index 0000000000..01480dedb3 Binary files /dev/null and b/typescript/aws-codepipeline-ecs-lambda/static_images/Architecture_diagram.png differ diff --git a/typescript/aws-codepipeline-ecs-lambda/tsconfig.json b/typescript/aws-codepipeline-ecs-lambda/tsconfig.json new file mode 100644 index 0000000000..aaa7dc510f --- /dev/null +++ b/typescript/aws-codepipeline-ecs-lambda/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +}