Skip to content

Instantly share code, notes, and snippets.

@flacial
Last active March 4, 2023 15:34
Show Gist options
  • Save flacial/a06f0078693f622c24caf742872301e1 to your computer and use it in GitHub Desktop.
Save flacial/a06f0078693f622c24caf742872301e1 to your computer and use it in GitHub Desktop.
CDK — API Gateawy with VCP Integration to EC2

Overview

The following document goes through how to write an CDK stack that creates an EC2 instance and integrate it with an API Gateway through a VPC link and create a CI/CD pipeline with CodePipeline, CodeDeploy, and CodeCommit.

Steps

  1. Create an EC2 instance
    1. Create a VPC instance
    2. Create a key pair for SSH access
    3. Create an instance by specifying its subnets, type, and image
    4. Set the security group
      1. Allow port 80 to be accessed from the VPC block
      2. Enable SSH access
    5. Optional: execute a startup script to quickly setup the server on port 80
  2. Create a target group
    1. Connect it with the EC2 instance and VPC from step 1
    2. Set the protocol as TCP for it to work with the network load balancer
  3. Create a network load balancer (nlb)
    1. Add a listener for TCP protocol and port 80
    2. Connect it with the target group from step 3
  4. Create a REST API Gateway
    1. Create a VPC link
      1. Set that target as the created nlb
    2. Create a VPC integration
    3. Create a REST API instance
    4. Add a proxy resource and an ANY method to the API
    5. Create a stage with variables
  5. Create a CodePipeline

CDK code

Read more about CodePipeline code

The IP for the network load balancer is the same for the VPC.

import * as cdk from 'aws-cdk-lib'
import * as apiGateway from 'aws-cdk-lib/aws-apigateway'
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as targets from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets'
import {
  NetworkLoadBalancer,
  NetworkTargetGroup,
  Protocol,
  TargetType,
} from 'aws-cdk-lib/aws-elasticloadbalancingv2'
import * as codedeploy from 'aws-cdk-lib/aws-codedeploy'
import * as codecommit from 'aws-cdk-lib/aws-codecommit'
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'
import * as codepipelineActions from 'aws-cdk-lib/aws-codepipeline-actions'
import * as iam from 'aws-cdk-lib/aws-iam'
import { HttpMethod } from '@aws-cdk/aws-apigatewayv2-alpha'
import { Construct } from 'constructs'
import { readFileSync } from 'fs'

const VPC_IP = '10.0.0.0/16'

const createEC2Instance = (parentThis: Construct) => {
  // create VPC in which we'll launch the Instance
  const vpc = new ec2.Vpc(parentThis, 'my-cdk-vpc', {
    ipAddresses: ec2.IpAddresses.cidr(VPC_IP),
    natGateways: 0,
    subnetConfiguration: [
      { name: 'public', cidrMask: 24, subnetType: ec2.SubnetType.PUBLIC },
    ],
  })

  // create a key pair for ssh access
  const keyPair = new ec2.CfnKeyPair(parentThis, 'key-pair', {
    keyName: 'MyKeyPairB',
  })

  const role = new iam.Role(parentThis, 'MyRole', {
    assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
    managedPolicies: [
      iam.ManagedPolicy.fromManagedPolicyArn(
        parentThis,
        'ManagedPolicy',
        'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole'
      ),
      iam.ManagedPolicy.fromManagedPolicyArn(
        parentThis,
        'ManagedPolicy2',
        'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforAWSCodeDeploy'
      ),
    ],
  })

  // create the EC2 Instance
  const ec2Instance = new ec2.Instance(parentThis, 'ec2-instance', {
    vpc,
    vpcSubnets: {
      subnetType: ec2.SubnetType.PUBLIC,
    },
    instanceType: ec2.InstanceType.of(
      ec2.InstanceClass.T2,
      ec2.InstanceSize.MICRO
    ),
    machineImage: new ec2.GenericLinuxImage({
      // ubuntu image AMI ID
      // from https://cloud-images.ubuntu.com/locator/ec2/
      'us-east-1': 'ami-0557a15b87f6559cf',
    }),
    keyName: keyPair.keyName,
    role,
    instanceName: 'CdkInstanceVfinal',
  })

  // bash script to quickly setup the server
  const userDataScript = readFileSync('./lib/startup.sh', 'utf8')
  // add the User Data script to the Instance
  ec2Instance.addUserData(userDataScript)

  return { ec2Instance, vpc }
}

const createTargetGroup = (
  parentThis: Construct,
  vpc: cdk.aws_ec2.IVpc,
  ec2Instance: cdk.aws_ec2.Instance
) => {
  return new NetworkTargetGroup(parentThis, 'target-group', {
    // Has to be TCP in order to work with the load balancer
    protocol: Protocol.TCP,
    port: 80,
    targetType: TargetType.INSTANCE,
    healthCheck: {
      protocol: Protocol.HTTP,
      path: '/health',
    },
    // EC2 instances
    targets: [new targets.InstanceTarget(ec2Instance)],
    vpc,
  })
}

const createNlb = (
  parentThis: Construct,
  vpc: cdk.aws_ec2.Vpc,
  targetGroup: cdk.aws_elasticloadbalancingv2.NetworkTargetGroup
) => {
  // create a network load balancer
  const nlb = new NetworkLoadBalancer(parentThis, 'nlb', {
    loadBalancerName: 'MyLoadBalancer-cdk',
    vpc,
    internetFacing: true,
  })

  // create a new listener
  const listener = nlb.addListener('Listener', {
    port: 80,
    protocol: Protocol.TCP,
    defaultTargetGroups: [targetGroup],
  })

  return { nlb, listener }
}

const setEc2SecurityGroup = (
  parentThis: Construct,
  vpc: cdk.aws_ec2.Vpc,
  ec2Instance: cdk.aws_ec2.Instance
) => {
  // create a Security Group for the Instance
  const SecurityGroup = new ec2.SecurityGroup(parentThis, 'ec2-sg', {
    vpc,
    allowAllOutbound: true,
  })

  // enable SSH
  SecurityGroup.addIngressRule(
    ec2.Peer.anyIpv4(),
    ec2.Port.tcp(22),
    'allow SSH access from anywhere'
  )

  // enable HTTP for the load balancer only
  SecurityGroup.addIngressRule(
    ec2.Peer.ipv4(VPC_IP),
    ec2.Port.tcp(80),
    'allow HTTP traffic from anywhere'
  )

  ec2Instance.addSecurityGroup(SecurityGroup)

  return SecurityGroup
}

const createRestApiWithVpc = (
  parentThis: Construct,
  nlb: cdk.aws_elasticloadbalancingv2.NetworkLoadBalancer
) => {
  // create vpc link
  const link = new apiGateway.VpcLink(parentThis, 'vpc-link', {
    // list of load balancers
    targets: [nlb],
  })

  // create vpc integration
  const vpcIntegration = new apiGateway.Integration({
    type: apiGateway.IntegrationType.HTTP_PROXY,
    integrationHttpMethod: HttpMethod.ANY,
    options: {
      connectionType: apiGateway.ConnectionType.VPC_LINK,
      vpcLink: link,
      // sets the integration {proxy} parameter as the method request's {proxy}
      requestParameters: {
        'integration.request.path.proxy': 'method.request.path.proxy',
      },
    },
    uri: 'http://${stageVariables.VPCNLB}/{proxy}',
  })

  // create new REST API
  const restApi = new apiGateway.RestApi(parentThis, 'vpc-api')

  // add proxy resource and an ANY method
  const proxy = restApi.root.addProxy({
    anyMethod: false,
  })

  // create ANY method with vpc integration
  proxy.addMethod(HttpMethod.ANY, vpcIntegration, {
    requestParameters: {
      // allow {proxy} parameter to be set
      'method.request.path.proxy': true,
    },
  })

  // create stage deployment instance
  const deployment = new apiGateway.Deployment(parentThis, 'Deployment', {
    api: restApi,
  })

  // create a new stage with environment variables
  new apiGateway.Stage(parentThis, 'main-stage', {
    deployment,
    stageName: 'main',
    variables: {
      VPCNLB: nlb.loadBalancerDnsName,
    },
  })

  // prints REST API stage invoke URL
  new cdk.CfnOutput(parentThis, 'Rest API URL', { value: restApi.url })
}

const createCommitRepo = (parentThis: Construct) =>
  new codecommit.Repository(parentThis, 'CodeCommitRepo', {
    repositoryName: 'MyCdkRepo',
  })

const setupCodeDeployApp = (
  parentThis: Construct,
  targetGroup: cdk.aws_elasticloadbalancingv2.NetworkTargetGroup
) => {
  const application = new codedeploy.ServerApplication(
    parentThis,
    'CodeDeployApplication'
  )

  // create deployment group
  const deploymentGroup = new codedeploy.ServerDeploymentGroup(
    parentThis,
    'CodeDeployDeploymentGroup',
    {
      application,
      deploymentGroupName: 'MyDeploymentGroup',
      // adds EC2 instances matching tags
      ec2InstanceTags: new codedeploy.InstanceTagSet({
        // any instance with tags satisfying
        // will match this group
        Name: ['CdkInstanceVfinal'],
      }),
      loadBalancer: codedeploy.LoadBalancer.network(targetGroup),
      // auto creates a role for it
      // role: <someRole>
    }
  )

  return { application, deploymentGroup }
}

const setupCodePipeline = (
  parentThis: Construct,
  application: cdk.aws_codedeploy.ServerApplication,
  deploymentGroup: cdk.aws_codedeploy.ServerDeploymentGroup,
  commitRepo: cdk.aws_codecommit.Repository
) => {
  const pipeline = new codepipeline.Pipeline(parentThis, 'MyPipeline', {
    // If this is true, our CodeCommit repo in S3 will be encrypted
    // and the deploying stage will fail because it can't decrypt the files
    // couldn't find a solution yet
    crossAccountKeys: false,
  })

  // create source & deploy stages
  const sourceStage = pipeline.addStage({
    stageName: 'Source',
  })
  const deployStage = pipeline.addStage({
    stageName: 'Deploy',
  })

  const sourceOutput = new codepipeline.Artifact('source')

  const sourceStageAction = new codepipelineActions.CodeCommitSourceAction({
    repository: codecommit.Repository.fromRepositoryName(
      parentThis,
      'CodeRepo',
      commitRepo.repositoryName
    ),
    actionName: 'Source',
    // based on my understanding, it stores the project code
    // after pulling/ cloning it into a store(artifact)
    // then the deploy stage takes the artifact or our project code as an input
    output: sourceOutput,
  })

  // set the action of what to happen in this stage
  sourceStage.addAction(sourceStageAction)

  const deployStageAction =
    new codepipelineActions.CodeDeployServerDeployAction({
      deploymentGroup:
        codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(
          parentThis,
          'Deployment-group',
          {
            application,
            deploymentGroupName: deploymentGroup.deploymentGroupName,
          }
        ),
      actionName: 'Deploy',
      input: sourceOutput,
    })

  // set the action of what to happen in this stage
  deployStage.addAction(deployStageAction)

  return pipeline
}

export class CdkStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const { ec2Instance, vpc } = createEC2Instance(this)
    const targetGroup = createTargetGroup(this, vpc, ec2Instance)
    const { nlb } = createNlb(this, vpc, targetGroup)

    setEc2SecurityGroup(this, vpc, ec2Instance)

    createRestApiWithVpc(this, nlb)

    // CodeCommit
    const commitRepo = createCommitRepo(this)

    // CodeDeploy
    const { application, deploymentGroup } = setupCodeDeployApp(
      this,
      targetGroup
    )

    // CodePipeline
    setupCodePipeline(this, application, deploymentGroup, commitRepo)

    // outputs the repo URL in the terminal
    new cdk.CfnOutput(this, 'apiUrl', {
      value: commitRepo.repositoryCloneUrlHttp,
    })
  }
}

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment