Preface

This article will talk through how I went about provisioning a basic AWS EC2 based application, making use of AWS RDS as a data store along with using an AWS ALB for simple HTTP to HTTPS redirection and Application Load Balancing.

Architecture diagram

VPC (Virtual Private Network)

Declare a basic VPC using a specific CIDR (Classless inter-domain routing) range and declare it as type public.

    const vpc = new ec2.Vpc(
      this, 
      'system-vpc',
      {
        cidr: "10.101.0.0/16",
        natGateways: 0,
        subnetConfiguration: [
          {
            cidrMask: 24,
            name: 'system-ingress',
            subnetType: ec2.SubnetType.PUBLIC,
          },
        ],
      }
    );

This is specifically for my use case, to make best use of AWS free tier. The easiest way to declare a VPC is shown below, as easy as that. It will create a best practice VPC configuration... in one line! Complete with subnets and route tables.

const vpc = new ec2.Vpc(this, 'VPC');

Auto Scaling Group + EC2 Instances

    // Pick the right Amazon Linux edition. All arguments shown are optional
    // and will default to these values when omitted.
    const amznLinux = ec2.MachineImage.latestAmazonLinux(
      {
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
        edition: ec2.AmazonLinuxEdition.STANDARD,
        virtualization: ec2.AmazonLinuxVirt.HVM,
        storage: ec2.AmazonLinuxStorage.GENERAL_PURPOSE,
      }
    );
  
    const asgSG = new ec2.SecurityGroup(
      this, 
      'ASGSecurityGroup', 
      { 
        vpc, 
        securityGroupName: 'ASGSecurityGroup',
        description: 'Base SG for HTTP Traffic'
      }
    );
    
    const asg = new autoscaling.AutoScalingGroup(
      this,
      'sys-ASG',
      {
        vpc,
        instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MICRO),
        machineImage: amznLinux,
        minCapacity: 1,
        maxCapacity: 1,
        associatePublicIpAddress: false,
        securityGroup: asgSG
      }
    );
    

    // Append these SGs to the instance

    asg.addUserData(
      "yum -y update",
      "yum -y install amazon-linux-extras",
      "yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm",
      "yum -y install http://rpms.remirepo.net/enterprise/remi-release-7.rpm",
      "yum -y install yum-utils",
      "yum-config-manager --enable remi-php56",
      "yum -y install php56 php56 php56-php-bcmath php56-php-cli php56-php-common php56-php-gd php56-php-pecl-jsonc php56-php-mbstring php56-php-mcrypt php56-php-mysqlnd php56-php-pdo php56-php-process php56-php-xml",
      "yum -y install httpd",
      "yum -y install mariadb",
      "echo '<html><body>Healthy</body></html>' > /var/www/html/index.html",
      "systemctl enable httpd",
      "systemctl start httpd",
      "echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCnpJF0IOs35WGtRQEQVWJUZBGF2EqjaT3SuuengbxXLscLUKOT3V2SfsB0Rd+h0WBfVJfuHtCKvMEvpjna1ZJyAisusc4MWBZTLOEHX7fsMlz+Hg0/VVrpDNCGvPcMm4NE5ghyXT/CqYdMe1l1FbAaJNJI3A/sWuYIrYKwtBko11cTcgzQCFH/qEyDM/KpjQKhoju+rNfTt8ACwOpoeefdpPKjBbQzcYORUNgBtbwmN06xpWGK3wMqkWzNCOl26/N7txeu6DJ6FHi+7RynUhZSaKFUhumFqlKoRJbiNx0aGoTIj/bJ8mPltDm1rwmx0xgwtaQ4JORdiK0+cP7r5E0v ansible@dc0-ansible' >> /home/ec2-user/.ssh/authorized_keys",
      "echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRjugjN0II8EWNdE77rbWYAEOhjSQBWZHCti53BujqTo+kIVIT7TSjA1Bn9xanU8bvUzYh0P/JVMjwTT8rGt2nOahXCw+hPLuRc7te6CeAX7BIbOsx5dKzEtgaYJQvwcA2EevCGKWQvWlLPP8s3nXp+00AyPpGBA6Jthxo6k1/I1nmM8Hrtafsa1QPB4fTStfrEjGvUSZPVN+Usa7Z4Zhzl7RA6wQRlX3UUK6x57ZPyjbKvR20ICW7n5hgKB9QOjtlsvUr6QrQb1gpOYQ+wGMGXGoZ6zslOjYEwXcq4+4lEkux7grnRAcAgZyigha25P78n23Gmqckqqpn531CpcoX jenkins@dc0-jenkins-p1a' >> /home/ec2-user/.ssh/authorized_keys",
      "systemctl restart sshd"
    )

RDS (Relational Database Service) Instance

The below creates a simple RDS instance that has both private and public access.

I have external access as I use an on premise Jenkins instance to run Liquibase against the database and perform nightly backups. This same Jenkins instance runs another job to whitelist my Dynamic IP address, which is my current ISP provided IP address and revokes any other entries.

    const mysqlSecurityGroup = new ec2.SecurityGroup(
      this,
      'system-sg-mysql',
      {
        vpc,
        securityGroupName: 'RDSSecurityGroupMYSQL',
        description: 'Allow MySQL access to RDS instance from VPC',
        allowAllOutbound: true   // Can be set to false
      }
    );
    mysqlSecurityGroup.addIngressRule(ec2.Peer.ipv4("10.101.0.0/16"), ec2.Port.tcp(3306), 'Allow MySQL access from the VPC');

    const mysqlExternalSecurityGroup = new ec2.SecurityGroup(
      this,
      'system-sg-mysql-ext',
      {
        vpc,
        securityGroupName: 'RDSSecurityGroupMYSQLEXT',
        description: 'Allow MySQL access to RDS instance externally',
        allowAllOutbound: true   // Can be set to false
      }
    );
    mysqlExternalSecurityGroup.addIngressRule(ec2.Peer.ipv4("10.0.0.0/16"), ec2.Port.tcp(3306), 'Allow MySQL access from on-prem');

    const mySQLRDSInstance = new rds.DatabaseInstance(
      this,
      'mysql-rds-instance',
      {
        engine: rds.DatabaseInstanceEngine.MARIADB,
        instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
        vpc: vpc,
        vpcPlacement: {subnetType: ec2.SubnetType.PUBLIC},
        securityGroups: [mysqlSecurityGroup, mysqlExternalSecurityGroup],
        storageEncrypted: false,
        multiAz: false,
        autoMinorVersionUpgrade: false,
        allocatedStorage: 20,
        storageType: rds.StorageType.GP2,
        deletionProtection: false,
        databaseName: 'db_change_log',
        port: 3306
      }
    );

ALB (Application Load Balancer)

Below snippet creates an ALB along with provisioning the required certificate to allow HTTPS traffic. It also allows HTTP traffic and does the automatic redirection from all HTTP connections to HTTPS.

There are also some key security modifications too, this is to allow only HTTP (Port: 80) traffic from the ALB to the EC2/AutoScaling Group. The ALB allows all HTTP & HTTPS traffic.

    const acmCert = new acm.Certificate(
      this,
      'Certificate',
      {
        domainName: '*.exampple.com',
        validation: acm.CertificateValidation.fromDns(), // Records must be added manually
      }
    );

    const albSGDefault = new ec2.SecurityGroup(
      this, 
      'ALBSecurityGroupDefault', 
      {
        vpc,
        securityGroupName: 'ALBSecurityGroupDefault',
        description: 'Base SG for ALB ALL Traffic external'
      }
    );
    const albSGSSH = new ec2.SecurityGroup(
      this, 
      'ALBSecurityGroupSSH', 
      {
        vpc,
        securityGroupName: 'ALBSecurityGroupSSH',
        description: 'Base SG for ALB SSH Traffic external'
      }
    );
    albSGDefault.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP Traffic from anywhere');
    albSGDefault.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'Allow HTTP Traffic from anywhere');
    // Create the load balancer in a VPC. 'internetFacing' is 'false'
    // by default, which creates an internal load balancer.
    const alb = new elbv2.ApplicationLoadBalancer(
      this,
      'system-ALB',
      {
        vpc,
        internetFacing: true,
        securityGroup: albSGDefault
      }
    );
    alb.addSecurityGroup(albSGSSH);

    // ASG allow traffic from ALBs
    asgSG.addIngressRule(albSGDefault, ec2.Port.tcp(80), 'Allow HTTP Traffic from ALB');

    alb.addRedirect({
      sourceProtocol: elbv2.ApplicationProtocol.HTTP,
      sourcePort: 80,
      targetProtocol: elbv2.ApplicationProtocol.HTTPS,
      targetPort: 443,
    });

    const albhttpslistener = alb.addListener(
      'HTTPSListener',
      {
        port: 443,
        open: false
      }
    );
    albhttpslistener.addCertificates(
      'main-cert', 
      [
        acmCert
      ]
    );
    albhttpslistener.addTargets(
      'sys-ASG-fleet-https',
      {
        port: 80,
        targets: [asg]
      }
    );

Automatic ACM certificate validation

In this example, the domain I was using is externally hosted from AWS so that required the validation step to be done manually via DNS records. You can skip this step if you're using AWS Route53 for domain management. It will automatically put the records in place using the following code snippet instead of the ACM step above.

const myHostedZone = new route53.HostedZone(this, 'HostedZone', {
  zoneName: 'example.com',
});
new acm.Certificate(this, 'Certificate', {
  domainName: 'hello.example.com',
  validation: acm.CertificateValidation.fromDns(myHostedZone),
});

In Conclusion

You can definitely see how powered with CDK as your toolkit you can very quickly bring together a basic LAMP stack application with a few code snippets and then running the cdk deploy process, this will then generate the Cloudformation template and apply it to the current account.

I have included the full code of the example used above for reference.

atownsend247/cdk-aws-system-initial-setup
Contribute to atownsend247/cdk-aws-system-initial-setup development by creating an account on GitHub.


Reference Links