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 block10.16.0.0/16
- Number of nat gateways required
- Subnet configurations - Lambda function with
java11
and size512
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 RestApi
builder.
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 testscdk ls
list all stacks in the appcdk synth
emits the synthesized CloudFormation templatecdk deploy
deploys this stack to your default AWS account/regioncdk diff
compare deployed stack with current statecdk docs
open CDK documentation
Information has been prepared for information purposes only and does not constitute advice.