Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,10 @@ jobs:
cd infrastructure
cdk diff --all

- name: Deploy Infrastructure to AWS
- name: Deploy Infrastructure (Phase 1 - Without ECS Service)
run: |
cd infrastructure
echo "Deploying infrastructure without ECS service (allows image push first)..."
cdk deploy --all --require-approval never
env:
CDK_DEFAULT_ACCOUNT: ${{ vars.AWS_ACCOUNT_ID }}
Expand Down Expand Up @@ -155,6 +156,15 @@ jobs:

echo "Image pushed to ECR: $ECR_REPOSITORY:latest"

- name: Deploy Infrastructure (Phase 2 - With ECS Service)
run: |
cd infrastructure
echo "Deploying ECS service now that image is in ECR..."
cdk deploy TodosEcsStack --require-approval never -c createEcsService=true
env:
CDK_DEFAULT_ACCOUNT: ${{ vars.AWS_ACCOUNT_ID }}
CDK_DEFAULT_REGION: ${{ vars.AWS_REGION }}

- name: Verify Deployment
run: |
echo "Verifying ECS service deployment..."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,47 +22,71 @@

/**
* ECS Fargate stack for running the Todos application. Deploys the native image container in
* private subnets with secure configuration.
* private subnets with secure configuration. ECS service creation is optional to allow image push
* before service deployment.
*/
public class EcsStack extends Stack {

private final FargateService service;
private final Repository ecrRepository;
private final Cluster cluster;
private final FargateTaskDefinition taskDefinition;

/**
* Creates a new EcsStack with Fargate service for the Todos application.
* Creates a new EcsStack with optional Fargate service for the Todos application.
*
* @param scope the parent construct
* @param id the construct ID
* @param props the stack properties
* @param vpc the VPC to deploy into
* @param securityGroup the security group for the service
* @param keyspaceName the Keyspaces database name
* @param createService whether to create the ECS service (false for initial deploy before image
* push)
*/
public EcsStack(
final Construct scope,
final String id,
final StackProps props,
final IVpc vpc,
final ISecurityGroup securityGroup,
final String keyspaceName) {
final String keyspaceName,
final boolean createService) {
super(scope, id, props);

this.ecrRepository = createEcrRepository();
Cluster cluster = createCluster(vpc);
FargateTaskDefinition taskDefinition = createTaskDefinition(keyspaceName);
this.service = createService(cluster, taskDefinition, securityGroup);

// Output service ARN and ECR repository URI for reference
CfnOutput.Builder.create(this, "ServiceArn")
.value(service.getServiceArn())
.description("ECS Service ARN for Todos application")
.build();

this.cluster = createCluster(vpc);
this.taskDefinition = createTaskDefinition(keyspaceName);

// Only create service if requested (allows ECR push before service creation)
if (createService) {
this.service = createService(cluster, taskDefinition, securityGroup);

// Output service ARN when service is created
CfnOutput.Builder.create(this, "ServiceArn")
.value(service.getServiceArn())
.description("ECS Service ARN for Todos application")
.build();
} else {
this.service = null;
// Output instructions for next deployment step
CfnOutput.Builder.create(this, "NextSteps")
.value("Push Docker image to ECR, then redeploy with -c createEcsService=true")
.description("Instructions for completing deployment")
.build();
}

// Always output ECR repository URI
CfnOutput.Builder.create(this, "EcrRepositoryUri")
.value(ecrRepository.getRepositoryUri())
.description("ECR Repository URI for Todos application image")
.build();

// Output cluster name for reference
CfnOutput.Builder.create(this, "ClusterName")
.value(cluster.getClusterName())
.description("ECS Cluster name")
.build();
}

/** Creates ECR repository for the Todos application image. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ public KeyspacesStack(final Construct scope, final String id, final StackProps p
CfnKeyspace keyspace = createKeyspace();
this.keyspaceName = keyspace.getKeyspaceName();

createChecklistTable(keyspace);
createTodoTable(keyspace);
CfnTable checklistTable = createChecklistTable(keyspace);
CfnTable todoTable = createTodoTable(keyspace);

// Explicitly declare CloudFormation dependencies to ensure keyspace is created first
checklistTable.addDependency(keyspace);
todoTable.addDependency(keyspace);

// Output keyspace name for application configuration
CfnOutput.Builder.create(this, "KeyspaceName")
Expand All @@ -46,8 +50,8 @@ private CfnKeyspace createKeyspace() {
}

/** Creates checklist table matching the domain model. */
private void createChecklistTable(final CfnKeyspace keyspace) {
CfnTable.Builder.create(this, "ChecklistTable")
private CfnTable createChecklistTable(final CfnKeyspace keyspace) {
return CfnTable.Builder.create(this, "ChecklistTable")
.keyspaceName(keyspace.getKeyspaceName())
.tableName("checklist")
.partitionKeyColumns(
Expand All @@ -69,8 +73,8 @@ private void createChecklistTable(final CfnKeyspace keyspace) {
}

/** Creates todo table matching the domain model. */
private void createTodoTable(final CfnKeyspace keyspace) {
CfnTable.Builder.create(this, "TodoTable")
private CfnTable createTodoTable(final CfnKeyspace keyspace) {
return CfnTable.Builder.create(this, "TodoTable")
.keyspaceName(keyspace.getKeyspaceName())
.tableName("todo")
.partitionKeyColumns(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,32 @@
import java.util.List;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;
import software.amazon.awscdk.services.ec2.GatewayVpcEndpointAwsService;
import software.amazon.awscdk.services.ec2.GatewayVpcEndpointOptions;
import software.amazon.awscdk.services.ec2.IVpc;
import software.amazon.awscdk.services.ec2.InterfaceVpcEndpointAwsService;
import software.amazon.awscdk.services.ec2.InterfaceVpcEndpointOptions;
import software.amazon.awscdk.services.ec2.Port;
import software.amazon.awscdk.services.ec2.SecurityGroup;
import software.amazon.awscdk.services.ec2.SubnetConfiguration;
import software.amazon.awscdk.services.ec2.SubnetSelection;
import software.amazon.awscdk.services.ec2.SubnetType;
import software.amazon.awscdk.services.ec2.Vpc;
import software.constructs.Construct;

/**
* Network infrastructure stack for Todos application. Creates VPC with private subnets only - no
* public access for security.
* public access for security. Uses VPC endpoints instead of NAT Gateway for cost-effective AWS
* service access.
*/
public class NetworkStack extends Stack {

private final IVpc vpc;
private final SecurityGroup privateSecurityGroup;
private final SecurityGroup vpcEndpointSecurityGroup;

/**
* Creates a new NetworkStack with private-only networking infrastructure.
* Creates a new NetworkStack with private-only networking infrastructure and VPC endpoints.
*
* @param scope the parent construct
* @param id the construct ID
Expand All @@ -31,6 +39,13 @@ public NetworkStack(final Construct scope, final String id, final StackProps pro

this.vpc = createVpc();
this.privateSecurityGroup = createPrivateSecurityGroup();
this.vpcEndpointSecurityGroup = createVpcEndpointSecurityGroup();

// Create VPC endpoints for AWS services (no NAT Gateway needed)
createS3GatewayEndpoint();
createEcrEndpoints();
createCloudWatchLogsEndpoint();
createKeyspacesEndpoint();
}

/**
Expand Down Expand Up @@ -65,19 +80,101 @@ private SecurityGroup createPrivateSecurityGroup() {
.build();

// Allow internal communication within the security group
sg.addIngressRule(
sg,
software.amazon.awscdk.services.ec2.Port.allTraffic(),
"Allow internal communication within security group");
sg.addIngressRule(sg, Port.allTraffic(), "Allow internal communication within security group");

return sg;
}

/**
* Creates security group for VPC endpoints. Allows HTTPS traffic from application security group.
*/
private SecurityGroup createVpcEndpointSecurityGroup() {
SecurityGroup sg =
SecurityGroup.Builder.create(this, "VpcEndpointSecurityGroup")
.vpc(vpc)
.description("Security group for VPC endpoints")
.allowAllOutbound(false)
.build();

// Allow HTTPS from application security group to VPC endpoints (ECR, CloudWatch, etc.)
sg.addIngressRule(privateSecurityGroup, Port.tcp(443), "Allow HTTPS from application");

// Allow Keyspaces port from application (Cassandra uses port 9142 with TLS)
sg.addIngressRule(privateSecurityGroup, Port.tcp(9142), "Allow Cassandra from application");

return sg;
}

/** Creates S3 Gateway endpoint for ECR image layers (no cost, no hourly charge). */
private void createS3GatewayEndpoint() {
vpc.addGatewayEndpoint(
"S3GatewayEndpoint",
GatewayVpcEndpointOptions.builder()
.service(GatewayVpcEndpointAwsService.S3)
.subnets(
List.of(SubnetSelection.builder().subnetType(SubnetType.PRIVATE_ISOLATED).build()))
.build());
}

/** Creates ECR VPC endpoints for Docker registry access without NAT Gateway. */
private void createEcrEndpoints() {
// ECR API endpoint - for Docker client API calls
vpc.addInterfaceEndpoint(
"EcrApiEndpoint",
InterfaceVpcEndpointOptions.builder()
.service(InterfaceVpcEndpointAwsService.ECR)
.subnets(SubnetSelection.builder().subnetType(SubnetType.PRIVATE_ISOLATED).build())
.securityGroups(List.of(vpcEndpointSecurityGroup))
.privateDnsEnabled(true)
.build());

// ECR Docker endpoint - for pulling Docker images
vpc.addInterfaceEndpoint(
"EcrDockerEndpoint",
InterfaceVpcEndpointOptions.builder()
.service(InterfaceVpcEndpointAwsService.ECR_DOCKER)
.subnets(SubnetSelection.builder().subnetType(SubnetType.PRIVATE_ISOLATED).build())
.securityGroups(List.of(vpcEndpointSecurityGroup))
.privateDnsEnabled(true)
.build());
}

/** Creates CloudWatch Logs endpoint for application logging without NAT Gateway. */
private void createCloudWatchLogsEndpoint() {
vpc.addInterfaceEndpoint(
"CloudWatchLogsEndpoint",
InterfaceVpcEndpointOptions.builder()
.service(InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS)
.subnets(SubnetSelection.builder().subnetType(SubnetType.PRIVATE_ISOLATED).build())
.securityGroups(List.of(vpcEndpointSecurityGroup))
.privateDnsEnabled(true)
.build());
}

/**
* Creates Keyspaces (Cassandra) endpoint for database access without NAT Gateway. Keyspaces
* requires HTTPS (port 9142 uses TLS).
*/
private void createKeyspacesEndpoint() {
vpc.addInterfaceEndpoint(
"KeyspacesEndpoint",
InterfaceVpcEndpointOptions.builder()
.service(InterfaceVpcEndpointAwsService.KEYSPACES)
.subnets(SubnetSelection.builder().subnetType(SubnetType.PRIVATE_ISOLATED).build())
.securityGroups(List.of(vpcEndpointSecurityGroup))
.privateDnsEnabled(true)
.build());
}

public IVpc getVpc() {
return vpc;
}

public SecurityGroup getPrivateSecurityGroup() {
return privateSecurityGroup;
}

public SecurityGroup getVpcEndpointSecurityGroup() {
return vpcEndpointSecurityGroup;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,28 @@ public static void main(final String[] args) {
Environment env = createEnvironment();
StackProps stackProps = StackProps.builder().env(env).build();

// Read context to determine if ECS service should be created
// Default to false for initial deployment (allows ECR image push first)
Object contextValue = app.getNode().tryGetContext("createEcsService");
boolean createEcsService =
contextValue != null && Boolean.parseBoolean(contextValue.toString());

// Create network infrastructure (VPC, subnets, security groups) - private only
NetworkStack networkStack = new NetworkStack(app, "TodosNetworkStack", stackProps);

// Create Keyspaces database
KeyspacesStack keyspacesStack = new KeyspacesStack(app, "TodosKeyspacesStack", stackProps);

// Create ECS service for application (private subnets only)
// Create ECS stack (ECR + optional service based on context parameter)
EcsStack ecsStack =
new EcsStack(
app,
"TodosEcsStack",
stackProps,
networkStack.getVpc(),
networkStack.getPrivateSecurityGroup(),
keyspacesStack.getKeyspaceName());
keyspacesStack.getKeyspaceName(),
createEcsService);

ecsStack.addDependency(networkStack);
ecsStack.addDependency(keyspacesStack);
Expand Down
Loading