Wednesday, May 7, 2025

Deploying Apache Tomcat and Running a WAR File on AWS ECS

Deploying Apache Tomcat and Running a WAR File on AWS ECS

Author: Harvinder Singh Saluja
Tags: #AWS #ECS #Tomcat #DevOps #Java #WARDeployment


Modern applications demand scalable and resilient infrastructure. Apache Tomcat, a popular Java servlet container, can be containerized and deployed on AWS ECS (Elastic Container Service) for high availability and manageability. In this blog, we walk through the end-to-end process of containerizing Tomcat with a custom WAR file and deploying it on AWS ECS using Fargate.


Objective

To deploy a Java .war file under Tomcat on AWS ECS Fargate and access the web application through an Application Load Balancer (ALB).


Prerequisites

  • AWS Account

  • Docker installed locally

  • AWS CLI configured

  • An existing .war file (e.g., myapp.war)

  • Basic understanding of ECS, Docker, and networking on AWS


Step 1: Create Dockerfile for Tomcat + WAR

Create a Dockerfile to extend the official Tomcat image and copy the WAR file into the webapps directory.

# Use official Tomcat base image
FROM tomcat:9.0

# Remove default ROOT webapp
RUN rm -rf /usr/local/tomcat/webapps/ROOT

# Copy custom WAR file
COPY myapp.war /usr/local/tomcat/webapps/ROOT.war

# Expose port
EXPOSE 8080

# Start Tomcat
CMD ["catalina.sh", "run"]

Place this Dockerfile alongside your myapp.war.


Step 2: Build and Push Docker Image to Amazon ECR

  1. Create ECR Repository

aws ecr create-repository --repository-name tomcat-myapp
  1. Authenticate Docker with ECR

aws ecr get-login-password | docker login --username AWS --password-stdin <aws_account_id>.dkr.ecr.<region>.amazonaws.com
  1. Build and Push Docker Image

docker build -t tomcat-myapp .
docker tag tomcat-myapp:latest <aws_account_id>.dkr.ecr.<region>.amazonaws.com/tomcat-myapp:latest
docker push <aws_account_id>.dkr.ecr.<region>.amazonaws.com/tomcat-myapp:latest

Step 3: Setup ECS Cluster and Fargate Service

  1. Create ECS Cluster

aws ecs create-cluster --cluster-name tomcat-cluster
  1. Create Task Definition JSON

Example: task-def.json

{
  "family": "tomcat-task",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "name": "tomcat-container",
      "image": "<aws_account_id>.dkr.ecr.<region>.amazonaws.com/tomcat-myapp:latest",
      "portMappings": [
        {
          "containerPort": 8080,
          "hostPort": 8080,
          "protocol": "tcp"
        }
      ],
      "essential": true
    }
  ],
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::<account_id>:role/ecsTaskExecutionRole"
}
  1. Register Task Definition

aws ecs register-task-definition --cli-input-json file://task-def.json
  1. Create Security Group & ALB

    • Create a security group allowing HTTP (port 80) and custom port 8080.

    • Create an Application Load Balancer with a target group pointing to port 8080.

  2. Run ECS Fargate Service

aws ecs create-service \
  --cluster tomcat-cluster \
  --service-name tomcat-service \
  --task-definition tomcat-task \
  --desired-count 1 \
  --launch-type FARGATE \
  --network-configuration '{
      "awsvpcConfiguration": {
          "subnets": ["subnet-xxxxxxx"],
          "securityGroups": ["sg-xxxxxxx"],
          "assignPublicIp": "ENABLED"
      }
  }' \
  --load-balancers '[
      {
          "targetGroupArn": "arn:aws:elasticloadbalancing:<region>:<account_id>:targetgroup/<target-group-name>",
          "containerName": "tomcat-container",
          "containerPort": 8080
      }
  ]'

Step 4: Access the Deployed App

Once the ECS service stabilizes, navigate to the DNS name of the ALB (e.g., http://<alb-dns-name>) to access your Java application running on Tomcat.


Troubleshooting Tips

  • WAR not deploying? Make sure it's named ROOT.war if you want it accessible directly at /.

  • Service unhealthy? Confirm security group rules allow traffic on port 8080.

  • Task failing? Check ECS task logs in CloudWatch.


A CloudFormation Template (CFT) was revised to deploy Apache Tomcat on ECS Fargate using private subnets. In this version:

  • Tomcat runs in private subnets

  • Application Load Balancer (ALB) resides in public subnets

  • NAT Gateway is used to allow ECS tasks to access the internet (e.g., for downloading updates)

  • WAR file is pre-packaged into the Docker image

  • Load Balancer forwards traffic to ECS service running in private subnets


tomcat-on-ecs-private.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy Tomcat in ECS Fargate with WAR file under private subnets and public-facing ALB

Parameters:
  VpcCidr:
    Type: String
    Default: 10.0.0.0/16
  PublicSubnet1Cidr:
    Type: String
    Default: 10.0.1.0/24
  PublicSubnet2Cidr:
    Type: String
    Default: 10.0.2.0/24
  PrivateSubnet1Cidr:
    Type: String
    Default: 10.0.3.0/24
  PrivateSubnet2Cidr:
    Type: String
    Default: 10.0.4.0/24
  ImageUrl:
    Type: String
    Description: ECR image URL for the Tomcat + WAR image

Resources:

  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true

  # Subnets
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PublicSubnet1Cidr
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      MapPublicIpOnLaunch: true

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PublicSubnet2Cidr
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      MapPublicIpOnLaunch: true

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PrivateSubnet1Cidr
      AvailabilityZone: !Select [ 0, !GetAZs '' ]

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PrivateSubnet2Cidr
      AvailabilityZone: !Select [ 1, !GetAZs '' ]

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicRouteAssoc1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicRouteAssoc2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  # NAT Gateway setup for private subnets
  EIP:
    Type: AWS::EC2::EIP

  NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt EIP.AllocationId
      SubnetId: !Ref PublicSubnet1

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway

  PrivateRouteAssoc1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateRouteAssoc2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

  # Security Group
  ECSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow inbound traffic from ALB
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          SourceSecurityGroupId: !Ref ALBSecurityGroup

  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP from internet
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0

  # ALB
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Subnets: [!Ref PublicSubnet1, !Ref PublicSubnet2]
      SecurityGroups: [!Ref ALBSecurityGroup]
      Scheme: internet-facing
      Type: application

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: 8080
      Protocol: HTTP
      VpcId: !Ref VPC
      TargetType: ip
      HealthCheckPath: /
      HealthCheckPort: 8080

  Listener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      LoadBalancerArn: !Ref LoadBalancer
      Port: 80
      Protocol: HTTP

  ECSCluster:
    Type: AWS::ECS::Cluster

  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: tomcat-task
      Cpu: 512
      Memory: 1024
      NetworkMode: awsvpc
      RequiresCompatibilities: [FARGATE]
      ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: tomcat-container
          Image: !Ref ImageUrl
          PortMappings:
            - ContainerPort: 8080
          Essential: true

  ECSService:
    Type: AWS::ECS::Service
    DependsOn: Listener
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: 1
      LaunchType: FARGATE
      TaskDefinition: !Ref TaskDefinition
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          Subnets: [!Ref PrivateSubnet1, !Ref PrivateSubnet2]
          SecurityGroups: [!Ref ECSSecurityGroup]
      LoadBalancers:
        - TargetGroupArn: !Ref TargetGroup
          ContainerName: tomcat-container
          ContainerPort: 8080

Outputs:
  ALBDNS:
    Description: DNS of the Application Load Balancer
    Value: !GetAtt LoadBalancer.DNSName

Deploy Instructions

  1. Save this file as tomcat-on-ecs-private.yaml

  2. Deploy using AWS CLI:

aws cloudformation deploy \
  --template-file tomcat-on-ecs-private.yaml \
  --stack-name tomcat-private-ecs \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides ImageUrl=<your-ecr-image-url>
  1. Once stack creation is complete, access the application via the ALB DNS output.


Would you like this exported to PDF, or want a GitHub Actions pipeline to automate container builds and deployments?


Conclusion

You’ve now deployed a containerized Tomcat server running a WAR application to AWS ECS using Fargate. This setup abstracts away server management, allowing you to focus on your application logic while AWS handles the infrastructure.


 

Amazon Sagemaker Studio

Amazon SageMaker Studio is an integrated development environment (IDE) for machine learning that provides everything data scientists and dev...