Add CloudWatch logs to your Docker Container

Take control of your application logs with CloudWatch!

Take control of your application logs with CloudWatch!

Logging is probably one of the most important things we can do as developers but it's often the most overlooked.  You never think about it until you need it.  Logging can be simple and it can also be incredibly complex.  You may ask yourself:

  • Where should I log it?
  • What tools and services should I use?
  • How long should I keep the log?
  • ... and the list goes on

At the end of the day, as long as you are logging something and you have access to your logs, you have a better chance at figuring out what went wrong.  You can always fine-tune the details later, the important thing is that you have them readily at your figure tips.

... as long as you ... have access to your logs, you have a better chance of figuring out what went wrong. 

In this post, I'm going to show you how easy it is to wire up a docker container to log to CloudWatch. 

In this example, I'm going to assume the following:

  1. You are using an EC2 instance
  2. You have docker installed on the EC2 instance and you're running a container

The next two are items are requirements as well but we'll cover them here:

  1. You have an EC2 instance with the CloudWatch agent running.
  2. You have the correct permissions via a role on the EC2 instance.

The CloudWatch Agent

Use the following scripts to install the CloudWatch agent.  I typically include this in my user-data scripts to automatically install on the EC2 instance

# get the package
sudo wget https://s3.amazonaws.com/amazoncloudwatch-agent/amazon_linux/amd64/latest/amazon-cloudwatch-agent.rpm

# install the agent
sudo rpm -U ./amazon-cloudwatch-agent.rpm

# start it
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -m ec2 -a start

# make sure it's running
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -m ec2 -a status

The AWS IAM Permissions

In order for your EC2 instance to submit logs through the CloudWatch agent, the agent must have permission to do so. The easiest way to accomplish this is to add them to a Role that the EC2 instance has assigned to it.

The minimum permissions are the following:

"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogGroup"
 
 

An example of the policy would look like this:


{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
      "logs:PutLogEvents"
    ],
      "Resource": [
        "arn:aws:logs:*:*:*"
    ]
  }
 ]
}

AWS CDK

If you are using the AWS CDK it would look something like this:

using Amazon.CDK.AWS.IAM;
...
private PolicyStatement GenerateWriteAccess()
{
var statementProps = new PolicyStatementProps
{
Effect = Effect.ALLOW,
Actions = new string[] {
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogGroup"
},
Resources = new string[] { "*" }
};

var statement = new PolicyStatement(statementProps);

return statement;
}

Docker Compose

Finally, Add the logging options to your docker-compose file:

logging:
driver: "awslogs"
options:
awslogs-stream: "THE_STREAM_NAME"
awslogs-group: "THE_LOG_GROUP_NAME"
awslogs-region: "THE_AWS_REGION"
awslogs-create-group: "true"

THE_STREAM_NAME is the name of the logging stream. Like a file name. Mine are typically in the form of the docker-service name and the EC2 instance Id, like web-api-service-instance-id.

THE_LOG_GROUP_NAME is the name that you see under the CloudWatch Log groups section and have /'s in them to help differentiate them.  Mine are typically in the form of env/app-name like:

  • dev/company/app-name
  • prod/company-name/app-name

THE_AWS_REGION is the AWS region you are logging them to e.g. us-east-1


I typically have a shell script that dynamically creates my docker-compose file based on environment variables. The variables are loaded and written to the file similar to the script below.

You'll see in some sections, I'm actually embedding or swapping out fillers for the environment variables (see the __VARS__ and sed command).  I'm doing this out of convenience to make sure the compose file doesn't have any problems but you could use the environment variables directly - assuming you make sure they are always set.


docker_dir="/app/docker"

################################################################################################################################
# APP Setup

## Add application scripts here such as a docker-compose creation

sudo mkdir -p "${docker_dir}"
sudo cat > "${docker_dir}/docker-compose.yml" << 'EOF'
version: "3.7"
services:
reverseproxy:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ports:
- "80:80"
logging:
driver: "awslogs"
options:
awslogs-stream: "nginx-__INSTANCE_ID__"
awslogs-group: "__ENVIRONMENT__/__PROJECT_NAME__"
awslogs-region: "__REGION__"
awslogs-create-group: "true"
restart: always
web-service:
image: __DOCKER_IMAGE__
container_name: web-site
volumes:
- ${MEDIA_PATH}:/app/www/wwwroot/uploads
environment:
ENVIRONMENT : __ENVIRONMENT__
APP_LOG_PATH: /app/log/
ASPNETCORE_ENVIRONMENT: __ENVIRONMENT__
ASPNETCORE_URLS: http://+:5000
APP_DB_CONNECTION_STRING: ${APP_DB_CONNECTION_STRING}
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: ${DB_NAME}
logging:
driver: "awslogs"
options:
awslogs-stream: "web-service-__INSTANCE_ID__"
awslogs-group: "__ENVIRONMENT__/__PROJECT_NAME__"
awslogs-region: "__REGION__"
awslogs-create-group: "true"
ports:
- "5000:5000"
depends_on:
- reverseproxy
expose:
- "5000"
restart: always
EOF


# replace the __PLACEHOLDERS__ with the variable
# getting an issue with / as delimeter, switching to pipe
sudo sed -i "s|__DOCKER_IMAGE__|${DOCKER_IMAGE}|g" "${docker_dir}/docker-compose.yml"
sudo sed -i "s|__PROJECT_NAME__|${PROJECT// /}|g" "${docker_dir}/docker-compose.yml"
sudo sed -i "s|__ENVIRONMENT__|${ENVIRONMENT}|g" "${docker_dir}/docker-compose.yml"
sudo sed -i "s|__REGION__|${REGION}|g" "${docker_dir}/docker-compose.yml"
sudo sed -i "s|__INSTANCE_ID__|${INSTANCE_ID}|g" "${docker_dir}/docker-compose.yml"


### switch to the correct directory
cd "${docker_dir}"
### launch it

### login to ecr
aws ecr get-login-password --region ${DOCKER_REPO_REGION} | docker login --username AWS --password-stdin ${DOCKER_REPO}

docker-compose up -d

# / App Setup
###############################################################################################################################

If all goes well, you should have something in your cloud logs that looks something like the following

Side note: the cloudwatch-agent and user-data logs streams shown above were not covered by this post.  Those were configured through the amazon-cloudwatch-agent.json configuration settings of the CloudWatch agent.

Leave a comment

Please note that we won't show your email to others, or use it for sending unwanted emails. We will only use it to render your Gravatar image and to validate you as a real person.