import * as cdk from '@aws-cdk/core'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecr from '@aws-cdk/aws-ecr'; import * as ecs from '@aws-cdk/aws-ecs'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as logs from '@aws-cdk/aws-logs'; import * as ssm from '@aws-cdk/aws-ssm'; import * as kms from '@aws-cdk/aws-kms'; import { CfnOutput } from '@aws-cdk/core'; import { AwsLogDriver } from '@aws-cdk/aws-ecs'; import { env } from 'process'; // import { env } from 'process'; //--------------------------------------------------------------------------- // Environment Variables const deploymentEnv = { type: process.env.THIS_DEPLOYMENT_ENV || "", namespace: process.env.THIS_DEPLOYMENT_NAMESPACE || "", domainName: process.env.THIS_DEPLOYMENT_DOMAINNAME || "", logStreamPrefix: process.env.THIS_LOG_STREAM_PREFIX || "", ecrRepoName: process.env.THIS_ECR_REPO_NAME || "", appName: process.env.APP_NAME || "", kmsKeyId: process.env.THIS_AWS_KMS_KEY_ID || "", }; function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1) } function generateName(name: string) { return capitalizeFirstLetter(deploymentEnv.type) + capitalizeFirstLetter(deploymentEnv.namespace) + name; } function getFromSsmParameterStoreString(scope: cdk.Construct, paramName: string) { return ssm.StringParameter.fromStringParameterName( scope, generateName('Ssm' + paramName), '/' + deploymentEnv.type + '/' + deploymentEnv.appName + '/' + paramName ); } function ecsValueFromSsmParameterString(scope: cdk.Construct, paramName: string) { return ecs.Secret.fromSsmParameter(getFromSsmParameterStoreString(scope, paramName)); } function ecsValueFromSsmParameterSecureString(scope: cdk.Construct, paramName: string, key: kms.IKey) { const paramVersion = cdk.Token.asNumber(getFromSsmParameterStoreString(scope, paramName+'ver')); return ecs.Secret.fromSsmParameter(ssm.StringParameter.fromSecureStringParameterAttributes( scope, generateName('Ssm' + paramName), { parameterName: '/' + deploymentEnv.type + '/' + deploymentEnv.appName + '/' + paramName, encryptionKey: key, version: paramVersion, })); } export class LaravelOnAwsWorkshopStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); //--------------------------------------------------------------------------- // VPC const vpc = new ec2.Vpc(this, generateName('VPC'), { maxAzs: 2, natGateways: 1, subnetConfiguration: [ { cidrMask: 24, name: generateName('ingress'), subnetType: ec2.SubnetType.PUBLIC, }, { cidrMask: 24, name: generateName('application'), subnetType: ec2.SubnetType.PRIVATE, }, { cidrMask: 24, name: generateName('database'), subnetType: ec2.SubnetType.ISOLATED, }, ], }); //--------------------------------------------------------------------------- // ECS // ECS Cluster const ecsCluster = new ecs.Cluster(this, 'LaravelWorkshopCluster', { vpc: vpc }); const asg = ecsCluster.addCapacity('DefaultAutoScalingGroup', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.SMALL), maxCapacity: 4, minCapacity: 2, }); // TODO: T4g // Task Definition const ec2TaskDefinition = new ecs.Ec2TaskDefinition(this, 'DefaultTaskDef'); const ecrRepo = ecr.Repository.fromRepositoryName(this, 'DefaultRepo', deploymentEnv.ecrRepoName); const kmsKey = kms.Key.fromKeyArn( this, generateName('kmsKey'), 'arn:aws:kms:' + props?.env?.region + ':' + props?.env?.account + ':key/' + deploymentEnv.kmsKeyId ); const container = ec2TaskDefinition.addContainer('defaultContainer', { image: ecs.ContainerImage.fromEcrRepository(ecrRepo), memoryLimitMiB: 512, cpu: 256, logging: new ecs.AwsLogDriver({ streamPrefix: deploymentEnv.logStreamPrefix, logRetention: logs.RetentionDays.SIX_MONTHS, }), secrets: { // Retrieved from AWS Secrets Manager or AWS Systems Manager Parameter Store at container start-up. 'APP_NAME' : ecsValueFromSsmParameterString(this, 'APP_NAME'), 'APP_ENV' : ecsValueFromSsmParameterString(this, 'APP_ENV'), 'APP_KEY' : ecsValueFromSsmParameterSecureString(this, 'APP_KEY', kmsKey), 'APP_DEBUG' : ecsValueFromSsmParameterString(this, 'APP_DEBUG'), 'APP_URL' : ecsValueFromSsmParameterString(this, 'APP_URL'), 'LOG_CHANNEL' : ecsValueFromSsmParameterString(this, 'LOG_CHANNEL'), 'DB_CONNECTION' : ecsValueFromSsmParameterString(this, 'DB_CONNECTION'), 'DB_HOST' : ecsValueFromSsmParameterString(this, 'DB_HOST'), 'DB_PORT' : ecsValueFromSsmParameterString(this, 'DB_PORT'), 'DB_DATABASE' : ecsValueFromSsmParameterString(this, 'DB_DATABASE'), 'DB_USERNAME' : ecsValueFromSsmParameterSecureString(this, 'DB_USERNAME', kmsKey), 'DB_PASSWORD' : ecsValueFromSsmParameterSecureString(this, 'DB_PASSWORD', kmsKey), 'BROADCAST_DRIVER' : ecsValueFromSsmParameterString(this, 'BROADCAST_DRIVER'), 'CACHE_DRIVER' : ecsValueFromSsmParameterString(this, 'CACHE_DRIVER'), 'QUEUE_CONNECTION' : ecsValueFromSsmParameterString(this, 'QUEUE_CONNECTION'), 'SESSION_DRIVER' : ecsValueFromSsmParameterString(this, 'SESSION_DRIVER'), 'SESSION_LIFETIME' : ecsValueFromSsmParameterString(this, 'SESSION_LIFETIME'), 'REDIS_HOST' : ecsValueFromSsmParameterString(this, 'REDIS_HOST'), 'REDIS_PASSWORD' : ecsValueFromSsmParameterSecureString(this, 'REDIS_PASSWORD', kmsKey), 'REDIS_PORT' : ecsValueFromSsmParameterString(this, 'REDIS_PORT'), 'MAIL_DRIVER' : ecsValueFromSsmParameterString(this, 'MAIL_DRIVER'), 'MAIL_HOST' : ecsValueFromSsmParameterString(this, 'MAIL_HOST'), 'MAIL_PORT' : ecsValueFromSsmParameterString(this, 'MAIL_PORT'), 'MAIL_USERNAME' : ecsValueFromSsmParameterSecureString(this, 'MAIL_USERNAME', kmsKey), 'MAIL_PASSWORD' : ecsValueFromSsmParameterSecureString(this, 'MAIL_PASSWORD', kmsKey), 'MAIL_ENCRYPTION' : ecsValueFromSsmParameterString(this, 'MAIL_ENCRYPTION'), 'MAIL_FROM_ADDRESS' : ecsValueFromSsmParameterString(this, 'MAIL_FROM_ADDRESS'), 'MAIL_FROM_NAME' : ecsValueFromSsmParameterString(this, 'MAIL_FROM_NAME'), 'ZZZ_KEY' : ecsValueFromSsmParameterSecureString(this, 'ZZZ_KEY', kmsKey), }, }); container.addPortMappings({ containerPort: 80, protocol: ecs.Protocol.TCP }); // ECS Service const ecsService = new ecs.Ec2Service(this, 'DefaultService', { cluster: ecsCluster, taskDefinition: ec2TaskDefinition, desiredCount: 2, }); //--------------------------------------------------------------------------- // Cert const domainAlternativeName = '*.' + deploymentEnv.domainName; const cert = new acm.Certificate(this, generateName('Cert'), { domainName: deploymentEnv.domainName, subjectAlternativeNames: [domainAlternativeName], validation: acm.CertificateValidation.fromDns(), // Records must be added manually }); //--------------------------------------------------------------------------- // ALB const alb = new elbv2.ApplicationLoadBalancer(this, generateName('ALB'), { vpc, internetFacing: true, }); const listener = alb.addListener('Listener', { open: true, certificates: [cert], protocol: elbv2.ApplicationProtocol.HTTPS, }); // Connect ecsService to TargetGroup const targetGroup = listener.addTargets(generateName('LaravelTargetGroup'), { protocol: elbv2.ApplicationProtocol.HTTP, targets: [ecsService] }); new cdk.CfnOutput(this, generateName('AlbDnsName'), { exportName: generateName('AlbDnsName'), value: alb.loadBalancerDnsName, }); new cdk.CfnOutput(this, generateName('ActionCname'), { exportName: generateName('ActionCname'), value: 'Please setup a CNAME record mylaravel.' + deploymentEnv.domainName + ' to ' + alb.loadBalancerDnsName, }); new cdk.CfnOutput(this, generateName('ActionVisit'), { exportName: generateName('ActionVisit'), value: 'Visit https://mylaravel.' + deploymentEnv.domainName, }); //--------------------------------------------------------------------------- // ecsService: Application Auto Scaling const scaling = ecsService.autoScaleTaskCount({ minCapacity: 2, maxCapacity: 10 }); scaling.scaleOnCpuUtilization('CpuScaling', { targetUtilizationPercent: 50 }); scaling.scaleOnRequestCount('RequestScaling', { requestsPerTarget: 30, targetGroup: targetGroup }); } }