Infrastructure as code in the same language as your code

The fast-paced micro-services era requires efficient and scalable infrastructure. Traditional infrastructure management methods involve manual tasks, are time-consuming, and are prone to unintentional configuration changes (configuration drift). Infrastructure as Code (IaC) and immutable infrastructure offer a new approach that revolutionizes how we build and deploy infrastructure. AWS CDK is an example of such an IaC framework. Developed by Amazon and maintained as an open-source project, AWS CDK enables developers to define scalable infrastructure using high-level programming languages like Java. Using AWS CDK,

  • You can write the infrastructure in the same language as your service,
  • Synthesize it into AWS CloudFormation, then
  • Provision physical resources from the resulting CloudFormation stacks.

Let’s illustrate its use through a practical example

Lambda deployer project

This project contains two components

  • A lambda handler that accepts a JSON object (a receipt), then saves this object as an object in S3
  • CDK infrastructure code

Directory Structure

save-receipt-function
save-receipts-infra
src
main
java
cdk
save-receipts-lambda
src
main
java
lambda

Lambda Handler code

Pretty simple logic

  • Accepts input,
  • Save in s3, then
  • Respond with the location of the receipt and 201 status code.
public class SaveReceiptFunction implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

private static final AmazonS3 S3_CLIENT = AmazonS3ClientBuilder.defaultClient();
private static final String BUCKET_NAME = "digital-receipts-dev";

public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
final LambdaLogger logger = context.getLogger();

final Map<String, String> headers = createResponseHeaderMap(context);

final String objectKey = String.format("%s.json", context.getAwsRequestId());
final String content = input.getBody();

APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
.withHeaders(headers);
try {

S3_CLIENT.putObject(BUCKET_NAME, objectKey, content);

return response
.withStatusCode(201);
} catch (final Exception e) {
logger.log(e.getLocalizedMessage());
return response
.withStatusCode(500);
}
}

private Map<String, String> createResponseHeaderMap(Context context) {
final Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("Location", String.format("/receipt/%s", context.getAwsRequestId()));
return headers;
}
}

Infrastructure code (aka CDK)

VPC stack that defines,

  • Non-default VPC with
    - CIDR block 10.16.0.0/16
    - Number of nat gateways required
    - Subnet configurations
  • Lambda function with java11 and size 512 inside the above VPC
  • S3 bucket with S3_MANAGED encryption
  • API gateway endpoint
  • IAM roles to grant access between
    - Lambda and S3
    - API gateway and lambda

The code is broken into the main VPCStack class and other fine-grained creator classes focus on creating certain resources. For instance

  • LambdaFunctionCreator
  • LambdaRestApiCreator
  • S3BucketCreator
  • SubnetCreator

Note: While granting access from lambda to S3 is explicit

bucket.grantPut(Objects.requireNonNull(saveReceiptsHandler.getRole()));

The access between the API gateway and lambda is more subtle and activated using the handler method of the RestApibuilder.

final LambdaRestApi lambdaRestApi = Builder.create(scope, endpoint)
.handler(function)
.proxy(proxy)
.restApiName(endpoint)
.deployOptions(StageOptions.builder().stageName(stageName).build())
.build();

Here is the Stack

public class VpcStack extends Stack {

private static final String CIDR = "10.16.0.0/16";
private static final int CIDR_MASK = 20;

public VpcStack(final Construct parent, final String id) {
this(parent, id, null);
}

public VpcStack(final Construct parent, final String id, final StackProps props) {
super(parent, id, props);
final String env = System.getenv("ENV");
final String vpcId = String.format("receipts-%s-vpc", env);

final Vpc vpc = Vpc.Builder.create(this, vpcId)
.cidr(CIDR)
.subnetConfiguration(createSubnetConfigurations())
.natGateways(1)
.natGatewayProvider(NatProvider.gateway())
.build();

Tags.of(vpc).add("env", env);

final Function saveReceiptsHandler = createLambdaFunction(vpc, env);

final Bucket bucket = createBucket(this, String.format("digital-receipts-%s", env), BLOCK_ALL, S3_MANAGED, false, false);

bucket.grantPut(Objects.requireNonNull(saveReceiptsHandler.getRole()));

createLambdaRestApi(this, saveReceiptsHandler, "ReceiptsEndpoint", "dev", false, "receipt", Lists.newArrayList("POST"));
}

private Function createLambdaFunction(final Vpc vpc, final String env) {
final String saveReceiptsHandler = "SaveReceiptsHandler";
final String handler = "lambda.SaveReceiptFunction";
final AssetCode assetCode = fromAsset("../save-receipts-lambda/target/save-receipts-lambda-0.1.jar");
final Function saveReceiptsFunction = createFunction(this, saveReceiptsHandler, handler, JAVA_11, assetCode, vpc);

Tags.of(saveReceiptsFunction).add("env", env);
return saveReceiptsFunction;
}

private List<? extends SubnetConfiguration> createSubnetConfigurations() {
final SubnetConfiguration zoneAReceiptPublicSubnet = new SubnetConfiguration.Builder()
.cidrMask(CIDR_MASK)
.subnetType(SubnetType.PUBLIC)
.name("receipt-public")
.build();

final SubnetConfiguration zoneBReceiptPublicSubnet = new SubnetConfiguration.Builder()
.cidrMask(CIDR_MASK)
.subnetType(SubnetType.PRIVATE)
.name("receipt-private")
.build();

return newArrayList(zoneAReceiptPublicSubnet, zoneBReceiptPublicSubnet);
}
}

Finally, an Application class that works as the entry point

public final class Application {

public static void main(final String... args) {
final App app = new App();
final VpcStack vpcStack = new VpcStack(app, "CreateVPCStack");
Tags.of(vpcStack).add("env", "dev");
app.synth();
}
}

~/.aws/credentials Changes

Append your ~/.aws/config with the following snippet

[profile receipts-dev]
region=ap-southeast-2
output=json
account=::************ # while ************ is management account
role_arn=arn:aws:iam::************:role/OrganizationAccountAccessRole # while ************ is your development account
source_profile=default
role_session_name=your_preferred_session_name

Deployment

cd /path/to/save-receipt-function
mvn clean package
cd save-receipts-cdk
cdk bootstrap --profile receipts-dev
cdk deploy --profile receipts-dev

Resulting CloudFormation template

What will this create in AWS

  • VPC
  • 4 subnets (two private and two public)
  • Nat gateway
  • S3 bucket
  • Lambda function to save files in S3
  • Api gateway endpoint to allow clients call lambda as a rest endpoint.
  • Roles and policies to allow RestApi call lambda and lambda to put objects in S3

Useful commands

  • mvn package compile and run tests
  • cdk ls list all stacks in the app
  • cdk synth emits the synthesized CloudFormation template
  • cdk deploy deploys this stack to your default AWS account/region
  • cdk diff compare deployed stack with current state
  • cdk docs open CDK documentation

Information has been prepared for information purposes only and does not constitute advice.

--

--