import * as cdk from '@aws-cdk/core'; import * as ec2 from '@aws-cdk/aws-ec2'; // import ec2 library import * as iam from '@aws-cdk/aws-iam'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as elbvtargets from '@aws-cdk/aws-elasticloadbalancingv2-targets'; import * as ssm from '@aws-cdk/aws-ssm'; import * as kms from '@aws-cdk/aws-kms'; export class InfraCdkappMysqlStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); //Variables const volumeEncryptionKey = ssm.StringParameter.valueForStringParameter(this, '/acme/cdk/ec2/VolumeKmsKeyId'); var servername = ssm.StringParameter.valueForStringParameter(this, '/acme/cdk/ec2/app-mysql/servername'); const environment = ssm.StringParameter.valueForStringParameter(this, '/acme/cdk/ec2/app-mysql/environment'); const serverkey = ssm.StringParameter.valueForStringParameter(this, '/acme/cdk/ec2/app-mysql/serverkey'); const serverdescription = ssm.StringParameter.valueForStringParameter(this, '/acme/cdk/ec2/app-mysql/serverdescription'); var instancetypeparam = ssm.StringParameter.valueForStringParameter(this, '/acme/cdk/ec2/app-mysql/instancetype'); const domainJoinDocument = ssm.StringParameter.valueForStringParameter(this, '/acme/cdk/ec2/app-mysql/domainJoinDocument'); const directoryID = ssm.StringParameter.valueForStringParameter(this, '/acme/cdk/ec2/app-mysql/directoryId'); var volumeDsize = 100; var volumeCsize = 50; const application = "appMysql"; const createdBy = "cloud-operations@acme.com"; const os = "Windows"; const legalEntitiy = "Acme AS"; const country = "Country"; //Set default value to be t3.medium var instancetype = ec2.InstanceType.of( ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM ); //Check the instancetypeparam from ssm param store and change the instance type accordingly if (instancetypeparam == "m5.large") { instancetype = ec2.InstanceType.of( ec2.InstanceClass.M5, ec2.InstanceSize.LARGE ) } //Create vpc object const vpc = ec2.Vpc.fromLookup(this, 'VPC', { vpcName: 'acme-standard-vpc' }); // var subnet1Id = cdk.Fn.importValue('acme-standard-vpc-private-sn-1'); var subnet1Id = "subnet-1237385573857398"; const subnet = vpc.selectSubnets({ subnets: [ ec2.Subnet.fromSubnetAttributes(this, 'subnet1', { subnetId: subnet1Id, availabilityZone: 'eu-west-1a' }) ] }); var publicSubnet1Id = "subnet-98765432123456"; const publicSubnet = vpc.selectSubnets({ subnets: [ ec2.Subnet.fromSubnetAttributes(this, 'publicSubnet1', { subnetId: publicSubnet1Id, availabilityZone: 'eu-west-1a' }) ] }); //Network load balancer const nlb = new elbv2.NetworkLoadBalancer(this, 'NLB', { vpc, internetFacing: true, vpcSubnets: publicSubnet, loadBalancerName: "app-mysql-nlb", crossZoneEnabled: false }); cdk.Tags.of(nlb).add("Name", servername + "-network-load-balancer"); cdk.Tags.of(nlb).add("Application", application); cdk.Tags.of(nlb).add("Description", "network-load-balancer-for-" + serverdescription); const role = new iam.Role(this, 'app-mysql-server-role', { assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') }); role.addManagedPolicy(iam.ManagedPolicy.fromManagedPolicyArn(this, "AmazonSSMAutomationRole", "arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole")); role.addManagedPolicy(iam.ManagedPolicy.fromManagedPolicyArn(this, "AmazonSSMManagedInstanceCore", "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore")); const appPolicy = { "Version": "2012-10-17", "Statement": [ { "Sid": "appIamInstancePolicy", "Effect": "Allow", "Action": [ "ec2:DescribeInstances", "ssm:ListTagsForResource", "ds:CreateComputer" ], "Resource": "*" }, { "Sid": "AttachVolume", "Effect": "Allow", "Action": [ "ec2:AttachVolume" ], "Resource": "*" }, { "Sid": "AssumeKMSRole", "Effect": "Allow", "Action": [ "sts:AssumeRole" ], "Resource": "arn:aws:iam::2222222222222:role/*" }, { "Sid": "GetKMSKey", "Effect": "Allow", "Action": [ "kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:DescribeKey", "kms:*" ], "Resource": "*" }, { "Sid": "GetSecretValue", "Effect": "Allow", "Action": "secretsmanager:GetSecretValue", "Resource": [ "arn:aws:secretsmanager:eu-west-1:111111111111:secret:domainJoinUserAD-test", "arn:aws:secretsmanager:eu-west-1:333333333333:secret:domainJoinUserAD-stage", "arn:aws:secretsmanager:eu-west-1:666666666666:secret:domainJoinUserAD-prod", "arn:aws:secretsmanager:eu-west-1:111111111111:secret:DefaultPasswordForWindowsServer-test", "arn:aws:secretsmanager:eu-west-1:333333333333:secret:DefaultPasswordForWindowsServer-stage", "arn:aws:secretsmanager:eu-west-1:444444444444:secret:DefaultPasswordForWindowsServer-prod" ] }, { "Sid": "DomainJoinComputer", "Effect": "Allow", "Action": [ "ds:CreateComputer" ], "Resource": [ "arn:aws:ds:eu-west-1:" + process.env.account + ":directory/" + directoryID ], } ] }; const appPolicyDocument = iam.PolicyDocument.fromJson(appPolicy); var appInstancePolicy = new iam.Policy(this, 'app-instance-policy', { policyName: "appIamInstancePolicy", document: appPolicyDocument }); appInstancePolicy.attachToRole(role); // lets create a security group for our instance // A security group acts as a virtual firewall for your instance to control inbound and outbound traffic. const securityGroup = new ec2.SecurityGroup(this, 'app-mysql-server-sg', { vpc: vpc, allowAllOutbound: true, // will let your instance send outboud traffic securityGroupName: 'app-mysql-server-sg', } ) cdk.Tags.of(securityGroup).add("Name", servername + "-security-group"); cdk.Tags.of(securityGroup).add("Application", application); cdk.Tags.of(securityGroup).add("Description", "security-group-for-" + serverdescription); // lets use the security group to allow inbound traffic on specific ports from different sources //allow mysql from a larger subnet cidr securityGroup.addIngressRule( ec2.Peer.ipv4("172.10.0.0/20"), ec2.Port.tcp(3306), 'Allows mysql access from AWSCIDR' ) //allow rdp from specific ip, for instance jumpbox securityGroup.addIngressRule( ec2.Peer.ipv4("172.10.0.55/32"), ec2.Port.tcp(3389), 'Allows rdp access from jumpbox based on ip' ) //allow rdp from another instance based on its security group id securityGroup.addIngressRule(ec2.SecurityGroup.fromSecurityGroupId(this, "jumpboxSG", "sg-12345678987654"), ec2.Port.tcp(3389), 'allow rdp access based on the jumpboxs security group id'); //get the public ip of the actuall machine you are doing this deployment from and allow rdp and mysql from that using the public-ip packet const publicIp = require('public-ip'); async function waitForIP() { var ip = await publicIp.v4(); // console.log(ip); return ip; } function setLaptopIngressRule() { waitForIP().then(result => securityGroup.addIngressRule(ec2.Peer.ipv4(result+"/32"), ec2.Port.tcp(3389), 'Allows rdp access from laptop public')); } function setLaptopIngressRuleMysql() { waitForIP().then(result => securityGroup.addIngressRule(ec2.Peer.ipv4(result+"/32"), ec2.Port.tcp(3306), 'Allows mysql access from laptop public')); } //initialize the functions that creates the ingress rules setLaptopIngressRule(); setLaptopIngressRuleMysql(); //Get the correct ami from your region for the Windows 2019 base server and put the region and ami id here const windows = ec2.MachineImage.latestWindows(ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE); const genericWindows = ec2.MachineImage.genericWindows({ 'eu-west-1': 'ami-87776hhg65f5' }); //define encrypted volume that would be used as D drive const dVolume = new ec2.Volume(this, 'DataDisk', { availabilityZone: 'eu-west-1a', size: cdk.Size.gibibytes(volumeDsize), encrypted: true, volumeType: ec2.EbsDeviceVolumeType.GP3, encryptionKey: kms.Key.fromKeyArn(this, 'VolumeEncryptionKey', volumeEncryptionKey), }); //apply tags to the volume cdk.Tags.of(dVolume).add("Name", servername + "-d-volume"); cdk.Tags.of(dVolume).add("Application", application); cdk.Tags.of(dVolume).add("Description", "d-volume-for-" + serverdescription); cdk.Tags.of(dVolume).add("CreatedBy", createdBy); //if extra custom userdata is needed set them here and uncommend the userdata on the instance const userdataScript = "true"; const userData = ec2.UserData.forWindows(); userData.addCommands(userdataScript); //set device name for the volume that would be used as D drive const targetDevice = '/dev/sdb'; //define the initData that will be used when creating the instance, this describes all the commands that will be ran on the instance const initData = ec2.CloudFormationInit.fromElements( // ec2.InitFile.fromUrl("c:\\cfn\\mysql.msi", "https://downloads.mysql.com/archives/get/p/25/file/mysql-installer-community-8.0.24.0.msi"), ec2.InitFile.fromAsset("c:\\cfn\\BootstrapScript.ps1", "./BootstrapScript.ps1"), ec2.InitFile.fromAsset("c:\\cfn\\CreateADGroups.ps1", "./CreateADGroups.ps1"), ec2.InitPackage.msi("https://s3.amazonaws.com/aws-cli/AWSCLI64.msi"), ec2.InitCommand.shellCommand('powershell.exe [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://chocolatey.org/install.ps1\'))', { key: "1-InstallChoco", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(5)) }), ec2.InitCommand.shellCommand("powershell.exe -Command Rename-Computer (Get-EC2Tag -Filter @{Name='resource-id'; Values=(Invoke-WebRequest http://169.254.169.254/latest/meta-data/instance-id -UseBasicParsing).Content}).Where({$_.Key -eq 'Name'}).Value", { key: "2-RenameComputer", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(5)) }), ec2.InitCommand.shellCommand('powershell.exe -Command Restart-Service AmazonSSMAgent"', { key: "3-RestartSSMAgent", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(5)) }), ec2.InitCommand.shellCommand('powershell.exe Add-EC2Volume -InstanceId (Invoke-WebRequest http://169.254.169.254/latest/meta-data/instance-id -UseBasicParsing).Content -VolumeId ' + dVolume.volumeId + ' -Device ' + targetDevice + ' -Region ' + process.env.CDK_DEFAULT_REGION, { key: "4-AttachVolume", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(60)) }), ec2.InitCommand.shellCommand('powershell.exe -File "c:\\cfn\\BootstrapScript.ps1"', { key: "5-install", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(10)) }), ec2.InitCommand.shellCommand('powershell.exe -Command Send-SSMCommand -InstanceId (Invoke-WebRequest http://169.254.169.254/latest/meta-data/instance-id -UseBasicParsing).Content -DocumentName ' + domainJoinDocument + ' -TimeoutSecond 600 -Region ' + process.env.CDK_DEFAULT_REGION, { key: "6-DomainJoinServer", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(60)) }), ec2.InitCommand.shellCommand('powershell.exe -Command Restart-Computer -force', { key: "6-Restart", waitAfterCompletion: ec2.InitCommandWaitDuration.forever() }), // // ec2.InitCommand.shellCommand('powershell.exe -File "c:\\cfn\\CreateADGroups.ps1"', { key: "7-CreateADGroups", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(5)) }), ec2.InitCommand.shellCommand('powershell.exe -Command Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled False', { key: "7-DisableFirewall", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(10)) }), ec2.InitCommand.shellCommand('powershell.exe -File C:\\ProgramData\\Amazon\\EC2-Windows\\Launch\\Scripts\\InitializeDisks.ps1', { key: "8-Initialize", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(5)) }), // ec2.InitCommand.shellCommand('msiexec /q /log c:\\cfn\\log\\mysql-install.log /i "C:\\cfn\\mysql.msi"', { key: "91-installMysql", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(30)) }), ec2.InitCommand.shellCommand('powershell.exe choco install mysql -y', { key: "9-installMysql", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(30)) }), ec2.InitCommand.shellCommand('cfn-signal.exe -e %ERRORLEVEL% --resource appmysqlserver --stack ' + this.stackId + ' --region ' + this.region, { key: "91-Signal", waitAfterCompletion: ec2.InitCommandWaitDuration.of(cdk.Duration.seconds(5)) }) ) //create a boolean variable to be able to deploy and undeploy the instance for troubleshooting purposes var deployInstance = false; var instance: ec2.Instance; if (deployInstance) { // Finally lets provision our ec2 instance instance = new ec2.Instance(this, "app-mysql-server-instance", { vpc: vpc, role: role, securityGroup: securityGroup, instanceName: servername, instanceType: instancetype, blockDevices: [ { deviceName: '/dev/sda1', volume: ec2.BlockDeviceVolume.ebs(volumeCsize), } ], machineImage: genericWindows, vpcSubnets: subnet, // userData: userData, //userdata is in this case not passed along as we dont need the persist tag since cfn-signal is done via init command // privateIpAddress: privateIp, init: initData, initOptions: { timeout: cdk.Duration.minutes(35), ignoreFailures: false, }, keyName: serverkey, // we will create this in the console before we deploy }) //allow for the volume to be attached to the instance dVolume.grantAttachVolume(instance); //override the logicalid instance.instance.overrideLogicalId('appmysqlserver'); //apply tags to the volume cdk.Tags.of(instance).add("Name", servername); cdk.Tags.of(instance).add("Application", application); cdk.Tags.of(instance).add("Description", serverdescription); cdk.Tags.of(instance).add("CreatedBy", createdBy); cdk.Tags.of(instance).add("Environment", environment); cdk.Tags.of(instance).add("OS", os); cdk.Tags.of(instance).add("Country", country); cdk.Tags.of(instance).add("Legal Entity", legalEntitiy); // cdk.Tags.of(instance).add("", ); // Add a listener on a mysql port. const listener = nlb.addListener('Listener', { port: 3306, protocol: elbv2.Protocol.TCP }); //apply tags to the listender cdk.Tags.of(listener).add("Name", servername + "-mysql-listener"); cdk.Tags.of(listener).add("Application", application); cdk.Tags.of(listener).add("Description", "listener-for-" + serverdescription); //add target to the listender, the target is the instance on the mysql port listener.addTargets('app-instance-mysql-target', { port: 3306, targets: [new elbvtargets.InstanceIdTarget(instance.instanceId)] }) //add a listener on the rdp port const rdpListener = nlb.addListener('RDP-listender', { port: 3389, protocol: elbv2.Protocol.TCP }); //apply tags to the listener cdk.Tags.of(rdpListener).add("Name", servername + "-rdp-listener"); cdk.Tags.of(rdpListener).add("Application", application); cdk.Tags.of(rdpListener).add("Description", "listener-for-" + serverdescription); //add rdp target to the listener, the target is the instance on the rdp port rdpListener.addTargets('app-instance-rdp-target', { port: 3389, targets: [new elbvtargets.InstanceIdTarget(instance.instanceId)] }) new cdk.CfnOutput(this, 'app-nlb-dns-name', { value: nlb.loadBalancerDnsName }) new cdk.CfnOutput(this, 'app-mysql-private-ip', { value: instance.instancePrivateIp }) }//end if deployInstance } }